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:
@@ -1,22 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { components } from '$lib/generated/api';
|
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
import { clickOutside } from '$lib/shared/actions/clickOutside';
|
import { clickOutside } from '$lib/shared/actions/clickOutside';
|
||||||
import { createTypeahead } from '$lib/shared/hooks/useTypeahead.svelte';
|
import {
|
||||||
import { formatDocumentDate, type DatePrecision } from '$lib/shared/utils/documentDate';
|
createDocumentTypeahead,
|
||||||
import { getLocale } from '$lib/paraglide/runtime.js';
|
formatDocumentOption,
|
||||||
|
type DocumentOption
|
||||||
type DocumentListItem = components['schemas']['DocumentListItem'];
|
} from './documentTypeahead';
|
||||||
|
|
||||||
/**
|
|
||||||
* 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'
|
|
||||||
>;
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selectedDocuments?: DocumentOption[];
|
selectedDocuments?: DocumentOption[];
|
||||||
@@ -34,20 +23,7 @@ let searchTerm = $state('');
|
|||||||
let inputEl: HTMLInputElement;
|
let inputEl: HTMLInputElement;
|
||||||
let dropdownStyle = $state('');
|
let dropdownStyle = $state('');
|
||||||
|
|
||||||
const picker = createTypeahead<DocumentOption>({
|
const picker = createDocumentTypeahead();
|
||||||
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
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filter out already-selected documents from typeahead results.
|
// Filter out already-selected documents from typeahead results.
|
||||||
const filteredResults = $derived(
|
const filteredResults = $derived(
|
||||||
@@ -77,18 +53,6 @@ function selectDocument(doc: DocumentOption) {
|
|||||||
function removeDocument(id: string | undefined) {
|
function removeDocument(id: string | undefined) {
|
||||||
selectedDocuments = selectedDocuments.filter((d) => d.id !== id);
|
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>
|
</script>
|
||||||
|
|
||||||
<svelte:window onscroll={updateDropdownPosition} onresize={updateDropdownPosition} />
|
<svelte:window onscroll={updateDropdownPosition} onresize={updateDropdownPosition} />
|
||||||
@@ -105,7 +69,7 @@ function formatDocLabel(doc: DocumentOption): string {
|
|||||||
<span
|
<span
|
||||||
class="inline-flex items-center gap-1 rounded bg-muted px-2 py-1 text-sm font-medium text-ink"
|
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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={() => removeDocument(doc.id)}
|
onclick={() => removeDocument(doc.id)}
|
||||||
@@ -152,7 +116,7 @@ function formatDocLabel(doc: DocumentOption): string {
|
|||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
{formatDocLabel(doc)}
|
{formatDocumentOption(doc)}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { components } from '$lib/generated/api';
|
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
import { clickOutside } from '$lib/shared/actions/clickOutside';
|
import { clickOutside } from '$lib/shared/actions/clickOutside';
|
||||||
import { createTypeahead } from '$lib/shared/hooks/useTypeahead.svelte';
|
import {
|
||||||
import { formatDocumentDate, type DatePrecision } from '$lib/shared/utils/documentDate';
|
createDocumentTypeahead,
|
||||||
import { getLocale } from '$lib/paraglide/runtime.js';
|
formatDocumentOption,
|
||||||
|
type DocumentOption
|
||||||
type DocumentListItem = components['schemas']['DocumentListItem'];
|
} from './documentTypeahead';
|
||||||
type DocumentOption = Pick<
|
|
||||||
DocumentListItem,
|
|
||||||
'id' | 'title' | 'documentDate' | 'metaDatePrecision' | 'metaDateEnd'
|
|
||||||
>;
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
alreadyAddedIds?: Set<string>;
|
alreadyAddedIds?: Set<string>;
|
||||||
@@ -26,20 +21,7 @@ let {
|
|||||||
|
|
||||||
const listboxId = 'doc-picker-listbox';
|
const listboxId = 'doc-picker-listbox';
|
||||||
|
|
||||||
const picker = createTypeahead<DocumentOption>({
|
const picker = createDocumentTypeahead();
|
||||||
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
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let inputValue = $state('');
|
let inputValue = $state('');
|
||||||
|
|
||||||
@@ -59,18 +41,6 @@ function handleSelect(doc: DocumentOption) {
|
|||||||
picker.close();
|
picker.close();
|
||||||
onSelect(doc);
|
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>
|
</script>
|
||||||
|
|
||||||
<div use:clickOutside onclickoutside={() => picker.close()} class="relative">
|
<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'
|
: 'cursor-pointer hover:bg-muted focus:bg-muted focus:outline-none'
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
{formatDocLabel(doc)}
|
{formatDocumentOption(doc)}
|
||||||
{#if disabled}
|
{#if disabled}
|
||||||
<span class="sr-only">{m.journey_already_added()}</span>
|
<span class="sr-only">{m.journey_already_added()}</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
40
frontend/src/lib/document/documentTypeahead.ts
Normal file
40
frontend/src/lib/document/documentTypeahead.ts
Normal 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}`;
|
||||||
|
}
|
||||||
@@ -1,13 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { components } from '$lib/generated/api';
|
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
import DocumentPickerDropdown from '$lib/document/DocumentPickerDropdown.svelte';
|
import DocumentPickerDropdown from '$lib/document/DocumentPickerDropdown.svelte';
|
||||||
|
import type { DocumentOption } from '$lib/document/documentTypeahead';
|
||||||
type DocumentListItem = components['schemas']['DocumentListItem'];
|
|
||||||
type DocumentOption = Pick<
|
|
||||||
DocumentListItem,
|
|
||||||
'id' | 'title' | 'documentDate' | 'metaDatePrecision' | 'metaDateEnd'
|
|
||||||
>;
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
alreadyAddedIds?: Set<string>;
|
alreadyAddedIds?: Set<string>;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { m } from '$lib/paraglide/messages.js';
|
|||||||
import { csrfFetch } from '$lib/shared/cookies';
|
import { csrfFetch } from '$lib/shared/cookies';
|
||||||
import { createBlockDragDrop } from '$lib/document/transcription/useBlockDragDrop.svelte';
|
import { createBlockDragDrop } from '$lib/document/transcription/useBlockDragDrop.svelte';
|
||||||
import { createUnsavedWarning } from '$lib/shared/hooks/useUnsavedWarning.svelte';
|
import { createUnsavedWarning } from '$lib/shared/hooks/useUnsavedWarning.svelte';
|
||||||
|
import type { DocumentOption } from '$lib/document/documentTypeahead';
|
||||||
import GeschichteSidebar from './GeschichteSidebar.svelte';
|
import GeschichteSidebar from './GeschichteSidebar.svelte';
|
||||||
import JourneyItemRow from './JourneyItemRow.svelte';
|
import JourneyItemRow from './JourneyItemRow.svelte';
|
||||||
import JourneyAddBar from './JourneyAddBar.svelte';
|
import JourneyAddBar from './JourneyAddBar.svelte';
|
||||||
@@ -11,11 +12,6 @@ import JourneyAddBar from './JourneyAddBar.svelte';
|
|||||||
type GeschichteView = components['schemas']['GeschichteView'];
|
type GeschichteView = components['schemas']['GeschichteView'];
|
||||||
type JourneyItemView = components['schemas']['JourneyItemView'];
|
type JourneyItemView = components['schemas']['JourneyItemView'];
|
||||||
type Person = components['schemas']['Person'];
|
type Person = components['schemas']['Person'];
|
||||||
type DocumentListItem = components['schemas']['DocumentListItem'];
|
|
||||||
type DocumentOption = Pick<
|
|
||||||
DocumentListItem,
|
|
||||||
'id' | 'title' | 'documentDate' | 'metaDatePrecision' | 'metaDateEnd'
|
|
||||||
>;
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
geschichte: GeschichteView;
|
geschichte: GeschichteView;
|
||||||
|
|||||||
Reference in New Issue
Block a user