think
16px
820px

New Document experience redesign — Design

Date: 2026-07-01
Status: Approved (design); pending implementation plan.

Goal

Replace the single mode-toggle "New document" modal with two clear entry points and a much better filing UX:
- Upload Document — a drag-drop-first modal that expands into a metadata form once a file is chosen, autofills the title from the filename, and files the document via a rich folder-suggestions box (multiple AI suggestions + the current folder), instead of the unreadable one-line chip.
- Write Document — jumps straight into the existing rich-text editor on a fresh unfiled draft; title / classification / Document ID / folder are collected in a new Save modal at save time, which lets the author Save (finalize to a PDF that stays editable) or Save as draft (HTML only).

The write-side flow is a reframe of existing systems (the TipTap editor, the finalize→PDF pipeline, the versions-by-MIME model, the "editable = has an HTML source version" derivation), not a rebuild.

Decisions (locked during brainstorming)

  1. Two separate entry points, not an in-modal mode toggle: Upload Document and Write Document buttons on the documents toolbar. (This removes the mode radio entirely.)
  2. Write Document → instant editor on an unfiled draft; metadata is collected at first save.
  3. Deferred Document ID: a write-doc is created without a Document ID; the ID is assigned at first Save from the format chosen in the Save modal (assign-if-absent — immutability is preserved after assignment; the assignment moment just moves from create to first save).
  4. Folder-suggestions box (shared by the upload modal and the write Save modal): a titled container listing several AI folder suggestions (the suggest endpoint already returns top-3) each with full path + match %, always shown and auto-run (no button), and it always includes the current folder as an option, plus a manual Browse fallback. Default selection = the current folder (predictable filing), AI folders one click away.
  5. Table "Editable" badge for authored (HTML-source) documents, in addition to the existing detail-view affordance.
  6. Sequencing: Phase A (Upload modal + folder box, frontend-only) ships first; Phase B (Write flow reframe) second. The folder box built in A is reused in B.
  7. Reframe, not rebuild the write/save/PDF/versions flow.

Reused (existing) systems — do NOT rebuild

  • TipTap editor web/src/features/documents/TextEditorView.tsx (route /documents/d/:docId/edit): autosave to localStorage, pessimistic checkout lock, seeds content from the latest text/html version.
  • Finalize→PDF POST /api/v1/documents/{docID}/finalize (handlers_finalize.go): renders the HTML source → PDF via gotenberg, adds it as a new version, resets status to draft. Already accepts mode (replace|new), title, formatId, folderId, letterheadId. Re-finalize finds the latest text/html version as the render source even when the current version is a PDF.
  • Versions by MIME: document_versions rows carry MIME (text/html vs application/pdf) + a per-version status (draft/signed/meteraied). ListVersions returns all.
  • Editable derivation: isTextDoc = versions.some(v => v.mime startsWith "text/html") (DocumentDetailView.tsx:119) — a doc stays editable as long as an HTML source version exists.
  • Publish/workflow gate: publishing and starting a workflow are already blocked while the current version is text/html (service.go SetDocumentStatus, handlers_workflow.go) — a doc must be finalized (current version = PDF) to publish.
  • Semantic suggest endpoint: POST /api/v1/semantic/suggest-folder (premium semantic module) → top-K {folder_id, path, score} over the caller's write-accessible folders. Already returns 3.
  • Folder/create/move: useCreateDocument, useChildFolders/MoveModal (folder picker), moveDocument, uploadVersion, checkout/checkin.

Component 1 — The folder-suggestions box (shared)

A self-contained, reusable React component (e.g. web/src/features/documents/FolderSuggestBox.tsx) used by both the upload modal and the write Save modal.

Inputs: the suggestion query (for upload: {title, filename}; for write: {title, text} from the editor content), the current folder id/path, and a controlled selected-folder value.

Behavior:
- Auto-runs the suggestion on mount / when its inputs settle (debounced); no "Suggest" button.
- Renders a titled section ("Where should this go?") with selectable rows: the AI suggestions (top-3, full readable path + a match badge derived from score), then a row for the current folder (labeled, deduped if the AI already surfaced it), then Browse for another folder… (opens the existing folder-picker tree).
- Default selection = the current folder. Selecting any row (or a browsed folder) sets the controlled value.
- Graceful degrade / gating: the AI rows come from the semantic-gated endpoint. When semantic is not enabled (moduleEnabled(enabledModules,'semantic') false), the box hides the AI rows and shows only Current folder + Browse — so the modal is fully usable without the premium module. Loading and error states show inline (error → AI rows simply absent, current-folder/browse remain). This is the fail-closed posture from the semantic module.

Readability fix (the immediate issue): this box replaces the low-contrast blue Tag entirely. Paths are shown in full (wrap/tooltip when long) at AA contrast; the match badge is a small, legible chip.

Component 2 — Upload Document modal (Phase A)

NewDocumentModal.tsx becomes the upload-only modal (the mode toggle and the text branch are removed from it; write is a separate entry point).

