Compare commits
13 Commits
d1d0acf029
...
80d77a53e9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80d77a53e9 | ||
|
|
a45652466e | ||
|
|
49a17b581b | ||
|
|
53c8d6e9f0 | ||
|
|
279b4f1098 | ||
|
|
15114c2d92 | ||
|
|
35017d91c4 | ||
|
|
5b367a53a1 | ||
|
|
cb91ed340d | ||
|
|
2e0eb40aec | ||
|
|
d9e01ef1ff | ||
|
|
2e0f85c360 | ||
|
|
a1035171c2 |
@@ -6,6 +6,7 @@ import org.raddatz.familienarchiv.person.Person;
|
|||||||
import org.raddatz.familienarchiv.tag.Tag;
|
import org.raddatz.familienarchiv.tag.Tag;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -32,5 +33,9 @@ public record DocumentListItem(
|
|||||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
List<ActivityActorDTO> contributors,
|
List<ActivityActorDTO> contributors,
|
||||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
SearchMatchData matchData
|
SearchMatchData matchData,
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
LocalDateTime createdAt,
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
LocalDateTime updatedAt
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -767,7 +767,9 @@ public class DocumentService {
|
|||||||
doc.getSummary(),
|
doc.getSummary(),
|
||||||
completionPct,
|
completionPct,
|
||||||
contributors,
|
contributors,
|
||||||
match
|
match,
|
||||||
|
doc.getCreatedAt(),
|
||||||
|
doc.getUpdatedAt()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,8 @@ class DocumentControllerTest {
|
|||||||
.thenReturn(DocumentSearchResult.of(List.of(new DocumentListItem(
|
.thenReturn(DocumentSearchResult.of(List.of(new DocumentListItem(
|
||||||
docId, "Brief an Anna", "brief.pdf", null, null, null,
|
docId, "Brief an Anna", "brief.pdf", null, null, null,
|
||||||
List.of(), List.of(), null, null, null, null,
|
List.of(), List.of(), null, null, null, null,
|
||||||
0, List.of(), matchData))));
|
0, List.of(), matchData,
|
||||||
|
LocalDateTime.of(2026, 1, 15, 10, 0), LocalDateTime.of(2026, 1, 15, 10, 0)))));
|
||||||
|
|
||||||
mockMvc.perform(get("/api/documents/search").param("q", "Brief"))
|
mockMvc.perform(get("/api/documents/search").param("q", "Brief"))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
@@ -153,7 +154,8 @@ class DocumentControllerTest {
|
|||||||
.thenReturn(DocumentSearchResult.of(List.of(new DocumentListItem(
|
.thenReturn(DocumentSearchResult.of(List.of(new DocumentListItem(
|
||||||
docId, "Brief an Anna", "brief.pdf", null, null, null,
|
docId, "Brief an Anna", "brief.pdf", null, null, null,
|
||||||
List.of(), List.of(), null, null, null, null,
|
List.of(), List.of(), null, null, null, null,
|
||||||
0, List.of(), matchData))));
|
0, List.of(), matchData,
|
||||||
|
LocalDateTime.of(2026, 1, 15, 10, 0), LocalDateTime.of(2026, 1, 15, 10, 0)))));
|
||||||
|
|
||||||
mockMvc.perform(get("/api/documents/search"))
|
mockMvc.perform(get("/api/documents/search"))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.raddatz.familienarchiv.audit.ActivityActorDTO;
|
import org.raddatz.familienarchiv.audit.ActivityActorDTO;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -16,7 +17,8 @@ class DocumentSearchResultTest {
|
|||||||
return new DocumentListItem(
|
return new DocumentListItem(
|
||||||
docId, "Test", "test.pdf", null, null, null,
|
docId, "Test", "test.pdf", null, null, null,
|
||||||
List.of(), List.of(), null, null, null, null,
|
List.of(), List.of(), null, null, null, null,
|
||||||
0, List.of(), SearchMatchData.empty());
|
0, List.of(), SearchMatchData.empty(),
|
||||||
|
LocalDateTime.of(2026, 1, 15, 10, 0), LocalDateTime.of(2026, 1, 15, 10, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -66,7 +68,8 @@ class DocumentSearchResultTest {
|
|||||||
DocumentListItem item = new DocumentListItem(
|
DocumentListItem item = new DocumentListItem(
|
||||||
id, "T", "t.pdf", null, null, null,
|
id, "T", "t.pdf", null, null, null,
|
||||||
List.of(), List.of(), null, null, null, null,
|
List.of(), List.of(), null, null, null, null,
|
||||||
75, List.of(actor), SearchMatchData.empty());
|
75, List.of(actor), SearchMatchData.empty(),
|
||||||
|
LocalDateTime.of(2026, 1, 15, 10, 0), LocalDateTime.of(2026, 1, 15, 10, 0));
|
||||||
|
|
||||||
DocumentSearchResult result = DocumentSearchResult.of(List.of(item));
|
DocumentSearchResult result = DocumentSearchResult.of(List.of(item));
|
||||||
|
|
||||||
|
|||||||
10
frontend/package-lock.json
generated
10
frontend/package-lock.json
generated
@@ -23,9 +23,9 @@
|
|||||||
"@eslint/compat": "^1.4.0",
|
"@eslint/compat": "^1.4.0",
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
"@inlang/paraglide-js": "^2.5.0",
|
"@inlang/paraglide-js": "^2.5.0",
|
||||||
"@playwright/test": "^1.58.2",
|
"@playwright/test": "^1.60.0",
|
||||||
"@sveltejs/adapter-node": "^5.4.0",
|
"@sveltejs/adapter-node": "^5.5.4",
|
||||||
"@sveltejs/kit": "^2.48.5",
|
"@sveltejs/kit": "^2.60.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
"openapi-typescript": "^7.8.0",
|
"openapi-typescript": "^7.8.0",
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"playwright": "^1.56.1",
|
"playwright": "^1.60.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-svelte": "^3.4.0",
|
"prettier-plugin-svelte": "^3.4.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.1",
|
"prettier-plugin-tailwindcss": "^0.7.1",
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.47.0",
|
"typescript-eslint": "^8.47.0",
|
||||||
"vite": "^7.2.2",
|
"vite": "^7.3.3",
|
||||||
"vite-plugin-devtools-json": "^1.0.0",
|
"vite-plugin-devtools-json": "^1.0.0",
|
||||||
"vitest": "^4.0.10",
|
"vitest": "^4.0.10",
|
||||||
"vitest-browser-svelte": "^2.0.1"
|
"vitest-browser-svelte": "^2.0.1"
|
||||||
|
|||||||
@@ -2205,10 +2205,10 @@ export interface components {
|
|||||||
totalStories: number;
|
totalStories: number;
|
||||||
};
|
};
|
||||||
PersonSummaryDTO: {
|
PersonSummaryDTO: {
|
||||||
|
title?: string;
|
||||||
/** Format: uuid */
|
/** Format: uuid */
|
||||||
id?: string;
|
id?: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
title?: string;
|
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
/** Format: int64 */
|
/** Format: int64 */
|
||||||
@@ -2315,8 +2315,6 @@ export interface components {
|
|||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
totalPages?: number;
|
totalPages?: number;
|
||||||
pageable?: components["schemas"]["PageableObject"];
|
pageable?: components["schemas"]["PageableObject"];
|
||||||
first?: boolean;
|
|
||||||
last?: boolean;
|
|
||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
size?: number;
|
size?: number;
|
||||||
content?: components["schemas"]["NotificationDTO"][];
|
content?: components["schemas"]["NotificationDTO"][];
|
||||||
@@ -2325,6 +2323,8 @@ export interface components {
|
|||||||
sort?: components["schemas"]["SortObject"];
|
sort?: components["schemas"]["SortObject"];
|
||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
numberOfElements?: number;
|
numberOfElements?: number;
|
||||||
|
first?: boolean;
|
||||||
|
last?: boolean;
|
||||||
empty?: boolean;
|
empty?: boolean;
|
||||||
};
|
};
|
||||||
PageableObject: {
|
PageableObject: {
|
||||||
@@ -2407,6 +2407,10 @@ export interface components {
|
|||||||
completionPercentage: number;
|
completionPercentage: number;
|
||||||
contributors: components["schemas"]["ActivityActorDTO"][];
|
contributors: components["schemas"]["ActivityActorDTO"][];
|
||||||
matchData: components["schemas"]["SearchMatchData"];
|
matchData: components["schemas"]["SearchMatchData"];
|
||||||
|
/** Format: date-time */
|
||||||
|
createdAt: string;
|
||||||
|
/** Format: date-time */
|
||||||
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
DocumentSearchResult: {
|
DocumentSearchResult: {
|
||||||
items: components["schemas"]["DocumentListItem"][];
|
items: components["schemas"]["DocumentListItem"][];
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ import * as m from '$lib/paraglide/messages.js';
|
|||||||
import { relativeTimeDe } from '$lib/shared/relativeTime';
|
import { relativeTimeDe } from '$lib/shared/relativeTime';
|
||||||
import type { components } from '$lib/generated/api';
|
import type { components } from '$lib/generated/api';
|
||||||
|
|
||||||
type Document = components['schemas']['Document'];
|
type DocumentListItem = components['schemas']['DocumentListItem'];
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
documents: Document[];
|
documents: DocumentListItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { documents }: Props = $props();
|
const { documents }: Props = $props();
|
||||||
|
|
||||||
function isNew(doc: Document): boolean {
|
function isNew(doc: DocumentListItem): boolean {
|
||||||
return new Date(doc.createdAt).getTime() === new Date(doc.updatedAt).getTime();
|
return new Date(doc.createdAt).getTime() > Date.now() - 7 * 24 * 60 * 60 * 1000;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -5,24 +5,33 @@ import { page } from 'vitest/browser';
|
|||||||
import ReaderRecentDocs from './ReaderRecentDocs.svelte';
|
import ReaderRecentDocs from './ReaderRecentDocs.svelte';
|
||||||
import type { components } from '$lib/generated/api';
|
import type { components } from '$lib/generated/api';
|
||||||
|
|
||||||
type Document = components['schemas']['Document'];
|
type DocumentListItem = components['schemas']['DocumentListItem'];
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
const baseDoc: Document = {
|
const baseDoc: DocumentListItem = {
|
||||||
id: 'doc1',
|
id: 'doc1',
|
||||||
title: 'Brief an Hans',
|
title: 'Brief an Hans',
|
||||||
originalFilename: 'brief.pdf',
|
originalFilename: 'brief.pdf',
|
||||||
status: 'UPLOADED',
|
completionPercentage: 0,
|
||||||
metadataComplete: true,
|
receivers: [],
|
||||||
scriptType: 'HANDWRITING_KURRENT',
|
tags: [],
|
||||||
|
contributors: [],
|
||||||
|
matchData: {
|
||||||
|
titleOffsets: [],
|
||||||
|
senderMatched: false,
|
||||||
|
matchedReceiverIds: [],
|
||||||
|
matchedTagIds: [],
|
||||||
|
snippetOffsets: [],
|
||||||
|
summaryOffsets: []
|
||||||
|
},
|
||||||
createdAt: '2025-01-01T12:00:00Z',
|
createdAt: '2025-01-01T12:00:00Z',
|
||||||
updatedAt: '2025-01-01T12:00:00Z'
|
updatedAt: '2025-01-01T12:00:00Z'
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedDoc: Document = {
|
const updatedDoc: DocumentListItem = {
|
||||||
...baseDoc,
|
...baseDoc,
|
||||||
id: 'doc2',
|
id: 'doc2',
|
||||||
title: 'Urkunde 1920',
|
title: 'Urkunde 1920',
|
||||||
@@ -88,8 +97,14 @@ describe('ReaderRecentDocs', () => {
|
|||||||
expect(thumb!.className).toMatch(/rounded-/);
|
expect(thumb!.className).toMatch(/rounded-/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows "Neu" accent-pill badge when createdAt equals updatedAt', async () => {
|
it('shows "Neu" accent-pill badge when document was created within the last 7 days', async () => {
|
||||||
render(ReaderRecentDocs, { documents: [baseDoc] });
|
const recentDoc: DocumentListItem = {
|
||||||
|
...baseDoc,
|
||||||
|
id: 'doc-recent',
|
||||||
|
createdAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
updatedAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString()
|
||||||
|
};
|
||||||
|
render(ReaderRecentDocs, { documents: [recentDoc] });
|
||||||
const badge = page.getByText(/^Neu$/i);
|
const badge = page.getByText(/^Neu$/i);
|
||||||
await expect.element(badge).toBeInTheDocument();
|
await expect.element(badge).toBeInTheDocument();
|
||||||
const cls = ((await badge.element()) as HTMLElement).className;
|
const cls = ((await badge.element()) as HTMLElement).className;
|
||||||
@@ -98,7 +113,7 @@ describe('ReaderRecentDocs', () => {
|
|||||||
expect(cls).toMatch(/\btext-ink\b/);
|
expect(cls).toMatch(/\btext-ink\b/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows no badge when updatedAt differs from createdAt', async () => {
|
it('shows no badge when document was created more than 7 days ago', async () => {
|
||||||
render(ReaderRecentDocs, { documents: [updatedDoc] });
|
render(ReaderRecentDocs, { documents: [updatedDoc] });
|
||||||
const badge = page.getByText(/^Neu$/i);
|
const badge = page.getByText(/^Neu$/i);
|
||||||
await expect.element(badge).not.toBeInTheDocument();
|
await expect.element(badge).not.toBeInTheDocument();
|
||||||
@@ -106,20 +121,20 @@ describe('ReaderRecentDocs', () => {
|
|||||||
await expect.element(updatedBadge).not.toBeInTheDocument();
|
await expect.element(updatedBadge).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows "Neu" badge when createdAt and updatedAt represent the same instant in different ISO formats', async () => {
|
it('shows "Neu" badge when document was created 6 days ago', async () => {
|
||||||
const sameInstantDoc: Document = {
|
const almostOldDoc: DocumentListItem = {
|
||||||
...baseDoc,
|
...baseDoc,
|
||||||
id: 'doc-same-instant',
|
id: 'doc-almost-old',
|
||||||
createdAt: '2025-01-01T12:00:00Z',
|
createdAt: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
updatedAt: '2025-01-01T12:00:00.000Z'
|
updatedAt: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString()
|
||||||
};
|
};
|
||||||
render(ReaderRecentDocs, { documents: [sameInstantDoc] });
|
render(ReaderRecentDocs, { documents: [almostOldDoc] });
|
||||||
const badge = page.getByText(/^Neu$/i);
|
const badge = page.getByText(/^Neu$/i);
|
||||||
await expect.element(badge).toBeInTheDocument();
|
await expect.element(badge).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders sender name text when sender is present', async () => {
|
it('renders sender name text when sender is present', async () => {
|
||||||
const docWithSender: Document = {
|
const docWithSender: DocumentListItem = {
|
||||||
...baseDoc,
|
...baseDoc,
|
||||||
sender: {
|
sender: {
|
||||||
id: 'p1',
|
id: 'p1',
|
||||||
|
|||||||
@@ -31,25 +31,25 @@ describe('ReaderRecentDocs', () => {
|
|||||||
.toHaveAttribute('href', '/documents');
|
.toHaveAttribute('href', '/documents');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the New badge when createdAt equals updatedAt', async () => {
|
it('renders the New badge when document was created within the last 7 days', async () => {
|
||||||
|
const recentDate = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString();
|
||||||
|
const laterUpdate = new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString();
|
||||||
render(ReaderRecentDocs, {
|
render(ReaderRecentDocs, {
|
||||||
props: {
|
props: {
|
||||||
documents: [
|
documents: [makeDoc({ createdAt: recentDate, updatedAt: laterUpdate })]
|
||||||
makeDoc({ createdAt: '2026-04-15T10:00:00Z', updatedAt: '2026-04-15T10:00:00Z' })
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect.element(page.getByText('Neu')).toBeVisible();
|
await expect.element(page.getByText('Neu')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hides the New badge when document was updated after creation', async () => {
|
it('hides the New badge when document was created more than 7 days ago', async () => {
|
||||||
render(ReaderRecentDocs, {
|
render(ReaderRecentDocs, {
|
||||||
props: {
|
props: {
|
||||||
documents: [
|
documents: [
|
||||||
makeDoc({
|
makeDoc({
|
||||||
createdAt: '2026-04-15T10:00:00Z',
|
createdAt: '2026-04-15T10:00:00Z',
|
||||||
updatedAt: '2026-04-15T11:00:00Z'
|
updatedAt: '2026-04-15T10:00:00Z'
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -409,19 +409,24 @@ describe('PersonMentionEditor — onExit cancels pending debounce', () => {
|
|||||||
await new Promise((r) => setTimeout(r, SEARCH_DEBOUNCE_MS + POST_DEBOUNCE_SLACK_MS));
|
await new Promise((r) => setTimeout(r, SEARCH_DEBOUNCE_MS + POST_DEBOUNCE_SLACK_MS));
|
||||||
const fetchesBeforeEscape = fetchMock.mock.calls.length;
|
const fetchesBeforeEscape = fetchMock.mock.calls.length;
|
||||||
|
|
||||||
// Trigger a new debounced search (queues runSearch after 150 ms), then
|
// Freeze setTimeout so the 150 ms debounce cannot fire before Escape
|
||||||
// immediately Escape *while focus is back in the editor* so Tiptap's
|
// triggers onExit. We install fake timers only now — after the setup
|
||||||
// suggestion-plugin Escape handler fires onExit before the debounce.
|
// above — so that vi.waitFor()'s real-timer polling still worked.
|
||||||
// Without onExit cancelling the pending debounce, runSearch executes
|
vi.useFakeTimers();
|
||||||
// against the now-unmounted dropdown's state.
|
try {
|
||||||
|
// fill() dispatches the input event synchronously via CDP; by the
|
||||||
|
// time the await resolves, onSearch('Walter') has run and the fake
|
||||||
|
// debounce timer is set.
|
||||||
await page.getByRole('searchbox').fill('Walter');
|
await page.getByRole('searchbox').fill('Walter');
|
||||||
// Focus the editor so the Escape lands on Tiptap's suggestion handler.
|
// Focus the editor so the Escape lands on Tiptap's suggestion handler.
|
||||||
(page.getByRole('textbox').element() as HTMLElement).focus();
|
(page.getByRole('textbox').element() as HTMLElement).focus();
|
||||||
await userEvent.keyboard('{Escape}');
|
await userEvent.keyboard('{Escape}');
|
||||||
|
// onExit has now called debouncedSearch.cancel(). Advance past the
|
||||||
// Wait past the debounce window. If onExit did not cancel the pending
|
// debounce window — the cancelled timer must not fire.
|
||||||
// debounce, a fetch with q=Walter would still fire here.
|
await vi.advanceTimersByTimeAsync(SEARCH_DEBOUNCE_MS + POST_DEBOUNCE_SLACK_MS);
|
||||||
await new Promise((r) => setTimeout(r, SEARCH_DEBOUNCE_MS + POST_DEBOUNCE_SLACK_MS));
|
} finally {
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
|
|
||||||
const newFetches = fetchMock.mock.calls.slice(fetchesBeforeEscape);
|
const newFetches = fetchMock.mock.calls.slice(fetchesBeforeEscape);
|
||||||
const walterFetches = newFetches.filter(
|
const walterFetches = newFetches.filter(
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ type DashboardPulseDTO = components['schemas']['DashboardPulseDTO'];
|
|||||||
type ActivityFeedItemDTO = components['schemas']['ActivityFeedItemDTO'];
|
type ActivityFeedItemDTO = components['schemas']['ActivityFeedItemDTO'];
|
||||||
type IncompleteDocumentDTO = components['schemas']['IncompleteDocumentDTO'];
|
type IncompleteDocumentDTO = components['schemas']['IncompleteDocumentDTO'];
|
||||||
type PersonSummaryDTO = components['schemas']['PersonSummaryDTO'];
|
type PersonSummaryDTO = components['schemas']['PersonSummaryDTO'];
|
||||||
type Document = components['schemas']['Document'];
|
type DocumentListItem = components['schemas']['DocumentListItem'];
|
||||||
type Geschichte = components['schemas']['Geschichte'];
|
type Geschichte = components['schemas']['Geschichte'];
|
||||||
type TagTreeNodeDTO = components['schemas']['TagTreeNodeDTO'];
|
type TagTreeNodeDTO = components['schemas']['TagTreeNodeDTO'];
|
||||||
|
|
||||||
@@ -55,8 +55,8 @@ export async function load({ fetch, parent }) {
|
|||||||
|
|
||||||
const readerStats = settled<StatsDTO>(statsRes);
|
const readerStats = settled<StatsDTO>(statsRes);
|
||||||
const topPersons = settled<PersonSummaryDTO[]>(topPersonsRes) ?? [];
|
const topPersons = settled<PersonSummaryDTO[]>(topPersonsRes) ?? [];
|
||||||
const searchData = settled<{ items: { document: Document }[] }>(recentDocsRes);
|
const searchData = settled<{ items: DocumentListItem[] }>(recentDocsRes);
|
||||||
const recentDocs = searchData?.items.map((i) => i.document) ?? [];
|
const recentDocs = searchData?.items ?? [];
|
||||||
const recentStories = settled<Geschichte[]>(recentStoriesRes) ?? [];
|
const recentStories = settled<Geschichte[]>(recentStoriesRes) ?? [];
|
||||||
const tagTree = settled<TagTreeNodeDTO[]>(tagTreeRes) ?? [];
|
const tagTree = settled<TagTreeNodeDTO[]>(tagTreeRes) ?? [];
|
||||||
const drafts = settled<Geschichte[]>(draftsRes) ?? [];
|
const drafts = settled<Geschichte[]>(draftsRes) ?? [];
|
||||||
@@ -178,7 +178,7 @@ export async function load({ fetch, parent }) {
|
|||||||
incompleteTotal: 0,
|
incompleteTotal: 0,
|
||||||
readerStats: null,
|
readerStats: null,
|
||||||
topPersons: [] as PersonSummaryDTO[],
|
topPersons: [] as PersonSummaryDTO[],
|
||||||
recentDocs: [] as Document[],
|
recentDocs: [] as DocumentListItem[],
|
||||||
recentStories: [] as Geschichte[],
|
recentStories: [] as Geschichte[],
|
||||||
tagTree: [] as TagTreeNodeDTO[],
|
tagTree: [] as TagTreeNodeDTO[],
|
||||||
drafts: [] as Geschichte[],
|
drafts: [] as Geschichte[],
|
||||||
|
|||||||
@@ -396,6 +396,55 @@ describe('home page load — reader branch (isReader = !canWrite && !canAnnotate
|
|||||||
expect(result.isReader).toBe(false);
|
expect(result.isReader).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('maps search result items directly to recentDocs without wrapping in a .document property', async () => {
|
||||||
|
const searchItem = {
|
||||||
|
id: 'd1',
|
||||||
|
title: 'Liebesbrief',
|
||||||
|
originalFilename: 'letter.pdf',
|
||||||
|
completionPercentage: 80,
|
||||||
|
receivers: [],
|
||||||
|
tags: [],
|
||||||
|
contributors: [],
|
||||||
|
matchData: { titleOffsets: [], senderMatched: false },
|
||||||
|
createdAt: '2026-05-01T10:00:00Z',
|
||||||
|
updatedAt: '2026-05-10T08:00:00Z'
|
||||||
|
};
|
||||||
|
const mockGet = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce({ response: { ok: true, status: 200 }, data: [] }) // initial persons
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
response: { ok: true },
|
||||||
|
data: { totalDocuments: 1, totalPersons: 1 }
|
||||||
|
}) // stats
|
||||||
|
.mockResolvedValueOnce({ response: { ok: true }, data: [] }) // topPersons
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
response: { ok: true },
|
||||||
|
data: { items: [searchItem], totalElements: 1, pageNumber: 0, pageSize: 5, totalPages: 1 }
|
||||||
|
}) // search
|
||||||
|
.mockResolvedValueOnce({ response: { ok: true }, data: [] }); // stories
|
||||||
|
vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
|
||||||
|
typeof createApiClient
|
||||||
|
>);
|
||||||
|
|
||||||
|
const result = await load({
|
||||||
|
url: makeUrl(),
|
||||||
|
request: new Request('http://localhost/'),
|
||||||
|
fetch: vi.fn() as unknown as typeof fetch,
|
||||||
|
parent: vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ canWrite: false, canAnnotate: false, canBlogWrite: false })
|
||||||
|
} as Parameters<typeof load>[0]);
|
||||||
|
|
||||||
|
expect(result.isReader).toBe(true);
|
||||||
|
if (result.isReader) {
|
||||||
|
expect(result.recentDocs).toHaveLength(1);
|
||||||
|
expect(result.recentDocs[0]).toBeDefined();
|
||||||
|
expect(result.recentDocs[0].id).toBe('d1');
|
||||||
|
expect(result.recentDocs[0].createdAt).toBe('2026-05-01T10:00:00Z');
|
||||||
|
expect(result.recentDocs[0].updatedAt).toBe('2026-05-10T08:00:00Z');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('returns topPersons=[] when topPersons fetch fails, rest of data still loads', async () => {
|
it('returns topPersons=[] when topPersons fetch fails, rest of data still loads', async () => {
|
||||||
const okStats = {
|
const okStats = {
|
||||||
response: { ok: true, status: 200 },
|
response: { ok: true, status: 200 },
|
||||||
|
|||||||
Reference in New Issue
Block a user