Milestone 7 of the Static Pages Module sprint.
- app.routes.ts: /backoffice/static-pages now redirects to /edit/static-pages
(absolute redirectTo) instead of rendering BackofficeComingSoonPageComponent
- Static Pages is a first-class Project Editor module, not a second CRUD
surface over the same bootstrap.staticPages data.
- New docs/StaticPages.md: full field reference (General/Localization/SEO/
Media/Publishing), the enabled+status storefront-gating story and its
backward-compat default (existing/legacy data normalizes to
enabled+published so nothing gets silently un-published; only new pages
default to draft), CRUD/search/filter/bulk, the "mutate from the
unfiltered list" implementation note, rich-text/HTML-mode contract
(pointer to EDITOR.md), nav integration, and the export/import/draft/
publish compatibility statement.
- docs/EDITOR.md: Static Pages row in the Sections table (was previously
absent - the row lived implicitly in Footer's description), HTML editor
section updated with the Sprint X+2 toolbar additions + validation
contract, redirect noted near the route line. `docs/Project-Editor.md`
(named in the original brief) no longer exists - superseded by EDITOR.md
per that file's own header; documentation went there + the new file
instead.
- docs/PROJECT.md: doc index entry for StaticPages.md.
Gate: tsc --noEmit, npm test (57/57), arch:check, build all green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
17 KiB
EDITOR (Project Editor)
Replaces the old docs/Project-Editor.md (content merged in below and extended with the Sprint 19 field-description/dropdown work).
The Project Editor (src/app/features/project-editor/) edits the tenant's BootstrapConfig (docs/BOOTSTRAP.md) directly — no parallel model. It is out of scope for products, categories, orders, or analytics management (those live under features/admin/*/features/backoffice/*, see docs/ADMIN.md).
src/app/features/project-editor/
pages/ route container
sections/ one component per editor tab (see below)
components/ shared editor UI (save bar, HTML editor)
models/ ProjectEditorState, EDITOR_SECTION_BOOTSTRAP_KEYS
schema/ field-schema registry, validators/, history.util (Sprint X+1, see below)
services/ ProjectValidator, ProjectEditorDraftStorageService, LocaleSyncService
facade/ ProjectEditorFacade
Route: /edit/:section or /{lang}/edit/:section. /backoffice/static-pages (the Admin dashboard) redirects here (/edit/static-pages) rather than hosting a second CRUD surface over the same bootstrap.staticPages data (Sprint X+2 — see docs/StaticPages.md).
Facade
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
| Section | Component | Covers |
|---|---|---|
| General | general-section |
marketplace name, domain, description, default/supported languages |
| Branding | branding-section |
logo, small logo, favicon, marketplace title |
| Theme | theme-section |
palette colors, theme mode, site layout mode |
| Header | header-section |
logo/search/categories/languages/cart/profile/wishlist/compare/region toggles |
| Footer | footer-section |
company info, address, phone, email, copyright, payment icons, social links, static pages list |
| Homepage | homepage-section |
homepage section list: visibility, order (drag-and-drop), layout strategy, columns |
| Widgets | widgets-section |
homepage widget configuration — typed editors for hero/categories/product-collection, JSON fallback for everything else |
| Static Pages | static-pages-editor (features/content-management/) |
Full CRUD, media, SEO, per-page draft/publish, device preview, nav integration — see docs/StaticPages.md (Sprint X+2) |
| Marketplace Features | features-section |
feature flags, catalog navigation mode, search suggestions/history, recently viewed, reviews/questions/recommendations |
| Languages | languages-section |
add/remove supported locale, set default locale; syncs translation keys across static pages and nav labels via LocaleSyncService |
| Navigation | navigation-section |
header nav: add/remove/reorder/edit label/URL/visibility, per-locale via app-locale-tabs. Flat footer nav: same. Grouped (column-based) footer nav is read-only here — edit via Footer tab. |
| Preview | preview-section |
export/import JSON, in-memory runtime preview without full reload |
Save / publish / draft / reset model
- Save:
save()snapshots the current in-memory bootstrap as "last saved" (lastSavedAt).ProjectEditorDraftStorageServicepersists the full draft tolocalStorage(projectEditor.draftBootstrap.v1, scoped bytenant.id) on everyupdateBootstrap(),save(), andpublish()call. - Publish: runs
ProjectValidator; if clean, callsPlatformRuntimeService.reloadFromBootstrap(), setsstatus = 'published', setslastPublishedAt, and becomes the neworiginalBootstrapbaseline used by reset. - Draft restore: on
loadBootstrap(), if a stored draft exists for the same tenant it loads instead of the fresh fetch, anddraftRestoredis set (shown as a dismissible banner in the save bar). - Reset section: reverts one section's bootstrap keys (per
EDITOR_SECTION_BOOTSTRAP_KEYSinmodels/project-editor.model.ts) tooriginalBootstrap. Confirmation required. - Reset draft: reverts the entire bootstrap to
originalBootstrapand clears the persisted local draft. Confirmation required. - 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.mditem 2 for the endpoints needed.
Configuration schema, form engine, and validation architecture (Sprint X+1)
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? }.keyis a dot path intoBootstrapConfig(e.g.theme.palette.primary), unique per section.validatorsreferences 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_SCHEMASflattens 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 login shares the exact same Telegram QR/session backend and TelegramLoginComponent as customer login (mode: 'admin' vs 'customer') — only the cookie name/SameSite policy, token storage keys, and guard differ. Backend gap: because both flows hit the same session endpoint, the backend cannot distinguish an admin scan from a customer scan today — real admin authorization must be enforced server-side. Full detail: docs/BACKEND.md item 1.
Design system primitives (post-Sprint 30 redesign)
All 11 section components (sections/*.component.html) share these 6 shared/ui/* primitives instead of copy-pasted markup. They mirror the existing InputComponent CVA idiom (standalone, OnPush, NG_VALUE_ACCESSOR where they're form controls):
| Primitive | Selector | Replaces | Used in |
|---|---|---|---|
ToggleComponent |
app-toggle |
raw <input type="checkbox"> + $any($event.target).checked |
header, homepage, widgets, navigation, features |
SelectComponent |
app-select |
raw <select> |
theme (theme.mode, layout.type) |
ColorPickerComponent |
app-color-picker |
raw <input type="color"> |
theme (8 palette colors) |
SectionCardComponent |
app-section-card |
the copy-pasted editor-section-card/<h2> shell |
all 11 sections |
LocaleTabsComponent |
app-locale-tabs |
(new capability) | languages, navigation |
KeyValueEditorComponent<T> |
app-key-value-editor |
pipe-delimited <textarea> lists |
footer (payment icons, social links) |
section.shared.scss's .editor-grid.* classes are unchanged and still used inside SectionCard bodies; only the outer .editor-section-card shell and per-section <h2> were replaced (that rule has been removed from the shared stylesheet since it has no remaining consumers).
Interaction / motion pass (2026-07-16)
Interaction feedback + motion applied consistently, all gated behind prefers-reduced-motion:
section.shared.scssbutton/button.secondarygained hover/active/focus-visible/disabledstates (previously flat, no feedback across all 11 sections).project-editor-page.component.scss: the active section fades/slides in (220ms) when the@switchswaps components; the "reset section" button got matching hover/focus states.project-editor-save-barbuttons now use the sharedapp-buttonprimitive (danger / secondary / primary variants) instead of unstyled native<button>s.
HTML editor (Static Pages)
MarketplaceHtmlEditorComponent (components/html-editor/marketplace-html-editor.component.ts) is a contenteditable WYSIWYG used inside the Static Pages editor (features/content-management/.../static-pages-editor.component.html), one instance per locale, bound [html] / (htmlChange).
Status: working. Verified live 2026-07-16 — typing captured, toolbar commands functional (bold toggles, insertUnorderedList wraps <ul><li>, H2/H3/link/image/table), htmlChange emits on every edit, and a "Код" toggle swaps to raw-HTML editing.
Toolbar (Sprint X+2 additions): horizontal rule, code block (<pre>), embed (prompt for a URL, inserts a sandboxed <iframe sandbox="allow-scripts allow-same-origin">) — alongside the original bold/italic/underline/H2/H3/lists/link/image/table set.
HTML-mode validation (Sprint X+2): switching from raw-HTML back to the visual surface now runs schema/validators/primitives.validateHtml (stack-based tag-balance check) first; a malformed edit (unclosed/mismatched tag) stays in code mode with an inline error instead of silently corrupting the visual editor.
Caveat: implemented on the deprecated document.execCommand API. It works in all current browsers today but is a legacy web API with no modern drop-in replacement; if a future browser drops it, this component needs a rewrite (e.g. a maintained rich-text library). By design it emits raw, unsanitized HTML — sanitization is a storefront-render concern, not an authoring one (see docs/BACKEND.md item 4 on server-side content moderation on publish; StaticPageComponent and StaticPagePreviewComponent both run content through DomSanitizer before render).
Field-description / dropdown UX (Sprint 19+)
Every field across the 10 editor section templates now carries a one-line, i18n'd description under its label explaining what it does in plain language (all new copy routed through TranslateService/TranslatePipe, added to Translations + en.ts/ru.ts/hy.ts following the existing builder.* key pattern — see src/app/i18n/translations.ts).
Converted from free-text <input> to <select> (backed by a closed TypeScript union), each option carrying a human label and a short description (via title attribute) instead of the raw enum value:
section.layout.strategy(Homepage section) —SectionLayoutStrategy:stack | grid | hero | carousel | split.theme.mode(Theme section) —light | dark | system.layout.type(Theme section, "Site Layout") —PlatformLayoutType:default | sidebar-left | carousel-home | minimal.catalog.navigationMode(Marketplace Features section) —CatalogNavigationModeConfig:default | left-category-navigation | mega-category-layout | top-category-carousel.
Each of these components defines a local readonly options array of { value, labelKey, descriptionKey } (per ADR-006, these are section/container components so this is allowed without a new shared UI library).
Still plain text/checkbox, with a description added, and why: marketplace name, domain, description, logo/favicon/small-logo URLs, palette colors (already <input type="color">, which is the correct native widget), company/address/phone/email, copyright, payment icons/social links (JSON-ish textarea), homepage section columns (a number, not an enum), widget-specific props (hero/categories/product-collection typed fields like layout/height/overlay/autoplay/cardsPerRow — these are widget props strings/booleans, not modeled as TypeScript unions anywhere, so they stay free text/checkbox with a description rather than a fabricated enum), navigation link label/URL, and the widget JSON fallback textarea for any widget type without a dedicated editor. These are genuinely open-ended or already have the correct native input type; converting them to <select> would either be wrong (URLs/colors/free text) or invent an enum that doesn't exist in the schema.