fix(journey): person chips rendered blank — share one PersonView→PersonOption projection
JourneyEditor fed GeschichteView.PersonView (no displayName) straight into the displayName-rendering PersonMultiSelect, so every chip on a journey was empty. The name mapping both editors need now lives once in $lib/person/personOption.ts (with the [Unbekannt] fallback matching GeschichteService.toView), and PersonMultiSelect/GeschichteSidebar import the narrow PersonOption contract from there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,10 +6,10 @@ import StarterKit from '@tiptap/starter-kit';
|
|||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
import type { components } from '$lib/generated/api';
|
import type { components } from '$lib/generated/api';
|
||||||
import GeschichteSidebar from '$lib/geschichte/GeschichteSidebar.svelte';
|
import GeschichteSidebar from '$lib/geschichte/GeschichteSidebar.svelte';
|
||||||
|
import { toPersonOption, type PersonOption } from '$lib/person/personOption';
|
||||||
|
|
||||||
type GeschichteView = components['schemas']['GeschichteView'];
|
type GeschichteView = components['schemas']['GeschichteView'];
|
||||||
type Person = components['schemas']['Person'];
|
type Person = components['schemas']['Person'];
|
||||||
type PersonOption = Pick<Person, 'id' | 'displayName'>;
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
geschichte?: GeschichteView | null;
|
geschichte?: GeschichteView | null;
|
||||||
@@ -33,12 +33,7 @@ let title = $state(geschichte?.title ?? '');
|
|||||||
let body = $state(geschichte?.body ?? '');
|
let body = $state(geschichte?.body ?? '');
|
||||||
let status: 'DRAFT' | 'PUBLISHED' = $state(geschichte?.status ?? 'DRAFT');
|
let status: 'DRAFT' | 'PUBLISHED' = $state(geschichte?.status ?? 'DRAFT');
|
||||||
let selectedPersons: PersonOption[] = $state(
|
let selectedPersons: PersonOption[] = $state(
|
||||||
geschichte?.persons
|
geschichte?.persons ? Array.from(geschichte.persons).map(toPersonOption) : initialPersons
|
||||||
? Array.from(geschichte.persons).map((p) => ({
|
|
||||||
id: p.id,
|
|
||||||
displayName: [p.firstName, p.lastName].filter(Boolean).join(' ')
|
|
||||||
}))
|
|
||||||
: initialPersons
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let dirty = $state(false);
|
let dirty = $state(false);
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
import type { components } from '$lib/generated/api';
|
|
||||||
import PersonMultiSelect from '$lib/person/PersonMultiSelect.svelte';
|
import PersonMultiSelect from '$lib/person/PersonMultiSelect.svelte';
|
||||||
|
import type { PersonOption } from '$lib/person/personOption';
|
||||||
type Person = components['schemas']['Person'];
|
|
||||||
type PersonOption = Pick<Person, 'id' | 'displayName'>;
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
status: 'DRAFT' | 'PUBLISHED';
|
status: 'DRAFT' | 'PUBLISHED';
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { components } from '$lib/generated/api';
|
|||||||
import { m } from '$lib/paraglide/messages.js';
|
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 { toPersonOption, type PersonOption } from '$lib/person/personOption';
|
||||||
import { createUnsavedWarning } from '$lib/shared/hooks/useUnsavedWarning.svelte';
|
import { createUnsavedWarning } from '$lib/shared/hooks/useUnsavedWarning.svelte';
|
||||||
import type { DocumentOption } from '$lib/document/documentTypeahead';
|
import type { DocumentOption } from '$lib/document/documentTypeahead';
|
||||||
import GeschichteSidebar from './GeschichteSidebar.svelte';
|
import GeschichteSidebar from './GeschichteSidebar.svelte';
|
||||||
@@ -11,7 +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'];
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
geschichte: GeschichteView;
|
geschichte: GeschichteView;
|
||||||
@@ -31,7 +31,9 @@ const unsaved = createUnsavedWarning();
|
|||||||
let title = $state(geschichte.title ?? '');
|
let title = $state(geschichte.title ?? '');
|
||||||
let body = $state(geschichte.body ?? '');
|
let body = $state(geschichte.body ?? '');
|
||||||
let status: 'DRAFT' | 'PUBLISHED' = $state(geschichte.status ?? 'DRAFT');
|
let status: 'DRAFT' | 'PUBLISHED' = $state(geschichte.status ?? 'DRAFT');
|
||||||
let selectedPersons: Person[] = $state(geschichte.persons ? Array.from(geschichte.persons) : []);
|
let selectedPersons: PersonOption[] = $state(
|
||||||
|
geschichte.persons ? Array.from(geschichte.persons).map(toPersonOption) : []
|
||||||
|
);
|
||||||
let items: JourneyItemView[] = $state(
|
let items: JourneyItemView[] = $state(
|
||||||
[...(geschichte.items ?? [])].sort((a, b) => a.position - b.position)
|
[...(geschichte.items ?? [])].sort((a, b) => a.position - b.position)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -394,3 +394,18 @@ describe('JourneyEditor — duplicate document aria-disabled', () => {
|
|||||||
expect(option.getAttribute('aria-disabled')).toBe('true');
|
expect(option.getAttribute('aria-disabled')).toBe('true');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('JourneyEditor — person chips from GeschichteView', () => {
|
||||||
|
it('renders person names in the sidebar chips (PersonView carries no displayName)', async () => {
|
||||||
|
render(
|
||||||
|
JourneyEditor,
|
||||||
|
defaultProps({
|
||||||
|
geschichte: makeGeschichte({
|
||||||
|
persons: [{ id: 'p1', firstName: 'Anna', lastName: 'Schmidt' }]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect.element(page.getByText('Anna Schmidt')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,10 +2,8 @@
|
|||||||
import type { components } from '$lib/generated/api';
|
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 type { PersonOption } from './personOption';
|
||||||
type Person = components['schemas']['Person'];
|
type Person = components['schemas']['Person'];
|
||||||
// Narrow contract: only what the chips render and dedup needs. Full Person
|
|
||||||
// objects from /api/persons remain assignable; view projections fit too.
|
|
||||||
type PersonOption = Pick<Person, 'id' | 'displayName'>;
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selectedPersons?: PersonOption[];
|
selectedPersons?: PersonOption[];
|
||||||
|
|||||||
26
frontend/src/lib/person/personOption.ts
Normal file
26
frontend/src/lib/person/personOption.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { components } from '$lib/generated/api';
|
||||||
|
|
||||||
|
type Person = components['schemas']['Person'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Narrow chip/dedup contract for person pickers: exactly what PersonMultiSelect
|
||||||
|
* renders. Full `Person` objects (search results) are structurally assignable;
|
||||||
|
* view projections without a displayName go through {@link toPersonOption}.
|
||||||
|
*/
|
||||||
|
export type PersonOption = Pick<Person, 'id' | 'displayName'>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a name-carrying projection (e.g. GeschichteView.PersonView, which has no
|
||||||
|
* server-computed displayName) into the chip contract. Mirrors the server-side
|
||||||
|
* fallback in GeschichteService.toView.
|
||||||
|
*/
|
||||||
|
export function toPersonOption(p: {
|
||||||
|
id: string;
|
||||||
|
firstName?: string | null;
|
||||||
|
lastName?: string | null;
|
||||||
|
}): PersonOption {
|
||||||
|
return {
|
||||||
|
id: p.id,
|
||||||
|
displayName: [p.firstName, p.lastName].filter(Boolean).join(' ') || '[Unbekannt]'
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user