feat(media): reusable media management
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>
This commit is contained in:
@@ -208,6 +208,89 @@ src/app/features/admin/categories/
|
||||
- **Preview**: simple read-only line in the editor showing computed discounted price.
|
||||
- **Infinite scroll**: `AdminProductsFacade.infiniteScroll` toggle - when on, `loadMore()` appends the next page to `products()` instead of replacing it; pagination UI swaps for a "Load more" button. Off by default (existing paginated behavior unchanged).
|
||||
|
||||
## Sprint 22 - Media System hardening
|
||||
|
||||
`core/media/` (`MediaRepository` abstraction, `MockMediaRepository` IndexedDB
|
||||
implementation) + `features/backoffice/media/` + the shared
|
||||
`shared/media/media-picker/`:
|
||||
|
||||
- **Folders**: flat `folder?: string` tag on `MediaAsset` (no nesting) -
|
||||
"New folder" just sets the active filter to a name typed via
|
||||
`window.prompt` (mirrors the `window.confirm` pattern already used for
|
||||
destructive actions elsewhere); the folder is created implicitly the next
|
||||
time something uploads into it. `MediaRepository.listFolders()` derives
|
||||
the folder list from existing records rather than a separate folder
|
||||
entity - intentionally light, matches the flat-storage reality of an
|
||||
IndexedDB mock.
|
||||
- **Tags**: already existed on `MediaAsset`; added an edit affordance
|
||||
(`window.prompt`, comma-separated) and `MediaLibraryFacade.updateTags()`.
|
||||
- **Validation**: `MockMediaRepository.validateFile()` rejects anything over
|
||||
10MB or outside the allow-list (`jpeg/png/webp/gif/svg+xml/pdf`); errors
|
||||
now propagate as real messages through `MediaLibraryFacade.error` (both
|
||||
`media-library-page` and `media-picker` display it - previously upload
|
||||
failures were swallowed into a generic string).
|
||||
- **SVG sanitization**: `sanitizeSvg()` strips `<script>` tags and
|
||||
`on*="..."` attributes from uploaded SVG markup before storing it, since
|
||||
SVG is the one accepted format that can carry inline script.
|
||||
- **Compression/resize**: raster images (not SVG/GIF) are downscaled to a
|
||||
2000px max dimension and re-encoded (JPEG/PNG, quality 0.85) via
|
||||
`<canvas>` before being stored - client-side only, no crop UI. A full
|
||||
interactive cropper was out of scope for this ticket; revisit if a real
|
||||
design need for manual cropping shows up.
|
||||
- **Reuse confirmed**: `MediaPickerComponent` is now wired into Category
|
||||
images (Sprint 20), Product gallery (Sprint 21), and Project Editor
|
||||
branding (logo / compact logo / favicon, this sprint) - one media library
|
||||
for the whole platform, per the sprint goal. Static Pages editor has no
|
||||
image fields to wire (confirmed, not a gap). Hero image: no dedicated
|
||||
hero-image field exists in `BootstrapConfig` today - nothing to wire.
|
||||
- **Storage abstraction**: already existed via `MediaRepository` (abstract
|
||||
class + DI token `providedIn: 'root'` on `MockMediaRepository`) - swapping
|
||||
to a real CDN/backend means implementing `MediaRepository` against a real
|
||||
API and rebinding the provider; no consumer (`MediaLibraryFacade`,
|
||||
`MediaPickerComponent`, or any of the pickers above) changes.
|
||||
|
||||
## Sprint 22 - Media System hardening
|
||||
|
||||
`core/media/` (`MediaRepository` abstraction, `MockMediaRepository` IndexedDB
|
||||
implementation) + `features/backoffice/media/` + the shared
|
||||
`shared/media/media-picker/`:
|
||||
|
||||
- **Folders**: flat `folder?: string` tag on `MediaAsset` (no nesting) -
|
||||
"New folder" just sets the active filter to a name typed via
|
||||
`window.prompt` (mirrors the `window.confirm` pattern already used for
|
||||
destructive actions elsewhere); the folder is created implicitly the next
|
||||
time something uploads into it. `MediaRepository.listFolders()` derives
|
||||
the folder list from existing records rather than a separate folder
|
||||
entity - intentionally light, matches the flat-storage reality of an
|
||||
IndexedDB mock.
|
||||
- **Tags**: already existed on `MediaAsset`; added an edit affordance
|
||||
(`window.prompt`, comma-separated) and `MediaLibraryFacade.updateTags()`.
|
||||
- **Validation**: `MockMediaRepository.validateFile()` rejects anything over
|
||||
10MB or outside the allow-list (`jpeg/png/webp/gif/svg+xml/pdf`); errors
|
||||
now propagate as real messages through `MediaLibraryFacade.error` (both
|
||||
`media-library-page` and `media-picker` display it - previously upload
|
||||
failures were swallowed into a generic string).
|
||||
- **SVG sanitization**: `sanitizeSvg()` strips `<script>` tags and
|
||||
`on*="..."` attributes from uploaded SVG markup before storing it, since
|
||||
SVG is the one accepted format that can carry inline script.
|
||||
- **Compression/resize**: raster images (not SVG/GIF) are downscaled to a
|
||||
2000px max dimension and re-encoded (JPEG/PNG, quality 0.85) via
|
||||
`<canvas>` before being stored - client-side only, no crop UI. A full
|
||||
interactive cropper was out of scope for this ticket; revisit if a real
|
||||
design need for manual cropping shows up.
|
||||
- **Reuse confirmed**: `MediaPickerComponent` is now wired into Category
|
||||
images (Sprint 20), Product gallery (Sprint 21), and Project Editor
|
||||
branding (logo / compact logo / favicon, this sprint) - one media library
|
||||
for the whole platform, per the sprint goal. Static Pages editor has no
|
||||
image fields to wire (confirmed, not a gap). Hero image: no dedicated
|
||||
hero-image field exists in `BootstrapConfig` today ('hero' only appears
|
||||
as a `SectionLayoutStrategy` enum value) - nothing to wire.
|
||||
- **Storage abstraction**: already existed via `MediaRepository` (abstract
|
||||
class + DI token `providedIn: 'root'` on `MockMediaRepository`) - swapping
|
||||
to a real CDN/backend means implementing `MediaRepository` against a real
|
||||
API and rebinding the provider; no consumer (`MediaLibraryFacade`,
|
||||
`MediaPickerComponent`, or any of the pickers above) changes.
|
||||
|
||||
## Known gaps / backend needs
|
||||
|
||||
- **Dashboard metrics endpoint.** Categories/Products counts are computed
|
||||
|
||||
Reference in New Issue
Block a user