Commit Graph

3 Commits

Author SHA1 Message Date
mAi
bd7896ef68 feat(submissions): Composer Slice F — section reorder / hide / add custom (m/paliad#141)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
The final Composer slice per design doc §12. Lawyer gains full
control over section composition: drag-and-drop reorder, per-section
delete, "+ Add section" picker for custom slugs that don't appear in
the base's default spec. Combined with Slice B's hide toggle, this
closes out the A→F sequence — Composer A→F is complete.

Backend (internal/services/submission_section_service.go, +120 LoC):

- SectionService.Create — adds a new section row to a draft. Validates
  section_key + labels + kind (must be prose/requests/evidence).
  Auto-assigns next order_index when OrderIndex=0; collisions on
  (draft_id, section_key) surface as ErrInvalidInput.

- SectionService.Delete — removes one section by id. Returns
  ErrSubmissionSectionNotFound when nothing was deleted.

- SectionService.Reorder — accepts a sequence of section_ids, rewrites
  every row's order_index to (1..N)×10 transactionally. Returns the
  refreshed list. Sections not present in the sequence are silently
  ignored (defensive — partial reorder doesn't lose rows).

Handlers (internal/handlers/submission_sections.go, +180 LoC):

- POST /api/submission-drafts/{draft_id}/sections — owner-scoped via
  SubmissionDraftService.Get. 400 on slug collision / invalid kind.
- DELETE /api/submission-drafts/{draft_id}/sections/{section_id} —
  owner + section-belongs-to-draft cross-check. 204 on success.
- POST /api/submission-drafts/{draft_id}/sections/reorder — accepts
  {"section_order": [uuid, uuid, ...]}; returns refreshed sections list.

Frontend (frontend/src/client/submission-draft.ts, +260 LoC):

- Each section row gains a drag handle (⋮⋮) on the left of the head.
  Drag handle is the only draggable element; contentEditable
  selections inside the editor body keep working. HTML5 native DnD,
  no library.
- Drop-target highlighting via .submission-draft-section--drop-target
  (border-top accent). Cleanup on dragend / drop / cancel.
- Per-section "Delete" button next to the existing Hide/Include
  toggle. Confirm prompt prevents accidental loss of typed prose.
- "+ Add section" trailing affordance below the section list opens an
  inline form (slug + DE label + EN label + kind dropdown). Submit
  POSTs to the new endpoint; on success splices the row into
  state.view.sections and re-paints.

CSS (frontend/src/styles/global.css, +65 LoC):

- .submission-draft-section-handle (grab cursor + hover background +
  active=grabbing).
- .submission-draft-section--dragging / --drop-target visual states.
- .submission-draft-add-section form layout (dashed border + lime
  primary submit).

Tests (internal/services/submission_section_slice_f_test.go, NEW,
TEST_DATABASE_URL-gated):
- Create custom section + slug-collision surface as ErrInvalidInput.
- Delete + repeat-delete returns ErrSubmissionSectionNotFound.
- Reorder reverses 10 seeded sections + verifies the resulting
  order_index sequence is ascending and matches the input order.

Build hygiene: go build/vet/test -short clean (all packages);
bun run build clean (2906 i18n keys, data-i18n scan clean).

Hard rules honoured:
- NO new migrations (Slice F is pure code on Slice A's schema).
- NO behavior change for pre-Composer drafts (no section rows → no
  drag handles to drag).
- {{rule.X}} aliases preserved (custom sections render through the
  same composer pipeline as default sections).
- Q2/Q9/Q10 ratifications preserved.

This closes the Composer slice sequence A → F. The full feature set
ratified by m on 2026-05-26 is now in place:
  A — base picker + read-only section list (mig 146/147/148)
  B — editable prose + anchor-spliced render + MD→OOXML walker
  C — building-blocks library + section picker (mig 149)
  D — rich prose (headings, lists, blockquote, hyperlinks)
  E — specialist bases lg-duesseldorf + upc-formal (mig 150)
  F — section reorder / delete / add custom

t-paliad-318 Slice F
2026-05-26 20:26:53 +02:00
mAi
f963b0df34 feat(submissions): Composer Slice B — editable prose sections + anchor-spliced render (m/paliad#141)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
The "Composer actually works" milestone per the design at
docs/design-submission-generator-v2-2026-05-26.md §12 Slice B. Builds on
Slice A's substrate (submission_bases, submission_sections, base_id on
drafts); no new migrations needed.

Backend additions:

- internal/services/submission_md.go (~240 LoC): Markdown → OOXML
  walker. Per the head's Slice B brief, scope is paragraphs +
  bold/italic + blank-line spacing. Placeholders pass through
  unchanged for the v1 substitution pass. CRLF normalisation; nested
  formatting (***bold-italic***); two delimiter forms (* and _);
  XML-escaping for &/</>; explicit empty-paragraph emit so blank
  lines round-trip. 12 unit tests.

- internal/services/submission_compose.go (~470 LoC): SubmissionComposer
  service. Pipeline: ConvertDotmToDocx pre-pass → extract
  word/document.xml → render each included section's content_md_<lang>
  → splice via {{#section:KEY}}/{{/section:KEY}} anchor pairs in
  the body → strip anchors for excluded sections → append unanchored
  sections before <w:sectPr> → repack zip → run v1 placeholder pass.
  RE2-friendly anchor scanner walks markers in body-order and matches
  open/close pairs with a stack (handles unbalanced anchors
  defensively). 6 unit tests covering anchor-mode splice,
  append-mode-no-anchors, excluded-section drop, placeholder
  resolution, lang column pick, order_index ASC.

- internal/services/submission_section_service.go: SectionPatch +
  Update method. Six optional fields (content_md_de/en, included,
  label_de/en, order_index). Sentinel ErrSubmissionSectionNotFound on
  RLS-filtered miss.

- internal/handlers/submission_sections.go (NEW, ~150 LoC):
  PATCH /api/submission-drafts/{draft_id}/sections/{section_id}.
  Owner-scoped via SubmissionDraftService.Get; section-belongs-to-draft
  cross-check. 404 on both missing-draft and section-belongs-elsewhere
  paths.

- internal/handlers/files.go: fetchComposerBaseBytes + composerBaseSlugMap
  reuse the existing Gitea proxy cache for base .docx bytes. hlc-letterhead
  → existing firmSkeletonSubmissionSlug, neutral → existing
  skeletonSubmissionSlug.

- internal/handlers/submission_drafts.go: exportSubmissionDraft helper
  branches on draft.BaseID. When set AND base + bytes + sections all
  resolve → Composer pipeline. Else v1 fallback render path stays.
  Audit metadata jsonb gains "composer": true + "base_id" flag when
  composer was used.

Wiring:
- handlers.Services gains SubmissionComposer.
- dbServices.submissionComposer wired from svc.SubmissionComposer.
- main.go instantiates NewSubmissionComposer with the existing
  SubmissionRenderer (so the {{rule.X}} alias contract stays preserved
  inside section content).

Frontend additions (~400 LoC):
- client/submission-draft.ts: paintSectionList rewritten to render a
  contentEditable per included section with a per-section B/I
  toolbar. Per-section autosave debounced 500ms; mousedown handlers on
  toolbar buttons preserve editor focus mid-command. domToMarkdown
  walks the contentEditable's DOM tree back to Markdown source-of-
  truth (b/strong → **…**, i/em → *…*, div/p → paragraph break, br
  → newline). Updated state.view.sections in-place on PATCH success
  without re-painting (avoids focus-stealing on every keystroke);
  re-paints only on structural changes (included toggle, label edits,
  order changes).

- client/submission-draft.ts: onSectionToggleIncluded hides/shows a
  section via PATCH. flushSectionAutosave on blur force-flushes
  pending edits so leaving an editor doesn't strand unsynced changes.

- styles/global.css: editor surface (contentEditable area with focus
  ring + placeholder), toolbar buttons (B/I 1.8rem squares),
  per-section "Hide"/"Include" toggle in the head row.

- Updated i18n hint copy: "Inhalt pro Abschnitt — Autosave nach
  500ms. Letztes Layout in Word."

Templates regenerated on Gitea:
- _skeleton.docx → composer-mode body (anchors only): blob SHA
  ac0cdeaf49f7cd417ec143e2319ffbb02ec65644.
- _firm-skeleton.docx → composer-mode body (anchors only, preserves
  sectPr → firm header/footer rIds): blob SHA
  f1e9a9fb9a29ca01bf7bee709a45c5dda2a8e317.
- Both uploaded as mAi via --netrc-file ~/.netrc-mai.
- gen-skeleton-submission-template script gains an -anchors flag
  (default true) so future regens emit composer-ready bodies. The
  _firm-skeleton.docx regen was done via a one-off /tmp helper since
  the gen-hl-skeleton-template script requires the proprietary .dotm
  source which lives in HL/mWorkRepo; extending that script to accept
  an existing .docx as input is a follow-up cleanup.

Build hygiene: go build/vet/test -short ./internal/... ./cmd/... all
clean; bun run build clean (2900 i18n keys, data-i18n scan clean).

NO behavior change for pre-Composer drafts (base_id NULL → v1
fallback render path stays compiled in). NO migrations needed in this
slice — sections were already in the schema from Slice A; only
content_md_de/en UPDATEs happen via the new PATCH endpoint.

Hard rules per Q2/Q10 ratification still honoured:
- No building_block_id lineage (Slice C territory; Q2).
- Caption/letterhead/signature are regular prose sections, seeded from
  base spec; lawyer can edit/hide freely (Q10).
- {{rule.X}} aliases preserved (renderer pass unchanged).

NOT in scope per Slice B brief:
- Headings 1–3, lists, blockquote (Slice D's MD walker extension).
- Building blocks library (Slice C).
- Reorder / add-custom-section (Slice F).
- Auto-upgrade of pre-Composer drafts (Slice C — explicitly NOT in
  this slice per head's brief msg #2393).

t-paliad-313 Slice B
2026-05-26 19:45:29 +02:00
mAi
e2969fc358 feat(submissions): Composer Slice A — base picker + read-only section list (m/paliad#141)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled
The first slice of the Submission generator v2 ("Composer") per the
design at docs/design-submission-generator-v2-2026-05-26.md §12 Slice A.
Ships the base concept + per-draft section seeding end-to-end with NO
change to the .docx render path — v1 export still works exactly as
today.

Schema (mig 146/147/148):
- paliad.submission_bases — catalog table; one row per template base
  (slug, firm, proceeding_family, label_de/en, gitea_path, section_spec
  jsonb, is_default_for[]). RLS: wide-open SELECT for authenticated
  users, mutations admin-only (handler-enforced, no RLS write paths).
  Seeded with 2 rows: hlc-letterhead → _firm-skeleton.docx; neutral →
  _skeleton.docx. Each section_spec carries the 10-section default
  (letterhead, caption, introduction, requests, facts, legal_argument,
  evidence, exhibits, closing, signature) with bilingual labels +
  bag-driven seed Markdown for caption/letterhead/signature.
- paliad.submission_drafts gains base_id (FK SET NULL, optional) +
  composer_meta jsonb (default '{}'). Purely additive; pre-Composer
  drafts keep base_id NULL → v1 fallback render path stays active.
- paliad.submission_sections — per-draft section rows (draft_id,
  section_key, order_index, kind ∈ {prose,requests,evidence},
  label_de/en, included, content_md_de/en). RLS mirrors
  submission_drafts (owner-scoped + can_see_project, four policies).

Backend:
- BaseService (read-only Slice A): List + GetByID + GetBySlug +
  GetDefaultForCode (firm/family fallback chain).
- SectionService: ListForDraft + Get + SeedFromSpec (transactional
  multi-INSERT).
- SubmissionDraftService.AttachComposer wires both; Create resolves
  the firm default base and seeds base_id + section rows in one tx.
  Composer wiring is additive — when bases==nil the service stays
  v1-shaped.
- Update accepts BaseID **uuid.UUID (set / clear / no-change).
- submissionDraftView gains BaseID, ComposerMeta, Sections fields.
- Routes: GET /api/submission-bases (catalog list). PATCH endpoints
  on both project-scoped and global drafts accept "base_id".

Frontend:
- submission-draft.tsx: base picker dropdown above language toggle
  (hidden until catalog loads); section-list pane above the preview
  (hidden when no rows).
- client/submission-draft.ts: loadBases() parallel-fetches on boot;
  paintBasePicker rebuilds <option> list on every paint; onBaseChange
  PATCHes base_id and repaints; paintSectionList renders each section
  read-only (label + kind chip + excluded badge + Markdown body).
- Per the brief: NO auto-upgrade of existing 11 drafts (that's Slice C).
  Pre-Composer drafts get the picker (catalog still loads) but the
  section pane stays hidden until they pick a base on a new draft.

Tests:
- TestFamilyOfCode + TestBaseSectionSpec_DecodeShape + _EmptyDecode
  (pure unit, no DB).
- TestComposerSeedFlow (live, TEST_DATABASE_URL-gated): asserts mig 146
  seeded 10 default sections on both bases; GetDefaultForCode picks
  hlc-letterhead for HLC/de.inf.lg.erwidg; new draft via Create seeds
  base_id + 10 section rows in tx with ascending order_index and
  bilingual labels populated.

NO behavior change to .docx export — the v1 path stays sole render
path this slice. Composer's anchor-based assembly engine + MD→OOXML
walker land in Slice B.

Build hygiene: go build/vet/test -short clean; bun run build clean
(2900 i18n keys, data-i18n scan clean).

t-paliad-313
2026-05-26 19:23:40 +02:00