refactor(document): extract shared DocumentOption type and createDocumentTypeahead factory

DocumentPickerDropdown and DocumentMultiSelect had identical createTypeahead
configs, fetch logic, and formatDocLabel helpers. Extracted to
documentTypeahead.ts; all four consumers import from the shared module.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-09 18:57:44 +02:00
parent 8adc39e2ce
commit 7df640201a
5 changed files with 57 additions and 93 deletions

View File

@@ -1,22 +1,11 @@
<script lang="ts">
import type { components } from '$lib/generated/api';
import { m } from '$lib/paraglide/messages.js';
import { clickOutside } from '$lib/shared/actions/clickOutside';
import { createTypeahead } from '$lib/shared/hooks/useTypeahead.svelte';
import { formatDocumentDate, type DatePrecision } from '$lib/shared/utils/documentDate';
import { getLocale } from '$lib/paraglide/runtime.js';
type DocumentListItem = components['schemas']['DocumentListItem'];
/**
* Exactly the fields this picker reads — id for selection/dedup, the rest for
* the honest date label. A full `Document` and a `DocumentListItem` are both
* structurally assignable, so the search results need no cast.
*/
type DocumentOption = Pick<
DocumentListItem,
'id' | 'title' | 'documentDate' | 'metaDatePrecision' | 'metaDateEnd'
>;
import {
createDocumentTypeahead,
formatDocumentOption,
type DocumentOption
} from './documentTypeahead';
interface Props {
selectedDocuments?: DocumentOption[];
@@ -34,20 +23,7 @@ let searchTerm = $state('');
let inputEl: HTMLInputElement;
let dropdownStyle = $state('');
const picker = createTypeahead<DocumentOption>({
fetchUrl: (q) =>
fetch(`/api/documents/search?q=${encodeURIComponent(q)}&size=10`)
.then((r) => r.json())
.then((b: { items: DocumentListItem[] }) =>
b.items.map((it) => ({
id: it.id,
title: it.title,
documentDate: it.documentDate,
metaDatePrecision: it.metaDatePrecision,
metaDateEnd: it.metaDateEnd
}))
)
});
const picker = createDocumentTypeahead();
// Filter out already-selected documents from typeahead results.
const filteredResults = $derived(
@@ -77,18 +53,6 @@ function selectDocument(doc: DocumentOption) {
function removeDocument(id: string | undefined) {
selectedDocuments = selectedDocuments.filter((d) => d.id !== id);
}
function formatDocLabel(doc: DocumentOption): string {
if (!doc.documentDate) return doc.title;
const label = formatDocumentDate(
doc.documentDate,
doc.metaDatePrecision as DatePrecision,
doc.metaDateEnd,
null,
getLocale()
);
return `${doc.title} · ${label}`;
}
</script>
<svelte:window onscroll={updateDropdownPosition} onresize={updateDropdownPosition} />
@@ -105,7 +69,7 @@ function formatDocLabel(doc: DocumentOption): string {
<span
class="inline-flex items-center gap-1 rounded bg-muted px-2 py-1 text-sm font-medium text-ink"
>
{formatDocLabel(doc)}
{formatDocumentOption(doc)}
<button
type="button"
onclick={() => removeDocument(doc.id)}
@@ -152,7 +116,7 @@ function formatDocLabel(doc: DocumentOption): string {
role="button"
tabindex="0"
>
{formatDocLabel(doc)}
{formatDocumentOption(doc)}
</div>
{/each}
{/if}

View File

@@ -1,16 +1,11 @@
<script lang="ts">
import type { components } from '$lib/generated/api';
import { m } from '$lib/paraglide/messages.js';
import { clickOutside } from '$lib/shared/actions/clickOutside';
import { createTypeahead } from '$lib/shared/hooks/useTypeahead.svelte';
import { formatDocumentDate, type DatePrecision } from '$lib/shared/utils/documentDate';
import { getLocale } from '$lib/paraglide/runtime.js';
type DocumentListItem = components['schemas']['DocumentListItem'];
type DocumentOption = Pick<
DocumentListItem,
'id' | 'title' | 'documentDate' | 'metaDatePrecision' | 'metaDateEnd'
>;
import {
createDocumentTypeahead,
formatDocumentOption,
type DocumentOption
} from './documentTypeahead';
interface Props {
alreadyAddedIds?: Set<string>;
@@ -26,20 +21,7 @@ let {
const listboxId = 'doc-picker-listbox';
const picker = createTypeahead<DocumentOption>({
fetchUrl: (q) =>
fetch(`/api/documents/search?q=${encodeURIComponent(q)}&size=10`)
.then((r) => r.json())
.then((b: { items: DocumentListItem[] }) =>
b.items.map((it) => ({
id: it.id,
title: it.title,
documentDate: it.documentDate,
metaDatePrecision: it.metaDatePrecision,
metaDateEnd: it.metaDateEnd
}))
)
});
const picker = createDocumentTypeahead();
let inputValue = $state('');
@@ -59,18 +41,6 @@ function handleSelect(doc: DocumentOption) {
picker.close();
onSelect(doc);
}
function formatDocLabel(doc: DocumentOption): string {
if (!doc.documentDate) return doc.title;
const label = formatDocumentDate(
doc.documentDate,
doc.metaDatePrecision as DatePrecision,
doc.metaDateEnd,
null,
getLocale()
);
return `${doc.title} · ${label}`;
}
</script>
<div use:clickOutside onclickoutside={() => picker.close()} class="relative">
@@ -113,7 +83,7 @@ function formatDocLabel(doc: DocumentOption): string {
: 'cursor-pointer hover:bg-muted focus:bg-muted focus:outline-none'
].join(' ')}
>
{formatDocLabel(doc)}
{formatDocumentOption(doc)}
{#if disabled}
<span class="sr-only">{m.journey_already_added()}</span>
{/if}

View File

@@ -0,0 +1,40 @@
import type { components } from '$lib/generated/api';
import { createTypeahead } from '$lib/shared/hooks/useTypeahead.svelte';
import { formatDocumentDate, type DatePrecision } from '$lib/shared/utils/documentDate';
import { getLocale } from '$lib/paraglide/runtime.js';
type DocumentListItem = components['schemas']['DocumentListItem'];
export type DocumentOption = Pick<
DocumentListItem,
'id' | 'title' | 'documentDate' | 'metaDatePrecision' | 'metaDateEnd'
>;
export function createDocumentTypeahead() {
return createTypeahead<DocumentOption>({
fetchUrl: (q) =>
fetch(`/api/documents/search?q=${encodeURIComponent(q)}&size=10`)
.then((r) => r.json())
.then((b: { items: DocumentListItem[] }) =>
b.items.map((it) => ({
id: it.id,
title: it.title,
documentDate: it.documentDate,
metaDatePrecision: it.metaDatePrecision,
metaDateEnd: it.metaDateEnd
}))
)
});
}
export function formatDocumentOption(doc: DocumentOption): string {
if (!doc.documentDate) return doc.title;
const label = formatDocumentDate(
doc.documentDate,
doc.metaDatePrecision as DatePrecision,
doc.metaDateEnd,
null,
getLocale()
);
return `${doc.title} · ${label}`;
}

View File

@@ -1,13 +1,7 @@
<script lang="ts">
import type { components } from '$lib/generated/api';
import { m } from '$lib/paraglide/messages.js';
import DocumentPickerDropdown from '$lib/document/DocumentPickerDropdown.svelte';
type DocumentListItem = components['schemas']['DocumentListItem'];
type DocumentOption = Pick<
DocumentListItem,
'id' | 'title' | 'documentDate' | 'metaDatePrecision' | 'metaDateEnd'
>;
import type { DocumentOption } from '$lib/document/documentTypeahead';
interface Props {
alreadyAddedIds?: Set<string>;

View File

@@ -4,6 +4,7 @@ import { m } from '$lib/paraglide/messages.js';
import { csrfFetch } from '$lib/shared/cookies';
import { createBlockDragDrop } from '$lib/document/transcription/useBlockDragDrop.svelte';
import { createUnsavedWarning } from '$lib/shared/hooks/useUnsavedWarning.svelte';
import type { DocumentOption } from '$lib/document/documentTypeahead';
import GeschichteSidebar from './GeschichteSidebar.svelte';
import JourneyItemRow from './JourneyItemRow.svelte';
import JourneyAddBar from './JourneyAddBar.svelte';
@@ -11,11 +12,6 @@ import JourneyAddBar from './JourneyAddBar.svelte';
type GeschichteView = components['schemas']['GeschichteView'];
type JourneyItemView = components['schemas']['JourneyItemView'];
type Person = components['schemas']['Person'];
type DocumentListItem = components['schemas']['DocumentListItem'];
type DocumentOption = Pick<
DocumentListItem,
'id' | 'title' | 'documentDate' | 'metaDatePrecision' | 'metaDateEnd'
>;
interface Props {
geschichte: GeschichteView;