feat(E): add MediaAsset catalog — model, CRUD API, MediaBrowser UI
Migration 040: media_assets table with RLS (tenant_isolation + admin_bypass). domains/media/: MediaAsset model, schemas, service, router with ZIP-download. publish_asset Celery task in rendering/tasks.py. core/storage.py: download_bytes() method for MinIO + LocalStorage. frontend: MediaBrowser.tsx (grid/list, multi-select, zip-download, pagination) + api/media.ts. Route /media (AdminRoute) + sidebar link with Image icon for admin+pm. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import api from './client'
|
||||
|
||||
export type MediaAssetType =
|
||||
| 'thumbnail'
|
||||
| 'still'
|
||||
| 'turntable'
|
||||
| 'stl_low'
|
||||
| 'stl_high'
|
||||
| 'gltf_geometry'
|
||||
| 'gltf_production'
|
||||
| 'blend_production'
|
||||
|
||||
export interface MediaAsset {
|
||||
id: string
|
||||
tenant_id: string | null
|
||||
product_id: string | null
|
||||
cad_file_id: string | null
|
||||
order_line_id: string | null
|
||||
workflow_run_id: string | null
|
||||
asset_type: MediaAssetType
|
||||
storage_key: string
|
||||
file_size_bytes: number | null
|
||||
mime_type: string | null
|
||||
width: number | null
|
||||
height: number | null
|
||||
duration_s: number | null
|
||||
render_config: Record<string, unknown> | null
|
||||
is_archived: boolean
|
||||
created_at: string
|
||||
download_url: string | null
|
||||
}
|
||||
|
||||
export interface MediaFilter {
|
||||
product_id?: string
|
||||
order_line_id?: string
|
||||
asset_type?: MediaAssetType
|
||||
skip?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export const getMediaAssets = (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)
|
||||
if (filters.asset_type) params.set('asset_type', filters.asset_type)
|
||||
if (filters.skip !== undefined) params.set('skip', String(filters.skip))
|
||||
if (filters.limit !== undefined) params.set('limit', String(filters.limit))
|
||||
return api.get(`/media?${params}`).then(r => r.data)
|
||||
}
|
||||
|
||||
export const getMediaAsset = (id: string): Promise<MediaAsset> =>
|
||||
api.get(`/media/${id}`).then(r => r.data)
|
||||
|
||||
export const downloadMediaAsset = (id: string): void => {
|
||||
window.open(`/api/media/${id}/download`, '_blank')
|
||||
}
|
||||
|
||||
export const zipDownloadAssets = (ids: string[]): Promise<void> =>
|
||||
api.post('/media/zip', ids, { responseType: 'blob' }).then(r => {
|
||||
const url = URL.createObjectURL(r.data)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = 'media-export.zip'
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
})
|
||||
|
||||
export const archiveMediaAsset = (id: string): Promise<void> =>
|
||||
api.delete(`/media/${id}`).then(() => undefined)
|
||||
Reference in New Issue
Block a user