Progressive layout:
1. No file yet: only a drag-and-drop zone (Carbon FileUploaderDropContainer) + "or browse", plus Cancel. The Create button is hidden/disabled until a file exists.
2. File chosen/dropped: the form expands to reveal — the selected filename (removable), Title (autofilled from the filename with the extension stripped; editable), Classification, Document type (optional), Document ID format (+ custom vars), and the folder-suggestions box (query = {title, filename}). Create is enabled.

Filing: the document is created via useCreateDocument and the chosen version uploaded (uploadVersion), then filed into the selected folder from the box (default = current folder). This makes upload filing explicit (today it silently uses the current folder).

Entry point: DocumentsPage toolbar shows Upload Document (opens this modal) alongside Write Document. (A split-button is an acceptable alternative if toolbar space is tight; two buttons preferred.)

Component 3 — Write Document flow (Phase B)

Entry: the Write Document toolbar button creates an unfiled draft document — placeholder title (e.g. "Untitled document"), classification none, no Document ID, folder_id = null — and navigates straight to /documents/d/:id/edit. (Uses the existing "no ID" create path; see the deferred-ID backend note.)

Editor: unchanged TipTap editor, except the two existing buttons (Save = HTML, Finalize = PDF) are replaced by a single primary "Save" button that opens the Save modal. (The existing autosave-to-localStorage and checkout behavior stay.)

Save modal (web/src/features/documents/SaveDocumentModal.tsx), the write-doc's metadata + filing gate:
- Fields: Title (required; pre-filled from the placeholder/first heading if available), Classification, Document ID format (assigned on this first save), and the folder-suggestions box (query = {title, text} from the current editor content — suggestions are accurate because real content exists).
- Two actions:
- Save = persist the current editor HTML as a version, then call finalize (folderId = selected folder) → a PDF version. The doc stays editable (the HTML source remains). On the first Save, also assign the Document ID from the chosen format and set the real title/classification.
- Save as draft = persist the current editor HTML as a version only (no PDF) and move the doc into the selected folder. Title/classification/ID handled the same way. The doc is not publishable until a real Save (existing gate: current version must be a PDF).
- On subsequent saves the folder box defaults to the doc's current folder; the Document ID is already assigned and shown read-only.

Component 4 — Table badge + version labels (Phase B)

  • Table badge: the documents table shows a small "Editable" badge on rows that have an HTML source version. Because the table list does not currently fetch per-row versions, add a small derived boolean to the documents list response — editable, computed as "the document has at least one text/html version" (exactly the existing isTextDoc derivation at DocumentDetailView.tsx:119, so the table badge and the detail-view "Edit content" affordance agree) — and render it in the existing DocFlags component (alongside shared/locked/hold).
  • Version labels: in the versions tab, render "Editable draft" for text/html and "PDF" for application/pdf instead of the raw MIME string. Presentation only — no versions-model change.

Backend additions (Phase B only — small)

  1. Deferred Document ID assignment: a new service method + endpoint to assign a Document ID (Reference) to an existing document that has none, from a chosen format — reusing allocateReference. It must reject if a Reference is already assigned (immutability). Called by the Save modal on first save. (CreateDocument today assigns the Reference inline via allocateReference and there is no post-create assign path, so this is genuinely new.)
  2. Editable flag on the documents list DTO: the derived boolean above, so the table badge doesn't need a per-row versions fetch.
  3. Everything else reuses existing endpoints (finalize with folderId, uploadVersion, moveDocument, setDocumentClassification, renameDocument, the semantic/suggest-folder endpoint).

Out of scope

  • Real-time collaborative editing, version diff/compare, inline (non-base64) image upload — unchanged.
  • Changing the finalize/publish semantics: re-editing a finalized (PDF) doc reverts its current version to HTML, so it is un-publishable until re-Saved. This existing tradeoff is kept, not changed.
  • Hiding unfiled drafts from listings: unfiled write-drafts appear under Unfiled/root until first Save (acceptable for v1; a "drafts" filter is a possible later follow-up).

Testing / verification

Per repo discipline: never go test (writes the live demo Postgres). Verify via go build ./... && go vet ./...; cd web tsc --noEmit + vite build; and a deployed e2e:
- Phase A: Upload Document opens a drag-drop modal; dropping a file expands the form and autofills the title (extension stripped); the folder box shows AI suggestions (semantic on) + current folder + browse, defaults to current folder, and files the upload into the selected folder. With semantic unlicensed the box shows only current-folder + browse (no AI rows) and upload still works.
- Phase B: Write Document lands directly in the editor on an unfiled, ID-less draft; Save assigns the ID from the chosen format, files into the selected folder, produces a PDF version, and the doc remains editable + shows the table "Editable" badge; Save as draft produces an HTML version only, files the doc, and leaves it un-publishable; the versions tab labels HTML vs PDF; a second Save on an already-ID'd doc does not re-assign the ID.
- After every deploy assert /me enabled_modules unchanged (still the 5 modules) and the demo stays intact; clean up test docs. Regenerate web/src/api/schema.ts (npm run gen:api) after any OpenAPI change.