feat(media): add Media Manager UI (grid, upload, delete)
Some checks failed
Architecture Governance / architecture (push) Has been cancelled

MediaLibraryPageComponent replaces the coming-soon placeholder at
/backoffice/media. Built entirely on Sprint 6 Design System primitives
(app-card, app-button, app-input, app-empty-state, app-dialog, app-pagination,
app-skeleton) and Sprint 4 Task 2's MediaRepository/MockMediaRepository.

- Grid view with per-tile filename/size and delete action
- Hidden native file input triggered by an app-button, uploads via
  MediaLibraryFacade -> MediaRepository.upload()
- Delete requires confirmation through app-dialog (destructive action)
- Search + pagination wired to MockMediaRepository's list() params
- Loading state shows app-skeleton tiles; empty state shows app-empty-state
- New mediaLibrary.* translation namespace across en/ru/hy

Verified in browser (via a temporary unguarded route, reverted before
commit - /backoffice/media itself requires Telegram QR admin auth not
available in this session): empty state renders correctly with translated
copy, search input and upload button present, no console errors.
This commit is contained in:
sdarbinyan
2026-07-15 07:14:16 +04:00
parent c663c9099c
commit 5ffb353011
9 changed files with 358 additions and 1 deletions

View File

@@ -0,0 +1,71 @@
import { Injectable, inject, signal } from '@angular/core';
import { MediaRepository } from '../../../../core/media/media-repository';
import { MediaAsset } from '../../../../core/media/models/media-asset.model';
const PAGE_SIZE = 24;
@Injectable({ providedIn: 'root' })
export class MediaLibraryFacade {
private readonly repository = inject(MediaRepository);
readonly items = signal<MediaAsset[]>([]);
readonly total = signal(0);
readonly loading = signal(false);
readonly uploading = signal(false);
readonly error = signal<string | null>(null);
readonly search = signal('');
readonly page = signal(1);
async load(): Promise<void> {
this.loading.set(true);
this.error.set(null);
try {
const result = await this.repository.list({
page: this.page(),
pageSize: PAGE_SIZE,
search: this.search(),
});
this.items.set(result.items);
this.total.set(result.total);
} catch {
this.error.set('Failed to load media assets.');
} finally {
this.loading.set(false);
}
}
setSearch(value: string): void {
this.search.set(value);
this.page.set(1);
void this.load();
}
setPage(page: number): void {
this.page.set(page);
void this.load();
}
async upload(file: File): Promise<void> {
this.uploading.set(true);
this.error.set(null);
try {
await this.repository.upload(file);
this.page.set(1);
await this.load();
} catch {
this.error.set('Failed to upload file.');
} finally {
this.uploading.set(false);
}
}
async remove(id: string): Promise<void> {
this.error.set(null);
try {
await this.repository.remove(id);
await this.load();
} catch {
this.error.set('Failed to delete file.');
}
}
}