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

8.7 KiB

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:

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:

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.