feat(phase7.2): media browser with server-side filters + pagination

- Migration 052: indexes on media_assets(asset_type, created_at) and
  products(category_key, lagertyp) for efficient filter queries
- GET /api/media/assets: JOINs media_assets→products→order_lines,
  filters by asset_type / category_key / render_status / q (ILIKE),
  paginated (page/page_size), returns total+pages count
- New schemas: MediaAssetBrowseItem, MediaAssetBrowseResponse
- frontend/src/api/media.ts: getMediaAssets(filters), typed interfaces
- MediaBrowser.tsx: rewritten with sticky filter bar (debounced search,
  type/category/status dropdowns), responsive grid, image previews,
  download buttons, pagination footer with page size selector
- Renamed legacy function to listMediaAssets for backward compat

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 20:24:03 +01:00
parent 89c44b846f
commit c99976cc85
8 changed files with 482 additions and 502 deletions
+48 -1
View File
@@ -10,6 +10,53 @@ export type MediaAssetType =
| 'gltf_production'
| 'blend_production'
// ── Media Browser (server-side filtered + paginated) ──────────────────────────
export interface MediaAssetFilters {
asset_type?: string
category_key?: string
render_status?: string
q?: string
page?: number
page_size?: number
}
export interface MediaAssetItem {
id: string
asset_type: MediaAssetType
file_path: string
file_size_bytes: number | null
mime_type: string | null
created_at: string
order_line_id: string | null
product_id: string | null
product_name: string | null
product_pim_id: string | null
category_key: string | null
render_status: string | null
download_url: string | null
thumbnail_url: string | null
}
export interface MediaAssetListResponse {
items: MediaAssetItem[]
total: number
page: number
page_size: number
pages: number
}
export function getMediaAssets(filters: MediaAssetFilters = {}): Promise<MediaAssetListResponse> {
const params = new URLSearchParams()
if (filters.asset_type) params.set('asset_type', filters.asset_type)
if (filters.category_key) params.set('category_key', filters.category_key)
if (filters.render_status) params.set('render_status', filters.render_status)
if (filters.q) params.set('q', filters.q)
if (filters.page !== undefined) params.set('page', String(filters.page))
if (filters.page_size !== undefined) params.set('page_size', String(filters.page_size))
return api.get(`/media/assets?${params}`).then(r => r.data)
}
export interface MediaAsset {
id: string
tenant_id: string | null
@@ -42,7 +89,7 @@ export interface MediaFilter {
sort_dir?: 'asc' | 'desc'
}
export const getMediaAssets = (filters: MediaFilter = {}): Promise<MediaAsset[]> => {
export const listMediaAssets = (filters: MediaFilter = {}): Promise<MediaAsset[]> => {
const params = new URLSearchParams()
if (filters.product_id) params.set('product_id', filters.product_id)
if (filters.order_line_id) params.set('order_line_id', filters.order_line_id)