From ee98db94fafbb982f8029e7e2573671740791304 Mon Sep 17 00:00:00 2001 From: mAi Date: Tue, 26 May 2026 20:04:40 +0200 Subject: [PATCH] =?UTF-8?q?feat(submissions):=20Composer=20Slice=20C=20?= =?UTF-8?q?=E2=80=94=20building=20blocks=20library=20(m/paliad#141)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the design at docs/design-submission-generator-v2-2026-05-26.md §8 and the Q2 / Q9 ratifications: - Q2 (m, 2026-05-26): building blocks are plain text paste sources. No building_block_id reference is stored on submission_sections. - Q9 (m, 2026-05-26): four visibility tiers — private / team / firm / global. Schema (mig 149): - paliad.submission_building_blocks — library catalog. Columns: slug, firm (NULL = cross-firm), section_key (binds to one section kind), proceeding_family (NULL = any), title_de/_en + description_de/_en + content_md_de/_en, author_id, visibility (CHECK in 4-tier set), is_published, created_at, updated_at, deleted_at (soft delete). RLS: coarse-grained SELECT — every authenticated user sees non-deleted non-private rows + own private rows. Tier-specific predicate (private/team/firm/global) applied in Go-layer service so semantics evolve without RLS migrations. Mutations admin-only (no RLS write paths). - paliad.submission_building_block_admin_versions — append-only history per block, retention=20. Admin-side only; NOT referenced from submission_sections (per Q2's plain-text-paste model). Exists so accidental delete / overwrite are recoverable. Backend: - internal/services/submission_building_block_service.go (~510 LoC): BuildingBlockService. ListVisible applies tier predicate at query time (private = author_id match; firm = firm column NULL OR matches branding.Name; team = author shares a project_team with caller via paliad.project_teams self-join; global = open). ListAllForAdmin drops the predicate. Create + Update + SoftDelete + RestoreVersion all transactional; appendVersionTx writes one audit row + GC-deletes anything past the retention=20 horizon in the same tx. InsertIntoSection (the paste mechanic) clones content_md_ into the section row with a "\n\n" separator if section already has content. NO building_block_id stamped per Q2. - internal/handlers/submission_building_blocks.go (~480 LoC): nine handlers split between the lawyer-facing picker (list, insert) and the admin editor (list, get, create, update, delete, list-versions, restore-version, page). buildingBlockUpdateInput uses presence- tracking UnmarshalJSON for the four nullable fields (firm, proceeding_family, description_de/_en) so PATCH can distinguish "no change" from "set to null". - Routes registered: lawyer-facing under /api/submission-building-blocks, admin-gated under /api/admin/submission-building-blocks/* and /admin/submission-building-blocks (page). - Wiring: handlers.Services + dbServices + cmd/server/main.go all gain SubmissionBuildingBlock. NewBuildingBlockService takes the branding.Name firm hint for the visibility predicate. Frontend: - frontend/src/admin-submission-building-blocks.tsx (~85 LoC): three-pane admin shell (list / editor / version log) registered in build.ts. - frontend/src/client/admin-submission-building-blocks.ts (~370 LoC): admin client — list paint, edit form (slug + firm + section_key + proceeding_family + title/desc/content per lang + visibility radio + is_published toggle), per-block version log with restore button. Bilingual labels. - frontend/src/client/submission-draft.ts: per-section "+ Baustein" button on the Composer editor toolbar (Slice B substrate gets one more affordance). openBlockPicker opens a modal filtered to the section's section_key, 200ms-debounced search by free text against title/description/content. Click a hit → POST insert-into-section → section row's content_md_ gains the block's content appended at the end (Q2's plain-text paste semantic, no lineage). - ~240 LoC of CSS: modal overlay + picker rows with tier-colored visibility chips + admin editor 3-pane grid + form rows + version list. - 12 new i18n keys × 2 langs (admin.building_blocks.*). Tests: - TestValidVisibility (8 cases including case-sensitivity + empty). - TestAppendBlockContent (8 cases covering empty-existing / empty- addition / whitespace-only / trailing newline collapse). - TestBuildingBlockVisibilityConstants pins the 4 string literals against drift (RLS predicate + DB CHECK depend on them). Build hygiene: go build/vet/test -short clean; bun run build clean (2906 i18n keys, data-i18n scan clean). Hard rules per ratifications honoured: - Q2: no building_block_id lineage on sections (paste is plain text). - Q9: 4 visibility tiers (private/team/firm/global). - NO behavior change for pre-Composer drafts (the picker just doesn't show — section list is hidden for base_id NULL drafts). - {{rule.X}} aliases preserved (block content goes through the same v1 placeholder pass on export as section prose). NOT in scope per Slice C brief: - User-authored private blocks (Slice C ships admin curation only; any-user create is a follow-up). - Tier promotion review workflow (admin sets tier directly today). - Per-section "where is this block used" reverse lookup (no lineage to query). - Slice D's rich-prose features (headings, lists, blockquote) still Slice D's job; this Slice doesn't extend the MD walker. t-paliad-315 Slice C --- cmd/server/main.go | 11 +- frontend/build.ts | 3 + .../src/admin-submission-building-blocks.tsx | 77 +++ .../admin-submission-building-blocks.ts | 429 ++++++++++++ frontend/src/client/i18n.ts | 14 + frontend/src/client/submission-draft.ts | 166 +++++ frontend/src/i18n-keys.ts | 6 + frontend/src/styles/global.css | 238 +++++++ .../149_submission_building_blocks.down.sql | 4 + .../149_submission_building_blocks.up.sql | 118 ++++ internal/handlers/handlers.go | 27 +- internal/handlers/projects.go | 9 +- .../handlers/submission_building_blocks.go | 482 ++++++++++++++ .../submission_building_block_service.go | 629 ++++++++++++++++++ .../submission_building_block_service_test.go | 60 ++ 15 files changed, 2261 insertions(+), 12 deletions(-) create mode 100644 frontend/src/admin-submission-building-blocks.tsx create mode 100644 frontend/src/client/admin-submission-building-blocks.ts create mode 100644 internal/db/migrations/149_submission_building_blocks.down.sql create mode 100644 internal/db/migrations/149_submission_building_blocks.up.sql create mode 100644 internal/handlers/submission_building_blocks.go create mode 100644 internal/services/submission_building_block_service.go create mode 100644 internal/services/submission_building_block_service_test.go diff --git a/cmd/server/main.go b/cmd/server/main.go index 3e9580f..b6de1d9 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -173,6 +173,8 @@ func main() { // the {{rule.X}} alias contract stays preserved inside the // composed body. submissionComposerSvc := services.NewSubmissionComposer(submissionRenderer) + // t-paliad-315 Slice C — building-block library. + submissionBuildingBlockSvc := services.NewBuildingBlockService(pool, branding.Name) // t-paliad-225 Slice A — user-authored checklist templates. // Slice B adds checklist_shares grants + admin promotion. checklistCatalogSvc := services.NewChecklistCatalogService(pool) @@ -184,10 +186,11 @@ func main() { Team: teamSvc, PartnerUnit: partnerUnitSvc, Party: partySvc, - SubmissionDraft: submissionDraftSvc, - SubmissionBase: submissionBaseSvc, - SubmissionSection: submissionSectionSvc, - SubmissionComposer: submissionComposerSvc, + SubmissionDraft: submissionDraftSvc, + SubmissionBase: submissionBaseSvc, + SubmissionSection: submissionSectionSvc, + SubmissionComposer: submissionComposerSvc, + SubmissionBuildingBlock: submissionBuildingBlockSvc, Deadline: deadlineSvc, Appointment: appointmentSvc, CalDAV: caldavSvc, diff --git a/frontend/build.ts b/frontend/build.ts index b6041af..a3e4e82 100644 --- a/frontend/build.ts +++ b/frontend/build.ts @@ -40,6 +40,7 @@ import { renderAdminTeam } from "./src/admin-team"; import { renderAdminAuditLog } from "./src/admin-audit-log"; import { renderAdminPartnerUnits } from "./src/admin-partner-units"; import { renderAdminEmailTemplates } from "./src/admin-email-templates"; +import { renderAdminSubmissionBuildingBlocks } from "./src/admin-submission-building-blocks"; import { renderAdminEmailTemplatesEdit } from "./src/admin-email-templates-edit"; import { renderAdminEventTypes } from "./src/admin-event-types"; import { renderAdminApprovalPolicies } from "./src/admin-approval-policies"; @@ -278,6 +279,7 @@ async function build() { join(import.meta.dir, "src/client/admin-partner-units.ts"), join(import.meta.dir, "src/client/admin-email-templates.ts"), join(import.meta.dir, "src/client/admin-email-templates-edit.ts"), + join(import.meta.dir, "src/client/admin-submission-building-blocks.ts"), join(import.meta.dir, "src/client/admin-event-types.ts"), join(import.meta.dir, "src/client/admin-approval-policies.ts"), join(import.meta.dir, "src/client/admin-broadcasts.ts"), @@ -409,6 +411,7 @@ async function build() { await Bun.write(join(DIST, "admin-partner-units.html"), renderAdminPartnerUnits()); await Bun.write(join(DIST, "admin-email-templates.html"), renderAdminEmailTemplates()); await Bun.write(join(DIST, "admin-email-templates-edit.html"), renderAdminEmailTemplatesEdit()); + await Bun.write(join(DIST, "admin-submission-building-blocks.html"), renderAdminSubmissionBuildingBlocks()); await Bun.write(join(DIST, "admin-event-types.html"), renderAdminEventTypes()); await Bun.write(join(DIST, "admin-approval-policies.html"), renderAdminApprovalPolicies()); await Bun.write(join(DIST, "admin-broadcasts.html"), renderAdminBroadcasts()); diff --git a/frontend/src/admin-submission-building-blocks.tsx b/frontend/src/admin-submission-building-blocks.tsx new file mode 100644 index 0000000..bc6da79 --- /dev/null +++ b/frontend/src/admin-submission-building-blocks.tsx @@ -0,0 +1,77 @@ +import { h } from "./jsx"; +import { Sidebar } from "./components/Sidebar"; +import { PaliadinWidget } from "./components/PaliadinWidget"; +import { BottomNav } from "./components/BottomNav"; +import { Footer } from "./components/Footer"; +import { PWAHead } from "./components/PWAHead"; + +// /admin/submission-building-blocks — Composer building-blocks library +// editor (t-paliad-315 Slice C). Three-pane layout: list on the left, +// edit form in the middle, version log on the right. Hydrated by +// client/admin-submission-building-blocks.ts from +// GET /api/admin/submission-building-blocks. + +export function renderAdminSubmissionBuildingBlocks(): string { + return "" + ( + + + + + + + + + Bausteine — Paliad + + + + + + +
+
+
+
+
+

Bausteine

+

+ Wiederverwendbare Textbausteine für Composer-Abschnitte. +

+
+
+ +
+
+ + +
+
+ +