Sprint 22.
- MediaAsset gains folder (flat) and MediaListParams gains folder/tag
filters; MediaRepository.listFolders() derives the folder list from
existing records
- upload validation: 10MB size cap, mime allow-list (jpeg/png/webp/gif/
svg+xml/pdf), real error messages surfaced through MediaLibraryFacade
instead of a generic swallowed string
- SVG uploads are sanitized (script tags and on*= attributes stripped)
before storage
- raster images (excl. gif) are downscaled to a 2000px max dimension and
re-encoded via canvas before storage - compression, not a crop UI
- tag editing (window.prompt, comma-separated) via
MediaLibraryFacade.updateTags()
- MediaPickerComponent wired into Project Editor branding (logo, compact
logo, favicon) alongside its existing category/product usage - confirmed
no image fields exist on Static Pages or as a dedicated hero field to
wire
docs/ADMIN.md updated with the new Sprint 22 section including the storage
abstraction note (MediaRepository was already the abstraction).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Applied app-input/app-form-field/app-button to 9 of 11 Project Editor
sections: general, branding, theme, footer, homepage, widgets, languages,
navigation, preview. header and features sections were left unchanged -
they contain only checkboxes and selects, and no Checkbox/Select primitive
exists yet.
Theme section's 8 color pickers stay native <input type=color> (app-input's
type union doesn't include 'color') but are now wrapped in app-form-field
for consistent label/hint treatment. Added app-form-field.full grid-column
rule to section.shared.scss (shared by all 11 sections) alongside the
existing label.full rule, since the custom element doesn't match that
selector.
Production build green, arch:check passes.
Wires up the remaining editor sections (theme, header, footer, homepage,
widgets, features, languages, navigation) with the description text and
dropdown UX started for General/Branding:
- Every enum-backed field is now a <select> with a description per option,
not free text: theme.mode, the new site-wide layout.type (previously had
no editor at all - added to the Theme section, and added to that
section's reset-scope), homepage section layout.strategy, and
catalog.navigationMode (also previously unedited, added to Features).
- Every other field (colors, header toggles, footer contact/company fields,
widget props, feature flags, language add, nav link label/url/visible)
gets a one-line plain-language description under its label via the new
*Desc i18n keys (en/ru/hy) prepared earlier.
- Genuinely open text (widget layout variant strings, JSON props) stays
free text, description-only, per the existing widgetLayoutDesc/widgetJsonDesc
wording - not force-fit into a dropdown.
Verified: tsc --noEmit and ng build both clean.
Adds a short plain-language description under each field label (new
.field-desc style, section.shared.scss) so a non-developer admin
understands what each field affects, per i18n (en/ru/hy) convention.
Only General and Branding sections are wired up so far - the *Desc i18n
keys for the remaining sections (theme/header/footer/homepage/widgets/
features/languages/navigation) were prepared in translations.ts/en/ru/hy
but not yet wired into their templates. Follow-up work.
- Project editor: persist draft to localStorage, restore on reload,
last-saved/draft-restored status indicators, section/whole-draft reset
with confirmation.
- Extract shared QR/polling/expiry engine from TelegramLoginComponent
(shared/qr-login) and reuse it for a new admin login flow.
- Admin authentication kept fully separate from customer session:
own cookie/localStorage keys, signals, guard, and header interceptor
(core/admin-auth).
- ?login=true / ?adminLogin=true open the respective login dialog for
manual testing.
- Ed25519 challenge/verify interfaces (fail-closed no-op binding) ready
for backend delivery.
- Document autosave/reset/admin-auth/QR-reuse/Ed25519 model and the
remaining full-field-coverage gap in docs/Project-Editor.md.
window.confirm/window.prompt calls in projectEditorDirtyGuard and
MarketplaceHtmlEditorComponent, and the hardcoded Preview/Code toggle
label in its template, bypassed the app's translation pipeline. Add
builder.confirmLeaveUnsaved, promptLinkUrl, promptImageUrl,
htmlEditorCode and htmlEditorPreview keys (en/ru/hy), resolve them via
TranslateService.t() before passing to confirm/prompt, and use
TranslatePipe for the toggle button label.
updateLabel() previously overwrote NavigationItemConfig.label with a bare
string via updateNavLink, destroying every other locale's translation
whenever a localized label object was edited. Add a facade method
updateNavLinkLabel() that inspects the existing label shape: plain
strings are replaced as before, but localized objects only have the
current default locale's key overwritten, leaving other locales intact.
Mounts a new ProjectEditorSaveBarComponent in the editor page that shows
draft/published status, unsaved-changes indicator, and validation issues,
wiring the Task 8 facade save()/publish()/dirty/status/validationIssues
signals to an actual UI for the first time.
Mock bootstrap seed data populates page.route but never page.slug, so
duplicateSlugIssues always collapsed every static page to the same
undefined key and reported a false-positive duplicate-slugs issue,
permanently blocking Publish. Fall back to route (leading slash
stripped) when slug is missing or empty.
Wires ProjectValidator into ProjectEditorFacade and extends ProjectEditorState
with status ('draft'|'published') and lastSavedBootstrap. Adds dirty computed
(diffed against lastSavedBootstrap), validationIssues computed, and save()/publish()
methods. publish() calls PlatformRuntimeService.reloadFromBootstrap() and refuses
when validation issues exist. loadBootstrap() seeds lastSavedBootstrap so a
freshly-loaded bootstrap is not dirty.
The @if/@else toggle between the contentEditable surface and the code
textarea broke the static ViewChild('surface') query: Angular never
resolves a static query for an element inside a conditional block, so
surface stayed undefined and every keystroke threw in emitChange().
Render both elements always and toggle visibility with [hidden]
instead, and add an ngAfterViewInit sync as a safety net for the
initial html input on the first change-detection pass.