feat(geschichten): show blog writers' own drafts on the Geschichten overview (#807) (#813)
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m48s
CI / OCR Service Tests (push) Successful in 22s
CI / Backend Unit Tests (push) Successful in 5m24s
CI / fail2ban Regex (push) Successful in 53s
CI / Semgrep Security Scan (push) Successful in 23s
CI / Compose Bucket Idempotency (push) Successful in 1m9s
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m48s
CI / OCR Service Tests (push) Successful in 22s
CI / Backend Unit Tests (push) Successful in 5m24s
CI / fail2ban Regex (push) Successful in 53s
CI / Semgrep Security Scan (push) Successful in 23s
CI / Compose Bucket Idempotency (push) Successful in 1m9s
This commit was merged in pull request #813.
This commit is contained in:
@@ -7,12 +7,13 @@ import type { components } from '$lib/generated/api';
|
||||
|
||||
type GeschichteRow = Pick<
|
||||
components['schemas']['GeschichteSummary'],
|
||||
'id' | 'title' | 'body' | 'type' | 'author' | 'publishedAt'
|
||||
'id' | 'title' | 'body' | 'type' | 'status' | 'author' | 'publishedAt'
|
||||
>;
|
||||
|
||||
let { geschichte }: { geschichte: GeschichteRow } = $props();
|
||||
|
||||
const isJourney = $derived(geschichte.type === 'JOURNEY');
|
||||
const isDraft = $derived(geschichte.status === 'DRAFT');
|
||||
|
||||
const publishedAt = $derived(formatPublishedAt(geschichte.publishedAt, 'short'));
|
||||
|
||||
@@ -44,12 +45,20 @@ const authorName = $derived(formatAuthorName(geschichte.author));
|
||||
{m.journey_badge_list()}
|
||||
</span>
|
||||
{/if}
|
||||
{#if isDraft}
|
||||
<span
|
||||
data-testid="draft-badge"
|
||||
class="inline-flex items-center rounded-sm border border-line bg-canvas px-1.5 py-px font-sans text-xs font-bold tracking-wide text-ink-3 uppercase"
|
||||
>
|
||||
{m.geschichten_draft_badge()}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Content column -->
|
||||
<div class="min-w-0 flex-1 p-3 sm:px-4">
|
||||
<!-- Compact meta line (mobile only) -->
|
||||
<div class="mb-1 flex items-center gap-1.5 sm:hidden">
|
||||
<div class="mb-1 flex flex-wrap items-center gap-1.5 sm:hidden">
|
||||
<!-- 7px initials render as smudge at this size — a plain color dot reads better -->
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -57,6 +66,14 @@ const authorName = $derived(formatAuthorName(geschichte.author));
|
||||
style="background-color: {personAvatarColor(authorName)}"
|
||||
></span>
|
||||
<span class="font-sans text-sm font-semibold text-ink">{authorName}</span>
|
||||
{#if isDraft}
|
||||
<span
|
||||
data-testid="draft-badge-mobile"
|
||||
class="inline-flex shrink-0 items-center rounded-sm border border-line bg-canvas px-1.5 py-px font-sans text-xs font-bold tracking-wide text-ink-3 uppercase"
|
||||
>
|
||||
{m.geschichten_draft_badge()}
|
||||
</span>
|
||||
{/if}
|
||||
{#if publishedAt}
|
||||
<span class="ml-auto font-sans text-sm text-ink-3">{publishedAt}</span>
|
||||
{/if}
|
||||
|
||||
@@ -91,4 +91,34 @@ describe('GeschichteListRow', () => {
|
||||
render(GeschichteListRow, { props: { geschichte: baseRow() } });
|
||||
expect(document.body.textContent).toContain('Anna Schmidt');
|
||||
});
|
||||
|
||||
it('shows no draft badge for PUBLISHED stories', async () => {
|
||||
render(GeschichteListRow, { props: { geschichte: baseRow({ status: 'PUBLISHED' }) } });
|
||||
expect(document.querySelector('[data-testid="draft-badge"]')).toBeNull();
|
||||
expect(document.querySelector('[data-testid="draft-badge-mobile"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('shows desktop draft badge for DRAFT stories', async () => {
|
||||
render(GeschichteListRow, { props: { geschichte: baseRow({ status: 'DRAFT' }) } });
|
||||
const badge = document.querySelector('[data-testid="draft-badge"]');
|
||||
expect(badge).not.toBeNull();
|
||||
});
|
||||
|
||||
it('shows mobile draft badge for DRAFT stories', async () => {
|
||||
render(GeschichteListRow, { props: { geschichte: baseRow({ status: 'DRAFT' }) } });
|
||||
const badge = document.querySelector('[data-testid="draft-badge-mobile"]');
|
||||
expect(badge).not.toBeNull();
|
||||
});
|
||||
|
||||
it('draft badge is a plain <span>', async () => {
|
||||
render(GeschichteListRow, { props: { geschichte: baseRow({ status: 'DRAFT' }) } });
|
||||
const badge = document.querySelector('[data-testid="draft-badge"]');
|
||||
expect(badge?.tagName.toLowerCase()).toBe('span');
|
||||
});
|
||||
|
||||
it('draft badge uses text-xs label size', async () => {
|
||||
render(GeschichteListRow, { props: { geschichte: baseRow({ status: 'DRAFT' }) } });
|
||||
const badge = document.querySelector('[data-testid="draft-badge"]');
|
||||
expect(badge!.className).toContain('text-xs');
|
||||
});
|
||||
});
|
||||
|
||||
37
frontend/src/lib/shared/server/settled.test.ts
Normal file
37
frontend/src/lib/shared/server/settled.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { settled } from './settled';
|
||||
|
||||
describe('settled', () => {
|
||||
it('returns the data for a fulfilled ok response', () => {
|
||||
const res: PromiseSettledResult<unknown> = {
|
||||
status: 'fulfilled',
|
||||
value: { response: { ok: true } as Response, data: [{ id: '1' }] }
|
||||
};
|
||||
expect(settled<{ id: string }[]>(res)).toEqual([{ id: '1' }]);
|
||||
});
|
||||
|
||||
it('returns null for a fulfilled non-ok response', () => {
|
||||
const res: PromiseSettledResult<unknown> = {
|
||||
status: 'fulfilled',
|
||||
value: { response: { ok: false, status: 403 } as Response, data: undefined }
|
||||
};
|
||||
expect(settled(res)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for a rejected result', () => {
|
||||
const res: PromiseSettledResult<unknown> = {
|
||||
status: 'rejected',
|
||||
reason: new Error('network error')
|
||||
};
|
||||
expect(settled(res)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for undefined input', () => {
|
||||
expect(settled(undefined)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for a fulfilled null value (Promise.resolve(null) placeholder slot)', () => {
|
||||
const res: PromiseSettledResult<unknown> = { status: 'fulfilled', value: null };
|
||||
expect(settled(res)).toBeNull();
|
||||
});
|
||||
});
|
||||
5
frontend/src/lib/shared/server/settled.ts
Normal file
5
frontend/src/lib/shared/server/settled.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function settled<T>(res: PromiseSettledResult<unknown> | undefined): T | null {
|
||||
if (res?.status !== 'fulfilled') return null;
|
||||
const v = res.value as { response: Response; data: unknown } | null;
|
||||
return v?.response?.ok ? ((v.data as T) ?? null) : null;
|
||||
}
|
||||
Reference in New Issue
Block a user