fix(bulk-edit): drop dead initial-* props and clear store on edit-mode discard
Felix B1 — `WhoWhenSection.svelte:37` and `DescriptionSection.svelte:42` mutated $bindable props at top-level script scope, seeding them from `initial*` companion props that no caller ever passes. The pattern stomps parent-owned state in any future component re-evaluation. Removed the dead initialDateIso / initialLocation / initialDocumentLocation props and let the bindables carry their own initial value. dateDisplay and currentTitle now seed from the bindable directly inside untrack — no re-assignment required. Elicit B2 — In edit mode the file map IS the user's bulk selection, so discarding must clear bulkSelectionStore and bounce back to /documents, otherwise the user is left on /documents/bulk-edit with an empty form and a stale count in the bottom bar. Refs #225, PR #331 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -134,6 +134,16 @@ async function handleDiscard() {
|
|||||||
});
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
}
|
}
|
||||||
|
if (mode === 'edit') {
|
||||||
|
// In edit mode the file map IS the user's bulk selection — discarding
|
||||||
|
// must clear the upstream store and bounce back to the list, otherwise
|
||||||
|
// the user is left on /documents/bulk-edit with an empty form and a
|
||||||
|
// stale count in the bottom bar (issue #225 Bulk-Edit Panel table).
|
||||||
|
bulkSelectionStore.clear();
|
||||||
|
discardAll();
|
||||||
|
await goto('/documents');
|
||||||
|
return;
|
||||||
|
}
|
||||||
discardAll();
|
discardAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -315,6 +315,30 @@ describe('BulkDocumentEditLayout', () => {
|
|||||||
|
|
||||||
// ─── mode="edit" ─────────────────────────────────────────────────────────────
|
// ─── mode="edit" ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('BulkDocumentEditLayout — mode="edit" discard', () => {
|
||||||
|
it('discard in edit mode clears the selection store and navigates back to /documents', async () => {
|
||||||
|
const { bulkSelectionStore } = await import('$lib/stores/bulkSelection.svelte');
|
||||||
|
bulkSelectionStore.setAll(['doc-1']);
|
||||||
|
|
||||||
|
const { container } = render(BulkDocumentEditLayout, {
|
||||||
|
mode: 'edit',
|
||||||
|
initialEditEntries: [
|
||||||
|
{ id: 'doc-1', title: 'Brief 1', pdfUrl: '/api/documents/doc-1/file' },
|
||||||
|
{ id: 'doc-2', title: 'Brief 2', pdfUrl: '/api/documents/doc-2/file' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const discardBtn = container.querySelector(
|
||||||
|
'button[data-testid="discard-all-btn"]'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
expect(discardBtn).not.toBeNull();
|
||||||
|
discardBtn.click();
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(goto).toHaveBeenCalledWith('/documents'), { timeout: 1000 });
|
||||||
|
expect(bulkSelectionStore.size).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('BulkDocumentEditLayout — mode="edit"', () => {
|
describe('BulkDocumentEditLayout — mode="edit"', () => {
|
||||||
const editEntry = (i: number) => ({
|
const editEntry = (i: number) => ({
|
||||||
id: `doc-${i}`,
|
id: `doc-${i}`,
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ let {
|
|||||||
archiveBox = $bindable(''),
|
archiveBox = $bindable(''),
|
||||||
archiveFolder = $bindable(''),
|
archiveFolder = $bindable(''),
|
||||||
initialTitle = '',
|
initialTitle = '',
|
||||||
initialDocumentLocation = '',
|
|
||||||
initialSummary = '',
|
initialSummary = '',
|
||||||
titleRequired = false,
|
titleRequired = false,
|
||||||
suggestedTitle = '',
|
suggestedTitle = '',
|
||||||
@@ -24,7 +23,6 @@ let {
|
|||||||
archiveBox?: string;
|
archiveBox?: string;
|
||||||
archiveFolder?: string;
|
archiveFolder?: string;
|
||||||
initialTitle?: string;
|
initialTitle?: string;
|
||||||
initialDocumentLocation?: string;
|
|
||||||
initialSummary?: string;
|
initialSummary?: string;
|
||||||
titleRequired?: boolean;
|
titleRequired?: boolean;
|
||||||
suggestedTitle?: string;
|
suggestedTitle?: string;
|
||||||
@@ -32,14 +30,11 @@ let {
|
|||||||
editMode?: boolean;
|
editMode?: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
|
// currentTitle seeds from initialTitle once at mount; subsequent edits flow
|
||||||
|
// through the oninput handler that flips titleDirty.
|
||||||
let titleDirty = $state(false);
|
let titleDirty = $state(false);
|
||||||
currentTitle = untrack(() => initialTitle);
|
currentTitle = untrack(() => initialTitle);
|
||||||
const titleValue = $derived(titleDirty ? currentTitle : suggestedTitle || currentTitle);
|
const titleValue = $derived(titleDirty ? currentTitle : suggestedTitle || currentTitle);
|
||||||
|
|
||||||
// Initialize controlled location field once from the legacy initial-* props so
|
|
||||||
// callers that haven't switched to the bindable form keep their existing
|
|
||||||
// pre-fill behaviour.
|
|
||||||
documentLocation = untrack(() => documentLocation || initialDocumentLocation);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="rounded-sm border border-line bg-surface p-6 shadow-sm">
|
<div class="rounded-sm border border-line bg-surface p-6 shadow-sm">
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ let {
|
|||||||
senderId = $bindable(''),
|
senderId = $bindable(''),
|
||||||
selectedReceivers = $bindable<Person[]>([]),
|
selectedReceivers = $bindable<Person[]>([]),
|
||||||
dateIso = $bindable(''),
|
dateIso = $bindable(''),
|
||||||
initialDateIso = '',
|
|
||||||
initialLocation = '',
|
|
||||||
initialSenderName = '',
|
initialSenderName = '',
|
||||||
suggestedDateIso = '',
|
suggestedDateIso = '',
|
||||||
suggestedSenderName = '',
|
suggestedSenderName = '',
|
||||||
@@ -24,8 +22,6 @@ let {
|
|||||||
senderId?: string;
|
senderId?: string;
|
||||||
selectedReceivers?: Person[];
|
selectedReceivers?: Person[];
|
||||||
dateIso?: string;
|
dateIso?: string;
|
||||||
initialDateIso?: string;
|
|
||||||
initialLocation?: string;
|
|
||||||
initialSenderName?: string;
|
initialSenderName?: string;
|
||||||
suggestedDateIso?: string;
|
suggestedDateIso?: string;
|
||||||
suggestedSenderName?: string;
|
suggestedSenderName?: string;
|
||||||
@@ -33,8 +29,9 @@ let {
|
|||||||
editMode?: boolean;
|
editMode?: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let dateDisplay = $state(untrack(() => isoToGerman(initialDateIso)));
|
// Seed dateDisplay from the bindable's current value once at mount; subsequent
|
||||||
dateIso = untrack(() => initialDateIso);
|
// edits flow through handleDateInput which writes back to dateIso.
|
||||||
|
let dateDisplay = $state(untrack(() => isoToGerman(dateIso)));
|
||||||
let dateDirty = $state(false);
|
let dateDirty = $state(false);
|
||||||
|
|
||||||
const dateInvalid = $derived(dateDirty && dateDisplay.length > 0 && dateIso === '');
|
const dateInvalid = $derived(dateDirty && dateDisplay.length > 0 && dateIso === '');
|
||||||
@@ -122,7 +119,6 @@ $effect(() => {
|
|||||||
id="location"
|
id="location"
|
||||||
type="text"
|
type="text"
|
||||||
name="location"
|
name="location"
|
||||||
value={initialLocation}
|
|
||||||
placeholder={m.form_placeholder_location()}
|
placeholder={m.form_placeholder_location()}
|
||||||
class="block w-full rounded border border-line px-2 py-3 text-sm shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
class="block w-full rounded border border-line px-2 py-3 text-sm shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user