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
117 lines
5.0 KiB
SQL
117 lines
5.0 KiB
SQL
-- t-paliad-313 (m/paliad#141): Composer Slice A — per-draft section rows.
|
|
--
|
|
-- paliad.submission_sections holds one row per (draft, section_key) for
|
|
-- Composer-mode drafts. Slice A seeds rows on draft create from the
|
|
-- base's section_spec.defaults; the editor renders them read-only. Slice
|
|
-- B turns them editable, Slice F adds reorder/hide/add-custom.
|
|
--
|
|
-- kind values per the design (Q10 ratification — no *_auto kind):
|
|
-- 'prose' — free Markdown content (default).
|
|
-- 'requests' — Anträge-style content (editor may add auto-numbering
|
|
-- later; Slice A treats identical to 'prose').
|
|
-- 'evidence' — Beweisangebote (editor may prefix lines with
|
|
-- 'Beweis: '; Slice A treats identical to 'prose').
|
|
--
|
|
-- Visibility flows through draft_id → submission_drafts → can_see_project
|
|
-- + owner-scoped. RLS policies mirror the four-policy shape on
|
|
-- submission_drafts so seeding from the Go service stays inside the
|
|
-- same RLS envelope.
|
|
--
|
|
-- content_md_de + content_md_en both NOT NULL DEFAULT '' so neither
|
|
-- side blocks the bilingual-by-construction render path. Empty content
|
|
-- renders as the missing-content marker per the editor's contract.
|
|
--
|
|
-- Per the brief (head's instruction msg #2392) Slice A does NOT auto-
|
|
-- upgrade the 11 pre-Composer drafts — those remain base_id=NULL with
|
|
-- no section rows. The v1 fallback render path stays compiled in to
|
|
-- keep them working.
|
|
|
|
CREATE TABLE IF NOT EXISTS paliad.submission_sections (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
draft_id uuid NOT NULL REFERENCES paliad.submission_drafts(id) ON DELETE CASCADE,
|
|
section_key text NOT NULL,
|
|
order_index int NOT NULL,
|
|
kind text NOT NULL,
|
|
label_de text NOT NULL,
|
|
label_en text NOT NULL,
|
|
included bool NOT NULL DEFAULT true,
|
|
content_md_de text NOT NULL DEFAULT '',
|
|
content_md_en text NOT NULL DEFAULT '',
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
|
|
CONSTRAINT submission_sections_kind_check
|
|
CHECK (kind IN ('prose', 'requests', 'evidence')),
|
|
CONSTRAINT submission_sections_unique_per_draft
|
|
UNIQUE (draft_id, section_key)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS submission_sections_draft_idx
|
|
ON paliad.submission_sections (draft_id, order_index);
|
|
|
|
ALTER TABLE paliad.submission_sections ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS submission_sections_select ON paliad.submission_sections;
|
|
CREATE POLICY submission_sections_select
|
|
ON paliad.submission_sections FOR SELECT TO authenticated
|
|
USING (
|
|
EXISTS (
|
|
SELECT 1 FROM paliad.submission_drafts d
|
|
WHERE d.id = paliad.submission_sections.draft_id
|
|
AND d.user_id = auth.uid()
|
|
AND (d.project_id IS NULL OR paliad.can_see_project(d.project_id))
|
|
)
|
|
);
|
|
|
|
DROP POLICY IF EXISTS submission_sections_insert ON paliad.submission_sections;
|
|
CREATE POLICY submission_sections_insert
|
|
ON paliad.submission_sections FOR INSERT TO authenticated
|
|
WITH CHECK (
|
|
EXISTS (
|
|
SELECT 1 FROM paliad.submission_drafts d
|
|
WHERE d.id = paliad.submission_sections.draft_id
|
|
AND d.user_id = auth.uid()
|
|
AND (d.project_id IS NULL OR paliad.can_see_project(d.project_id))
|
|
)
|
|
);
|
|
|
|
DROP POLICY IF EXISTS submission_sections_update ON paliad.submission_sections;
|
|
CREATE POLICY submission_sections_update
|
|
ON paliad.submission_sections FOR UPDATE TO authenticated
|
|
USING (
|
|
EXISTS (
|
|
SELECT 1 FROM paliad.submission_drafts d
|
|
WHERE d.id = paliad.submission_sections.draft_id
|
|
AND d.user_id = auth.uid()
|
|
AND (d.project_id IS NULL OR paliad.can_see_project(d.project_id))
|
|
)
|
|
)
|
|
WITH CHECK (
|
|
EXISTS (
|
|
SELECT 1 FROM paliad.submission_drafts d
|
|
WHERE d.id = paliad.submission_sections.draft_id
|
|
AND d.user_id = auth.uid()
|
|
AND (d.project_id IS NULL OR paliad.can_see_project(d.project_id))
|
|
)
|
|
);
|
|
|
|
DROP POLICY IF EXISTS submission_sections_delete ON paliad.submission_sections;
|
|
CREATE POLICY submission_sections_delete
|
|
ON paliad.submission_sections FOR DELETE TO authenticated
|
|
USING (
|
|
EXISTS (
|
|
SELECT 1 FROM paliad.submission_drafts d
|
|
WHERE d.id = paliad.submission_sections.draft_id
|
|
AND d.user_id = auth.uid()
|
|
AND (d.project_id IS NULL OR paliad.can_see_project(d.project_id))
|
|
)
|
|
);
|
|
|
|
DROP TRIGGER IF EXISTS submission_sections_set_updated_at ON paliad.submission_sections;
|
|
CREATE TRIGGER submission_sections_set_updated_at
|
|
BEFORE UPDATE ON paliad.submission_sections
|
|
FOR EACH ROW EXECUTE FUNCTION paliad.tg_set_updated_at();
|
|
|
|
COMMENT ON TABLE paliad.submission_sections IS
|
|
't-paliad-313: per-draft Composer section rows. Slice A: seeded on draft create from base.section_spec.defaults, rendered read-only. Slice B: editable. RLS mirrors submission_drafts (owner-scoped + can_see_project).';
|