Files
marketplaces/docs/EDITOR.md
sdarbinyan d853ecb1da
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
changes
2026-07-19 15:28:35 +04:00

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/BACKEND-INTEGRATION.md#4-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/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, 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). ProjectEditorDraftStorageService persists the full draft to localStorage (projectEditor.draftBootstrap.v1, scoped by tenant.id) on every updateBootstrap(), save(), and publish() call.
  • Publish: runs ProjectValidator; if clean, calls PlatformRuntimeService.reloadFromBootstrap(), sets status = 'published', sets lastPublishedAt, and becomes the new originalBootstrap baseline used by reset.
  • Draft restore: on loadBootstrap(), if a stored draft exists for the same tenant it loads instead of the fresh fetch, and draftRestored is set (shown as a dismissible banner in the save bar).
  • Reset section: reverts one section's bootstrap keys (per EDITOR_SECTION_BOOTSTRAP_KEYS in models/project-editor.model.ts) to originalBootstrap. Confirmation required.
  • Reset draft: reverts the entire bootstrap to originalBootstrap and 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/BACKEND-INTEGRATION.md#67-builder--bootstrap-draftpublishvalidate-planned-highest-priority 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.tsFieldSchema: { 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.tsSECTION_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, 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/BACKEND-INTEGRATION.md#25-the-admin-authorization-gap-critical--security-relevant-unresolved.

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.scss button/button.secondary gained hover/active/focus-visible/disabled states (previously flat, no feedback across all 11 sections).
  • project-editor-page.component.scss: the active section fades/slides in (220ms) when the @switch swaps components; the "reset section" button got matching hover/focus states.
  • project-editor-save-bar buttons now use the shared app-button primitive (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/BACKEND-INTEGRATION.md#68-builder--content-pages--cms-planned 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, corrupting footer.component.html's @for (... track item.id) DOM identity on the public storefront. Payment-icon @for tracked by icon.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> AND userExperience.<key>.enabled — see feature-config.service.ts), but the editor only exposed a toggle for the first. Both default true so it was silent, but a config with the second explicitly false left 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 to propsJson(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 whether LocaleSyncService actually accepted the code — adding an already-supported locale silently no-opped. Now shows an inline error and leaves the input untouched.
  • Preview: importBootstrap() replaced state.bootstrap directly instead of routing through updateBootstrap() — so an import never got a draftStorage.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}) and duplicatePage()'s slug/route (fixed -copy suffix) both reproducibly collide the same way as the footer bug above (create/delete/create; duplicate the same page twice). Added a shared uniqueValue() helper (appends -2, -3, ... until free).
  • General: "Supported Languages" is a free-text comma list that bypassed LocaleSyncService entirely, 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 through facade.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 by SeoService.resetToDefaults() — the OG/Twitter image fallback stayed on appIconUrl || logoUrl. Fixed to check it first.
  • Media picker: MediaLibraryFacade is a root-provided singleton shared by every app-media-picker instance on a page (branding alone renders 4). ngOnInit loaded unconditionally on mount regardless of dialog state, and search/folder/page filters leaked between independently-opened picker dialogs. Replaced with an effect() that resets those filters and loads only when that instance's own open input actually becomes true.

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 a data-theme-mode attribute (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 + matchMedia for "system"), not a wiring fix.
  • layout.type (Site Layout) and the homepage section's type field both feed a rendering pipeline that was never wired up. src/app/dynamic-renderer/ has services/models for page/section/widget rendering but zero components or templates (every directory has only a .gitkeep) — the storefront homepage renders through a separate, older path that ignores both fields. homepage-section.component.ts's updateSection(id, 'type', ...) has no UI calling it because of this; not built, since building UI for a field nothing reads would be inventing dead controls.
  • HeaderConfig.showProfile is a real toggle in header-section with no corresponding profile/account menu anywhere in header.component.html — the toggle currently does nothing. Building the actual menu is a feature (needs an auth-system check first), not an editor-wiring fix.