Sprint A: storefront header profile control (login/logout only, no menu), wired to existing customer Telegram auth (AuthService). Sprint B: backoffice/reports page, reuses AdminAnalyticsFacade (Sales, Top Products, Marketplace Health cards + CSV export). Sprint C: backoffice/settings page, admin UI density preference (comfortable/compact), localStorage-persisted, applied to app-table across all admin list pages. Sprint D: admin bottom-nav Help -> mailto using existing supportEmail, Documentation -> external link via new TenantConfig.documentationUrl. AdminNavLink gains externalHref for non-routerLink nav entries. Docs: docs/GLOBAL-SPRINT-PLAN.md tracks the full sprint breakdown. docs/COMING-SOON-AUDIT.md removed, folded into docs/KNOWN-ISSUES.md. docs/BACKEND.md updated with the new documentationUrl bootstrap field. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
24 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/BACKEND.md#1-bootstrap) 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/BACKEND.md §3 CRUD Contracts).
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, social share (OG) image, gallery, marketplace title — all image fields use app-image-field (thumbnail preview + replace/remove) |
| Theme | theme-section |
palette colors (live, applied as CSS custom properties), theme mode (not applied at runtime, see Known gaps), site layout mode |
| Header | header-section |
logo/search/categories/languages/cart/profile/wishlist/compare/region toggles, layout (default/centered), sticky |
| 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 (with draft-preserving inline error, not silent-discard) 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.md§1 (Bootstrap: Draft vs Published) and §8 (Real Backend Implementation Guide) 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, default locale not itself in the supported-locales list (error — catches General's free-text default-language field pointing at an unsupported code), 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, layout/section-layout values outside the known enums, invalid company contact email (error), invalid footer social-link URL (warning), and incomplete footer payment icon — only one of src/alt set (warning).
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 (or a standalone <p class="editor-error"> where the target isn't a single form-field, e.g. a whole list) for every field that has a matching ProjectValidator fieldKey today: theme palette, general name/domain/default-locale, branding logo, languages (localization.supportedLocales), homepage/widgets (pages), navigation (navigation.header), static-pages (staticPages), and footer (contact email, social links, payment icons). Header/features have no matching validator checks (every field there is a bool/enum, always valid by construction), so nothing is wired there — not an oversight. 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 §4 Authentication and §5 Security (permission matrix).
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) |
ImageFieldComponent |
app-image-field |
manual URL <input> + separate "choose image" button, no preview |
branding (logo/small-logo/favicon/social/gallery), footer (logo, payment icons) — thumbnail preview + Replace/Remove, opens its own app-media-picker |
CodeEditorComponent |
app-code-editor |
plain <textarea> for raw-HTML mode |
MarketplaceHtmlEditorComponent's "Код" toggle — overlay-textarea syntax highlighting (no Monaco/CodeMirror dependency); tokenizes HTML tags/comments and delegates <style> block contents to a CSS tokenizer (selector/property/value/string/comment-aware) |
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 §3 CRUD Contracts, CMS, 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.
Bug-hunt audit pass (2026-07-17)
A section-by-section correctness audit (not a feature pass) — for each section, checked whether its controls actually do what they claim at runtime, not just whether they render. 9 real, verified defects found and fixed (each confirmed live via window.ng.getComponent() reproducing the exact bug, then re-verified fixed):
- Footer:
createSocialLinkRow's id was derived from array length (social-${length+1}) — add/remove/add reliably collides with a surviving row's id, corruptingfooter.component.html's@for (... track item.id)DOM identity on the public storefront. Payment-icon@fortracked byicon.src, which collides whenever two rows share a src (most commonly two blank ones). Both switched to safe keys. - Features: wishlist/compare visibility is gated by two flags at runtime (
featureFlags.<key>ANDuserExperience.<key>.enabled— seefeature-config.service.ts), but the editor only exposed a toggle for the first. Both defaulttrueso it was silent, but a config with the second explicitlyfalseleft the toggle looking "on" with no way to fix it from this screen. Now one toggle drives both. - Widgets: the JSON-fallback textarea's
updateJson()caught parse errors and did nothing, but the textarea was bound topropsJson(committed props)— so an in-progress invalid edit got silently overwritten on the next change-detection pass. Now keeps the user's draft on screen with an inline error until it's valid. - Languages:
addLocale()cleared the input regardless of whetherLocaleSyncServiceactually accepted the code — adding an already-supported locale silently no-opped. Now shows an inline error and leaves the input untouched. - Preview:
importBootstrap()replacedstate.bootstrapdirectly instead of routing throughupdateBootstrap()— so an import never got adraftStorage.save()(lost on refresh before an explicit Save) and was never an undo-able history step. Now routed through the same pipeline as every other edit. - Static Pages:
createPage()'s slug (custom-page-${length+1}) andduplicatePage()'s slug/route (fixed-copysuffix) both reproducibly collide the same way as the footer bug above (create/delete/create; duplicate the same page twice). Added a shareduniqueValue()helper (appends-2,-3, ... until free). - General: "Supported Languages" is a free-text comma list that bypassed
LocaleSyncServiceentirely, so adding a locale here never seeded the empty translation entries Languages' add-button produces — the two UI paths silently diverged. Now diffs and routes throughfacade.addLocale()/removeLocale(). Also added the "default locale not itself supported" validator check described above, since this field had (and still has, by design — it's free text) no format guard. - Branding → SEO:
branding.socialImageUrl(added earlier this same pass) wasn't actually read bySeoService.resetToDefaults()— the OG/Twitter image fallback stayed onappIconUrl || logoUrl. Fixed to check it first. - Media picker:
MediaLibraryFacadeis a root-provided singleton shared by everyapp-media-pickerinstance on a page (branding alone renders 4).ngOnInitloaded unconditionally on mount regardless of dialog state, andsearch/folder/pagefilters leaked between independently-opened picker dialogs. Replaced with aneffect()that resets those filters and loads only when that instance's ownopeninput actually becomestrue.
Known gaps found but not fixed (real, out of scope for this pass)
- Theme Mode has no runtime effect.
theme-section's light/dark/system selector correctly saves and sets adata-theme-modeattribute (theme-engine.service.ts), but zero CSS anywhere in the app reads that attribute — picking Dark or System currently changes nothing visually. (Theme palette colors are live — real CSS custom properties consumed throughout the stylesheets — only the mode switch is dead.) Fixing this is a real dark-mode implementation project (dark palette + CSS strategy +matchMediafor "system"), not a wiring fix. Tracked:docs/PRODUCT_BACKLOG.md. — fixed:HeaderConfig.showProfilehas no corresponding profile/account menuheader.component.html/.tsnow render a login/logout-only control (no dropdown, no account links) gated by this toggle, reusing the customer TelegramAuthService. Seedocs/KNOWN-ISSUES.md"Fixed (this cycle)" anddocs/GLOBAL-SPRINT-PLAN.mdSprint A.— stale, corrected:layout.type/homepagetypefield feed an unwireddynamic-renderer/dynamic-renderer/(PageRendererService/SectionRendererService/WidgetHostService) is the live homepage rendering pipeline, wired throughdynamic-page-layout.component.ts. Verified fixed/non-issue indocs/KNOWN-ISSUES.md"Fixed (this cycle)".