docs(project-editor): document config schema, form engine, validation
Milestone 6 (final) of the Configuration Engine sprint. - docs/EDITOR.md: new "Configuration schema, form engine, and validation architecture" section covering the field-schema registry, centralized validators, live inline feedback, undo/redo, modified-field tracking, and pre-publish preview added in M1-M5. Updated the facade signal list and folder tour to include schema/. - ADR-0002 (docs/context/adrs/): records the metadata-augmented-vs-fully- schema-driven decision, why severity splits blocking/advisory, and the accepted debt (partial [error] binding coverage, schema not yet driving template labels). - FACTS.jsonl (project-editor): decision fact pointing at the ADR. Note: `barry-cache` is a phantom devDependency (no bin resolves, confirmed in M1) - ADR/FACTS were authored by hand matching the existing schema/format rather than via `npm run barry -- adr new` / `validate`. Gate: tsc --noEmit, npm test (33/33), arch:check, build all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ src/app/features/project-editor/
|
|||||||
sections/ one component per editor tab (see below)
|
sections/ one component per editor tab (see below)
|
||||||
components/ shared editor UI (save bar, HTML editor)
|
components/ shared editor UI (save bar, HTML editor)
|
||||||
models/ ProjectEditorState, EDITOR_SECTION_BOOTSTRAP_KEYS
|
models/ ProjectEditorState, EDITOR_SECTION_BOOTSTRAP_KEYS
|
||||||
|
schema/ field-schema registry, validators/, history.util (Sprint X+1, see below)
|
||||||
services/ ProjectValidator, ProjectEditorDraftStorageService, LocaleSyncService
|
services/ ProjectValidator, ProjectEditorDraftStorageService, LocaleSyncService
|
||||||
facade/ ProjectEditorFacade
|
facade/ ProjectEditorFacade
|
||||||
```
|
```
|
||||||
@@ -18,7 +19,7 @@ Route: `/edit/:section` or `/{lang}/edit/:section`.
|
|||||||
|
|
||||||
## Facade
|
## Facade
|
||||||
|
|
||||||
`ProjectEditorFacade` exposes: `loadBootstrap()`, `updateBootstrap(updater)`, `exportBootstrap()`, `importBootstrap()`, `preview()`, `save()`, `publish()`, plus signals `bootstrap`, `status` (`draft|published`), `dirty`, `lastSavedAt`, `lastPublishedAt`, `validationIssues`, `homepageWidgets`, `homepagePage`. Components in `sections/*` inject this facade directly (an accepted exception to the presentational-component rule, per ADR-006 — these are container/section components, not shared UI).
|
`ProjectEditorFacade` exposes: `loadBootstrap()`, `updateBootstrap(updater)`, `exportBootstrap()`, `importBootstrap()`, `preview()`, `save()`, `publish()`, `undo()`, `redo()`, plus signals `bootstrap`, `status` (`draft|published`), `dirty`, `canUndo`, `canRedo`, `lastSavedAt`, `lastPublishedAt`, `validationIssues`, `blockingIssues`, `hasBlockingIssues`, `issuesByField`, `issuesBySection`, `modifiedFields`, `modifiedSections`, `changeSummary`, `homepageWidgets`, `homepagePage`, plus the `fieldError(key)` method. Components in `sections/*` inject this facade directly (an accepted exception to the presentational-component rule, per ADR-006 — these are container/section components, not shared UI). See "Configuration schema, form engine, and validation architecture" below for the schema/validator/undo internals.
|
||||||
|
|
||||||
## Sections
|
## Sections
|
||||||
|
|
||||||
@@ -46,9 +47,43 @@ Route: `/edit/:section` or `/{lang}/edit/:section`.
|
|||||||
- **Per-field reset is not implemented** — no per-field default registry exists; only section- and project-level reset.
|
- **Per-field reset is not implemented** — no per-field default registry exists; only section- and project-level reset.
|
||||||
- **No backend persistence exists for any of this today** — see `docs/BACKEND.md` item 2 for the endpoints needed.
|
- **No backend persistence exists for any of this today** — see `docs/BACKEND.md` item 2 for the endpoints needed.
|
||||||
|
|
||||||
## Validation
|
## Configuration schema, form engine, and validation architecture (Sprint X+1)
|
||||||
|
|
||||||
`ProjectValidator` (`services/project-validator.service.ts`) runs on every save-bar render and blocks Publish (not Save) on: missing `branding.logoUrl`, no supported locales, invalid `tenant.websiteBaseUrl`, duplicate static-page slugs (falls back to `route`), empty homepage, a homepage widget with no `type`, duplicate header nav links, invalid theme colors, missing translations for a supported locale, and layout/section-layout values outside the known enums (`PlatformLayoutType`, `SectionLayoutStrategy`).
|
**Approach: metadata-augmented, not fully schema-driven.** Section templates stay hand-authored (`sections/*.component.html`); a field-schema registry sits alongside them as the single source of truth for field identity, labels, and validator wiring. This was chosen over a schema-driven renderer to preserve every existing template/UX pixel-for-pixel while still centralizing metadata and validation — the highest-value, lowest-regression-risk option given 11 mature section templates already built on the `shared/ui` primitives (see the primitives table above).
|
||||||
|
|
||||||
|
### Field-schema registry (`schema/`)
|
||||||
|
|
||||||
|
- `field-schema.model.ts` — `FieldSchema`: `{ key, section, type, labelKey, hintKey?, default?, required?, validators? }`. `key` is a dot path into `BootstrapConfig` (e.g. `theme.palette.primary`), unique per section. `validators` references reusable validator names (`hexColor`, `url`, `email`, `json`, `css`, `localeCompleteness`, `duplicateRoutes`, `widgetConfig`) rather than embedding logic.
|
||||||
|
- `editor-schema.ts` — `SECTION_FIELD_SCHEMAS`: every editable field, one entry per section, sourced from what each template already renders. `ALL_FIELD_SCHEMAS` flattens it.
|
||||||
|
- `editor-schema.service.ts` (`EditorSchemaService`, `providedIn: 'root'`) — `getFields(section)`, `getField(key)`, `all()`, `getByPath(source, key)` (safe dot-path resolver, never throws on a missing segment).
|
||||||
|
|
||||||
|
The schema is currently consumed by the facade (validation issue → field mapping, modified-field diffing, change-summary labels), not by the templates directly — templates keep calling `facade.updateBootstrap()` the same way they always did.
|
||||||
|
|
||||||
|
### Centralized validators (`schema/validators/`)
|
||||||
|
|
||||||
|
`primitives.ts` holds pure, framework-free functions — one per concern, reused everywhere that concern appears: `isValidHexColor`, `isValidHttpUrl`, `isValidEmail`, `validateJson`, `validateCss` (brace-balance check, comments stripped), `extractStyleBlocks` (pulls `<style>` bodies out of static-page HTML), `normalizeRoute` (trim/strip-slashes/lowercase for duplicate comparison).
|
||||||
|
|
||||||
|
`ProjectValidator` (`services/project-validator.service.ts`) composes these primitives into checks and tags every `ProjectValidationIssue` with `section`, `fieldKey`, and `severity` (`'error'` blocks Publish, `'warning'` is advisory). Checks: missing `branding.logoUrl`, no supported locales, invalid `tenant.websiteBaseUrl`, duplicate static-page slugs, **duplicate routes** across `pages`/`staticPages` (warning), empty homepage, a homepage widget with no `type`, **malformed widget config** — missing `id`/`type`/`version`/`props` (error), duplicate header nav links, invalid theme colors, **invalid CSS** inside static-page `<style>` blocks (warning), missing translations for a supported locale, and layout/section-layout values outside the known enums.
|
||||||
|
|
||||||
|
### Live inline feedback (facade)
|
||||||
|
|
||||||
|
`ProjectEditorFacade` exposes, on top of `validationIssues`: `blockingIssues` / `hasBlockingIssues` (severity-filtered), `issuesByField: Map<string, ProjectValidationIssue[]>`, `issuesBySection: Map<ProjectEditorSectionId, number>`, and `fieldError(key)` (first message for a field, or `null`). `publish()` gates on `hasBlockingIssues()`, not "any issue" — a duplicate-route or invalid-CSS warning no longer blocks publishing. Sections bind `[error]` on `app-form-field` for schema-backed fields (currently: theme palette, general name/domain, branding logo — the rest read `validationIssues`/`issuesBySection` via the save bar and nav badges rather than per-field, since not every section has been wired yet). `project-editor-nav` shows a red badge with the blocking-issue count per section.
|
||||||
|
|
||||||
|
### Undo / redo (`schema/history.util.ts` + facade)
|
||||||
|
|
||||||
|
A pure, framework-free reducer (`emptyHistory`, `commit`, `undo`, `redo`) over immutable `BootstrapConfig` snapshots, capped at 50 entries. The facade wraps it with **debounced commits** (~300ms): `updateBootstrap()` captures the pre-burst snapshot on the first call in a burst and only pushes it to history once edits settle, so a run of rapid typing collapses into one undo step instead of one per keystroke. `undo()`/`redo()` route through the same draft-save path as every other mutation, so the `localStorage` autosave never desyncs from the in-memory undo stack. History is cleared on `loadBootstrap()`, `publish()`, and `resetDraft()` (a fresh baseline invalidates old snapshots). UI: save-bar Undo/Redo buttons (`canUndo`/`canRedo`), `Ctrl/Cmd+Z` / `Ctrl/Cmd+Shift+Z` / `Ctrl/Cmd+Y` page-level shortcuts (skipped while a text field has focus, so native per-field text undo still works).
|
||||||
|
|
||||||
|
### Modified-field tracking
|
||||||
|
|
||||||
|
`modifiedFields` (facade, `computed<Set<string>>`) diffs every schema field's current value against `originalBootstrap`. `modifiedSections` rolls that up per section for an amber dot in the nav (shown only when a section has no blocking-issue badge). `changeSummary` builds the before/after rows (schema label + stringified value, truncated for objects) consumed by the Preview section below.
|
||||||
|
|
||||||
|
### Pre-publish preview
|
||||||
|
|
||||||
|
The Preview tab (`preview-section`) now opens with a "changes since last publish" card: the full validation issue list (warning/error styled) plus a before/after table from `changeSummary`, ahead of the existing export/import/live-preview card. Reuses the existing `ProjectEditorPreviewService.preview()` — no new preview mechanism, just more visibility before triggering it.
|
||||||
|
|
||||||
|
### What stayed the same
|
||||||
|
|
||||||
|
No changes to `ProjectEditorIoService` (export/import), `ProjectEditorDraftStorageService` (draft `localStorage` format), or the publish/draft/reset flow described above — draft/publish/import/export compatibility is fully preserved. Section templates are unchanged except for `[error]` bindings on already-existing `app-form-field` usages.
|
||||||
|
|
||||||
## Admin Authentication (QR reuse)
|
## Admin Authentication (QR reuse)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
id: ADR-0002
|
||||||
|
title: Project Editor field-schema registry, centralized validation, and metadata-augmented form engine
|
||||||
|
status: active
|
||||||
|
date: 2026-07-16
|
||||||
|
tags: ["project-editor", "schema", "validation", "undo-redo"]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The Project Editor (`src/app/features/project-editor/`) edits the tenant `BootstrapConfig` across 11 hand-authored section templates, all built on the `shared/ui` field primitives (ADR established post-Sprint 30 redesign; see `docs/EDITOR.md`). Field labels/hints/defaults lived inline per template, `ProjectValidator` issues were not addressable to a field, there was no undo/redo, and no per-field modified/error state. Sprint X+1 ("Configuration Engine & Dynamic Form Foundation") required: a field-schema registry, centralized validation (JSON/CSS/URL/color/locale/duplicate-route/widget-config), live inline validation with publish-gating, pre-publish preview, dirty/modified-field tracking with a leave-warning, and session undo/redo — without duplicating form logic or validators, and without breaking draft/publish/import/export.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Metadata-augmented, not fully schema-driven.** A field-schema registry (`schema/field-schema.model.ts`, `schema/editor-schema.ts`, `schema/editor-schema.service.ts`) declares every editable field (dot-path key, section, type, label/hint keys, default, required, validator refs) as the single source of truth for field identity and validator wiring — but section templates stay hand-authored. The schema drives validation and metadata; it does not render fields. This was chosen over a fully schema-driven renderer because 11 mature templates already exist on top of the `shared/ui` kit, and a renderer rewrite carried materially higher regression risk against "preserve all existing functionality" for no UX gain.
|
||||||
|
|
||||||
|
**Validators are pure, composed, and tagged.** `schema/validators/primitives.ts` holds one pure function per concern (hex color, HTTP URL, email, JSON, CSS brace-balance, style-block extraction, route normalization). `ProjectValidator` composes them and attaches `section`, `fieldKey`, and `severity` (`error` | `warning`) to every issue, so the same validator is never re-implemented per field or per section.
|
||||||
|
|
||||||
|
**Severity splits blocking from advisory.** `publish()` now gates on `hasBlockingIssues()` (`severity === 'error'`) instead of "any issue exists." All 9 pre-existing checks stayed `error` (no behavior change); the new duplicate-routes and invalid-CSS checks are `warning` — informative, non-blocking, by design.
|
||||||
|
|
||||||
|
**Undo/redo is a pure reducer wrapped in debounced facade state.** `schema/history.util.ts` is a framework-free `{past, future}` snapshot reducer (commit/undo/redo, depth-capped). The facade debounces commits (~300ms) so a typing burst collapses into one undo step, and routes undo/redo through the same `localStorage` draft-save path as every other mutation so the autosave never desyncs from the undo stack.
|
||||||
|
|
||||||
|
**Modified-field tracking is a schema diff, not a form-state library.** `modifiedFields` walks every schema field and compares current vs. `originalBootstrap` by dot-path — no new dependency, reuses `EditorSchemaService.getByPath`.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
Positive:
|
||||||
|
- One registry answers "what fields exist, what validates them, what do they mean" — new fields register once and get validation + inline-error wiring for free.
|
||||||
|
- No validator is duplicated: JSON/CSS/color/URL/email logic lives in exactly one place each.
|
||||||
|
- Zero changes to `ProjectEditorIoService`, `ProjectEditorDraftStorageService`, or the draft/publish/reset flow — full backward compatibility.
|
||||||
|
- Undo/redo and modified-field tracking added without a state-management library.
|
||||||
|
|
||||||
|
Negative / accepted debt:
|
||||||
|
- Inline `[error]` binding is wired on a subset of fields (theme palette, general name/domain, branding logo) — not yet every schema-backed field across all 11 sections. Section-level visibility (nav badges, save-bar issue list) covers the rest today.
|
||||||
|
- The field-schema registry is not yet consumed by templates for label/hint rendering (still inline i18n keys in each template) — only for validation, diffing, and change-summary labels. A future pass could fully drive labels from the schema.
|
||||||
|
- CSS/JSON validators have a thin binding surface today (CSS only via static-page `<style>` blocks; JSON only via import) since no dedicated `customCss`/raw-JSON field exists yet in `BootstrapConfig`.
|
||||||
|
|
||||||
|
## Compliance Requirements
|
||||||
|
|
||||||
|
- New editable `BootstrapConfig` fields should get a `FieldSchema` entry in `editor-schema.ts` alongside their template addition.
|
||||||
|
- New validation rules must be added as a pure function in `schema/validators/primitives.ts` and composed into `ProjectValidator` — never inlined ad hoc in a section component.
|
||||||
|
- `severity: 'error'` is reserved for checks that must block Publish; anything advisory is `'warning'`.
|
||||||
@@ -3,3 +3,4 @@
|
|||||||
{"id":"PE-20260713T010000Z-0003","subject":"draft-publish-flow","predicate":"is","object":"client-side only (ProjectEditorFacade.status/dirty/save/publish) because no backend draft/publish endpoint exists yet; PUT /builder/bootstrap/draft and POST /builder/bootstrap/publish are the documented backend gap","src":["docs/Project-Editor.md","src/app/features/project-editor/facade/project-editor.facade.ts"],"status":"active","kind":"constraint","confidence":"high","updated_at":"2026-07-13T01:00:00Z","tags":["project-editor","backend-gap"]}
|
{"id":"PE-20260713T010000Z-0003","subject":"draft-publish-flow","predicate":"is","object":"client-side only (ProjectEditorFacade.status/dirty/save/publish) because no backend draft/publish endpoint exists yet; PUT /builder/bootstrap/draft and POST /builder/bootstrap/publish are the documented backend gap","src":["docs/Project-Editor.md","src/app/features/project-editor/facade/project-editor.facade.ts"],"status":"active","kind":"constraint","confidence":"high","updated_at":"2026-07-13T01:00:00Z","tags":["project-editor","backend-gap"]}
|
||||||
{"id":"PE-20260713T010000Z-0004","subject":"html-editing","predicate":"uses","object":"MarketplaceHtmlEditorComponent, a contentEditable + toolbar component with no external rich-text dependency; emits raw HTML, never sanitizes during editing","src":["src/app/features/project-editor/components/html-editor/marketplace-html-editor.component.ts"],"status":"active","kind":"decision","confidence":"high","updated_at":"2026-07-13T01:00:00Z","tags":["project-editor","html-editor"]}
|
{"id":"PE-20260713T010000Z-0004","subject":"html-editing","predicate":"uses","object":"MarketplaceHtmlEditorComponent, a contentEditable + toolbar component with no external rich-text dependency; emits raw HTML, never sanitizes during editing","src":["src/app/features/project-editor/components/html-editor/marketplace-html-editor.component.ts"],"status":"active","kind":"decision","confidence":"high","updated_at":"2026-07-13T01:00:00Z","tags":["project-editor","html-editor"]}
|
||||||
{"id":"PE-20260713T010000Z-0005","subject":"navigation-tab","predicate":"supports","object":"header navigation and flat-list footer navigation (add/remove/reorder/edit); grouped-column footer navigation is read-only until a future sprint","src":["src/app/features/project-editor/sections/navigation-section.component.ts"],"status":"active","kind":"constraint","confidence":"high","updated_at":"2026-07-13T01:00:00Z","tags":["project-editor","navigation"]}
|
{"id":"PE-20260713T010000Z-0005","subject":"navigation-tab","predicate":"supports","object":"header navigation and flat-list footer navigation (add/remove/reorder/edit); grouped-column footer navigation is read-only until a future sprint","src":["src/app/features/project-editor/sections/navigation-section.component.ts"],"status":"active","kind":"constraint","confidence":"high","updated_at":"2026-07-13T01:00:00Z","tags":["project-editor","navigation"]}
|
||||||
|
{"id":"PE-20260716T220000Z-0006","subject":"config-schema-and-validation","predicate":"is-implemented-by","object":"a field-schema registry (schema/editor-schema.ts, EditorSchemaService) driving centralized, severity-tagged validation (ProjectValidator composing pure schema/validators/primitives functions) and debounced undo/redo (schema/history.util) in ProjectEditorFacade; section templates stay hand-authored (metadata-augmented, not schema-rendered)","src":["docs/context/adrs/ADR-0002-project-editor-config-schema-and-validation-engine.md","src/app/features/project-editor/schema/editor-schema.ts","src/app/features/project-editor/services/project-validator.service.ts","src/app/features/project-editor/facade/project-editor.facade.ts"],"status":"active","kind":"decision","confidence":"high","updated_at":"2026-07-16T22:00:00Z","tags":["project-editor","schema","validation","undo-redo"]}
|
||||||
|
|||||||
Reference in New Issue
Block a user