Files
bea-edit/plan/done/subdocument-download-plan.md
2026-10-06 22:52:35 +02:00

189 lines
8.7 KiB
Markdown

# Plan: Download an individual sub-document
**Status: Done**
## Goal
Each sub-document displayed inside a processed ZIP archive shall have its own download action. The
action creates and downloads one PDF containing only that sub-document's attachments, in their
current order.
This is an additional interaction. The existing archive-level export modes remain available:
- **Eine PDF** continues to flatten all sub-documents and loose attachments into one PDF.
- **Getrennt** continues to download every non-empty sub-document plus the loose-attachment PDF.
- The new sub-document action downloads only the selected sub-document and does not include loose
attachments or sibling sub-documents.
## Current implementation
- `ProcessedZipArchiveEditor.svelte` owns archive-level download state and the browser download helper.
- `SubDocumentEditor.svelte` renders each sub-document but currently exposes only edit, drag/drop, and
dissolve callbacks.
- `mergeAttachments()` in `src/lib/zip-processing.ts` already creates a PDF from an ordered attachment
list.
- `buildArchiveExport(archive, 'separate')` already defines the desired sub-document naming convention:
`{archiveName} - {subDocumentName}.pdf`. It currently creates exports for every sub-document, so it
should not be called for an individual download.
## UX and behavior
Add a **PDF herunterladen** button to each sub-document header, before **Auflösen**.
- Clicking it downloads one PDF for that sub-document.
- The PDF page order matches `subDocument.attachments` exactly.
- The filename remains consistent with separate archive export:
`{archiveName without .pdf} - {subDocumentName without .pdf}.pdf`.
- The action is disabled when the sub-document has no attachments.
- While an individual PDF is being generated, its button reads **PDF wird erstellt** and cannot be
triggered again.
- Only one PDF-generation operation may run in an archive editor at a time. While an individual
download is active, archive-level and sibling download actions are disabled; while archive-level
export is active, individual actions are disabled.
- A failure leaves the archive and sub-document unchanged and shows the existing inline error area
with a sub-document-specific German message.
- Existing drag/drop, rename, selection, dissolve, and archive export behavior must remain unchanged.
## Implementation
### 1. Add a focused export helper
Update `src/lib/zip-processing.ts` with an exported helper:
```ts
export const buildSubDocumentExport = async (
archiveName: string,
subDocument: SubDocument
): Promise<ExportUnit>
```
Behavior:
1. Reject an empty attachment list with the same explicit empty-export behavior used by
`mergeAttachments()`.
2. Merge `subDocument.attachments` by calling `mergeAttachments()`.
3. Return one `ExportUnit` using the existing archive/sub-document filename convention.
4. Keep `.pdf` normalization centralized through the existing `stripPdfExtension()` helper.
Refactor the `mode === 'separate'` branch of `buildArchiveExport()` to call
`buildSubDocumentExport()` for each non-empty sub-document. This ensures bulk-separate and
individual exports cannot drift in PDF contents or naming. Preserve sequential generation and the
existing loose-attachment export behavior.
Do not construct a temporary `ProcessedZipArchive` and do not invoke `buildArchiveExport(...,
'separate')` for one item: both approaches encode the wrong scope and risk exporting unrelated
attachments later.
### 2. Add individual download orchestration to the archive editor
Update `src/lib/components/ProcessedZipArchiveEditor.svelte`:
- Import `buildSubDocumentExport`.
- Add `downloadingSubDocumentId: string | null` state alongside `isMerging` and `mergeError`.
- Derive whether any export is active from `isMerging || downloadingSubDocumentId !== null`.
- Add `downloadSubDocument(subDocument: SubDocument)` that:
1. Returns when another export is active or the sub-document is empty.
2. Stores the selected sub-document ID and clears `mergeError`.
3. Calls `buildSubDocumentExport(archiveName, subDocument)`.
4. Passes the returned bytes and name to the existing `downloadPdfBytes()` helper.
5. On failure, logs context including the archive and sub-document and sets
`Teildokument „{name}“ konnte nicht als PDF erstellt werden.`.
6. Clears the active ID in `finally`.
- Update `downloadArchive()` to guard against any active export, not only `isMerging`.
- Disable the archive-level download button and export-mode controls while an individual export is
active. The mode controls should remain visible and retain their selected value.
- Pass these props to every `SubDocumentEditor`:
- `onDownload={() => downloadSubDocument(subDocument)}`
- `isDownloading={downloadingSubDocumentId === subDocument.id}`
- `downloadDisabled={isMerging || downloadingSubDocumentId !== null}`
The browser download implementation stays in the parent so Blob/object-URL creation, filename
normalization, loading coordination, and errors remain centralized.
### 3. Add the sub-document header action
Update `src/lib/components/SubDocumentEditor.svelte`:
- Import the existing local `Button` component.
- Extend `Props` with:
- `onDownload?: () => void`
- `isDownloading?: boolean`
- `downloadDisabled?: boolean`
- Give all new props safe defaults.
- Render a bordered button before **Auflösen**:
- Normal label: **PDF herunterladen**
- Active label: **PDF wird erstellt**
- Disabled when `downloadDisabled`, `isDownloading`, or `attachments.length === 0`
- Keep the control as a native button through the shared `Button` component, with a clear accessible
label that includes the sub-document name if the visible label alone is ambiguous in assistive
technology.
- Prevent the button from participating in drag initiation in the same way as the existing name and
dissolve controls.
- Keep **Auflösen** visually distinct as the destructive action; do not change its behavior.
Use existing button variants and semantic theme utilities. Do not introduce a new UI dependency or a
second download utility in the child component.
## Files changed
- `src/lib/zip-processing.ts`
- Add `buildSubDocumentExport()`.
- Reuse it from separate archive export.
- `src/lib/components/ProcessedZipArchiveEditor.svelte`
- Coordinate individual export state, errors, and browser download.
- `src/lib/components/SubDocumentEditor.svelte`
- Add the individual download button and callback props.
- `agents.md`
- Clarify that per-archive separate export and direct per-sub-document export are both supported.
No route, server endpoint, persistence, package, lockfile, or Docker changes are required.
## Verification
### Automated checks
Run:
```sh
pnpm exec prettier --write \
src/lib/zip-processing.ts \
src/lib/components/ProcessedZipArchiveEditor.svelte \
src/lib/components/SubDocumentEditor.svelte \
agents.md
pnpm check
pnpm lint
pnpm build
```
There is no test framework in the repository. Do not add one solely for this feature. If a test
framework is introduced separately, cover `buildSubDocumentExport()` with non-empty, empty, ordering,
and filename cases.
### Manual scenarios
1. Create an archive with two sub-documents and at least one loose attachment.
2. Download the first sub-document and verify exactly one PDF is triggered.
3. Confirm the PDF contains only the first sub-document's attachments and preserves their displayed
order.
4. Confirm the filename is `{archive} - {sub-document}.pdf` without duplicate `.pdf` suffixes.
5. Download the second sub-document and verify it has independent contents and naming.
6. Confirm an empty sub-document's download action is disabled.
7. Start an individual export and verify duplicate, sibling, and archive-level export actions cannot
start until it finishes.
8. Force an invalid PDF/image merge and verify a German inline error appears, loading state clears,
and editor state is unchanged.
9. Verify **Eine PDF** still exports all sub-documents followed by loose attachments.
10. Verify **Getrennt** still exports every non-empty sub-document and the loose-attachment PDF.
11. Verify rename, reorder, move, selection, Escape, and **Auflösen** interactions still work.
12. Verify the new control remains usable on a narrow viewport and by keyboard.
## Acceptance criteria
- Every non-empty sub-document has a directly accessible download action.
- The action downloads one PDF containing only that sub-document's ordered attachments.
- Individual and archive-level exports use the same filename convention and merge implementation.
- Empty sub-documents cannot be exported.
- Loading and error states are visible, duplicate operations are prevented, and state always resets.
- Existing archive-level export behavior and all editor interactions are preserved.
- `pnpm check`, `pnpm lint`, and `pnpm build` pass without new issues.