feat: per-position camera settings, material alias dialog, product delete, media browser links
- Per-render-position focal_length_mm/sensor_width_mm (DB → pipeline → Blender)
- FOV-based camera distance with min clamp fix for wide-angle lenses
- Unmapped materials blocking dialog on "Dispatch Renders" with batch alias creation
- Material check endpoint (GET /orders/{id}/check-materials)
- Batch alias endpoint (POST /materials/batch-aliases)
- Quick-map "No alias" badges on Materials page
- Full product hard-delete with storage cleanup (MinIO + disk files + orphaned CadFile)
- Delete button on ProductDetail page with confirmation
- Clickable product names in Media Browser (links to product page)
- Single-line render dispatch/retry (POST /orders/{id}/lines/{id}/dispatch-render)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -87,3 +87,37 @@ export async function seedAliases(): Promise<{ inserted: number; total: number }
|
||||
const res = await api.post<{ inserted: number; total: number }>('/materials/seed-aliases')
|
||||
return res.data
|
||||
}
|
||||
|
||||
// --- Material check / batch alias ---
|
||||
|
||||
export interface MaterialSuggestion {
|
||||
id: string
|
||||
name: string
|
||||
schaeffler_code: string
|
||||
}
|
||||
|
||||
export interface UnmappedMaterial {
|
||||
raw_name: string
|
||||
suggestions: MaterialSuggestion[]
|
||||
}
|
||||
|
||||
export interface UnmappedMaterialCheck {
|
||||
unmapped: UnmappedMaterial[]
|
||||
total_materials: number
|
||||
mapped_count: number
|
||||
}
|
||||
|
||||
export async function checkOrderMaterials(orderId: string): Promise<UnmappedMaterialCheck> {
|
||||
const res = await api.get<UnmappedMaterialCheck>(`/orders/${orderId}/check-materials`)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function batchCreateAliases(
|
||||
mappings: Array<{ alias: string; material_id: string }>
|
||||
): Promise<{ created: number; skipped: number }> {
|
||||
const res = await api.post<{ created: number; skipped: number }>(
|
||||
'/materials/batch-aliases',
|
||||
{ mappings }
|
||||
)
|
||||
return res.data
|
||||
}
|
||||
|
||||
@@ -133,3 +133,6 @@ export const archiveMediaAsset = (id: string): Promise<void> =>
|
||||
|
||||
export const deleteMediaAssetPermanent = (id: string): Promise<void> =>
|
||||
api.delete(`/media/${id}/permanent`).then(() => undefined)
|
||||
|
||||
export const batchDeleteAssets = (ids: string[]): Promise<{ deleted: number; requested: number }> =>
|
||||
api.post('/media/batch-delete', ids).then(r => r.data)
|
||||
|
||||
@@ -235,6 +235,13 @@ export async function cancelLineRender(orderId: string, lineId: string) {
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function dispatchLineRender(orderId: string, lineId: string) {
|
||||
const res = await api.post<{ dispatched: boolean; line_id: string }>(
|
||||
`/orders/${orderId}/lines/${lineId}/dispatch-render`
|
||||
)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function cancelOrderRenders(orderId: string) {
|
||||
const res = await api.post<{ cancelled: number; order_status: string; errors: string[] | null }>(
|
||||
`/orders/${orderId}/cancel-renders`
|
||||
|
||||
@@ -8,6 +8,8 @@ export interface GlobalRenderPosition {
|
||||
rotation_z: number
|
||||
is_default: boolean
|
||||
sort_order: number
|
||||
focal_length_mm: number | null
|
||||
sensor_width_mm: number | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
@@ -19,6 +21,8 @@ export interface GlobalRenderPositionCreate {
|
||||
rotation_z?: number
|
||||
is_default?: boolean
|
||||
sort_order?: number
|
||||
focal_length_mm?: number | null
|
||||
sensor_width_mm?: number | null
|
||||
}
|
||||
|
||||
export interface GlobalRenderPositionPatch {
|
||||
@@ -28,6 +32,8 @@ export interface GlobalRenderPositionPatch {
|
||||
rotation_z?: number
|
||||
is_default?: boolean
|
||||
sort_order?: number
|
||||
focal_length_mm?: number | null
|
||||
sensor_width_mm?: number | null
|
||||
}
|
||||
|
||||
export async function listGlobalRenderPositions(): Promise<GlobalRenderPosition[]> {
|
||||
|
||||
@@ -39,6 +39,26 @@ export async function createRenderTemplate(formData: FormData): Promise<RenderTe
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function duplicateRenderTemplate(
|
||||
sourceId: string,
|
||||
overrides: Partial<Pick<RenderTemplate, 'name' | 'category_key' | 'target_collection' | 'material_replace_enabled' | 'lighting_only' | 'shadow_catcher_enabled' | 'camera_orbit'>> & { output_type_ids?: string[] },
|
||||
): Promise<RenderTemplate> {
|
||||
const fd = new FormData();
|
||||
fd.append('name', overrides.name || 'Untitled (copy)');
|
||||
fd.append('clone_blend_from', sourceId);
|
||||
fd.append('category_key', overrides.category_key || '');
|
||||
fd.append('output_type_ids', (overrides.output_type_ids || []).join(','));
|
||||
fd.append('target_collection', overrides.target_collection || 'Product');
|
||||
fd.append('material_replace_enabled', String(overrides.material_replace_enabled ?? false));
|
||||
fd.append('lighting_only', String(overrides.lighting_only ?? false));
|
||||
fd.append('shadow_catcher_enabled', String(overrides.shadow_catcher_enabled ?? false));
|
||||
fd.append('camera_orbit', String(overrides.camera_orbit ?? true));
|
||||
const { data } = await api.post<RenderTemplate>('/render-templates', fd, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function updateRenderTemplate(
|
||||
id: string,
|
||||
updates: Partial<Pick<RenderTemplate, 'name' | 'category_key' | 'output_type_ids' | 'target_collection' | 'material_replace_enabled' | 'lighting_only' | 'shadow_catcher_enabled' | 'camera_orbit' | 'is_active'>>,
|
||||
|
||||
@@ -146,7 +146,7 @@ export interface CeleryWorkersResponse {
|
||||
}
|
||||
|
||||
export interface ScaleRequest {
|
||||
service: 'render-worker' | 'worker' | 'worker-thumbnail'
|
||||
service: 'render-worker' | 'worker'
|
||||
count: number
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user