import { useState, useRef } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Pencil, Trash2, Plus, Check, X, Upload, Download } from 'lucide-react' import HelpTooltip from '../HelpTooltip' import { toast } from 'sonner' import { listRenderTemplates, createRenderTemplate, updateRenderTemplate, deleteRenderTemplate, reuploadBlendFile, } from '../../api/renderTemplates' import type { RenderTemplate } from '../../api/renderTemplates' import { listOutputTypes } from '../../api/outputTypes' import type { OutputType } from '../../api/outputTypes' const ALL_CATEGORIES = [ { key: 'TRB', label: 'TRB' }, { key: 'Kugellager', label: 'Kugellager' }, { key: 'CRB', label: 'CRB' }, { key: 'Gleitlager', label: 'Gleitlager' }, { key: 'SRB_TORB', label: 'SRB/TORB' }, { key: 'Linear_schiene', label: 'Linear' }, { key: 'Anschlagplatten', label: 'Anschlag' }, ] const EMPTY_FORM = { name: '', category_key: '' as string, output_type_id: '' as string, target_collection: 'Product', material_replace_enabled: false, lighting_only: false, shadow_catcher_enabled: false, camera_orbit: true, } export default function RenderTemplateTable() { const qc = useQueryClient() const [showAdd, setShowAdd] = useState(false) const [form, setForm] = useState(EMPTY_FORM) const [addFile, setAddFile] = useState(null) const [editingId, setEditingId] = useState(null) const [editDraft, setEditDraft] = useState>({}) const fileInputRef = useRef(null) const reuploadRef = useRef(null) const [reuploadId, setReuploadId] = useState(null) const { data: templates, isLoading } = useQuery({ queryKey: ['render-templates'], queryFn: listRenderTemplates, }) const { data: outputTypes } = useQuery({ queryKey: ['output-types-admin'], queryFn: () => listOutputTypes(true), }) const createMut = useMutation({ mutationFn: () => { if (!addFile) throw new Error('Please select a .blend file') const fd = new FormData() fd.append('name', form.name.trim()) fd.append('file', addFile) fd.append('category_key', form.category_key || '') fd.append('output_type_id', form.output_type_id || '') fd.append('target_collection', form.target_collection || 'Product') fd.append('material_replace_enabled', String(form.material_replace_enabled)) fd.append('lighting_only', String(form.lighting_only)) fd.append('shadow_catcher_enabled', String(form.shadow_catcher_enabled)) fd.append('camera_orbit', String(form.camera_orbit)) return createRenderTemplate(fd) }, onSuccess: () => { toast.success('Render template created') qc.invalidateQueries({ queryKey: ['render-templates'] }) setForm(EMPTY_FORM) setAddFile(null) setShowAdd(false) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to create template'), }) const updateMut = useMutation({ mutationFn: ({ id, data }: { id: string; data: Record }) => updateRenderTemplate(id, data as any), onSuccess: () => { toast.success('Template updated') qc.invalidateQueries({ queryKey: ['render-templates'] }) setEditingId(null) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to update'), }) const deleteMut = useMutation({ mutationFn: deleteRenderTemplate, onSuccess: () => { toast.success('Template deleted') qc.invalidateQueries({ queryKey: ['render-templates'] }) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to delete'), }) const reuploadMut = useMutation({ mutationFn: ({ id, file }: { id: string; file: File }) => reuploadBlendFile(id, file), onSuccess: () => { toast.success('.blend file updated') qc.invalidateQueries({ queryKey: ['render-templates'] }) setReuploadId(null) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to upload'), }) function startEdit(t: RenderTemplate) { setEditingId(t.id) setEditDraft({ name: t.name, category_key: t.category_key, output_type_ids: t.output_type_ids ?? [], target_collection: t.target_collection, material_replace_enabled: t.material_replace_enabled, lighting_only: t.lighting_only, shadow_catcher_enabled: t.shadow_catcher_enabled, camera_orbit: t.camera_orbit, is_active: t.is_active, }) } function saveEdit() { if (!editingId) return updateMut.mutate({ id: editingId, data: editDraft as Record }) } const inputCls = 'px-2 py-1 text-sm border border-border-default rounded bg-surface focus:outline-none focus:ring-1 focus:ring-blue-400' return (

Render Templates

{/* Hidden file inputs */} { const file = e.target.files?.[0] if (file && reuploadId) reuploadMut.mutate({ id: reuploadId, file }) e.target.value = '' }} />
{/* Add row */} {showAdd && ( )} {/* Template rows */} {isLoading && ( )} {templates?.map((t) => { const isEditing = editingId === t.id return ( ) })} {!isLoading && (!templates || templates.length === 0) && !showAdd && ( )}
Name Category Output Type Collection Mat. Replace Lighting Only Shadow Catcher Cam Orbit .blend File Active Actions
setForm({ ...form, name: e.target.value })} /> setForm({ ...form, target_collection: e.target.value })} /> setForm({ ...form, material_replace_enabled: e.target.checked })} /> setForm({ ...form, lighting_only: e.target.checked })} /> setForm({ ...form, shadow_catcher_enabled: e.target.checked })} /> setForm({ ...form, camera_orbit: e.target.checked })} />
Loading...
{isEditing ? ( setEditDraft({ ...editDraft, name: e.target.value })} /> ) : ( {t.name} )} {isEditing ? ( ) : ( t.category_key || Any )} {isEditing ? (
{outputTypes?.map((ot: OutputType) => { const checked = (editDraft.output_type_ids ?? []).includes(ot.id) return ( ) })}
) : ( t.output_type_names && t.output_type_names.length > 0 ? (
{t.output_type_names.map((name, i) => ( {name} ))}
) : ( Any ) )}
{isEditing ? ( setEditDraft({ ...editDraft, target_collection: e.target.value })} /> ) : ( {t.target_collection} )} {isEditing ? ( setEditDraft({ ...editDraft, material_replace_enabled: e.target.checked })} /> ) : ( t.material_replace_enabled ? ( Yes ) : ( No ) )} {isEditing ? ( setEditDraft({ ...editDraft, lighting_only: e.target.checked })} /> ) : ( t.lighting_only ? ( HDR ) : ( ) )} {isEditing ? ( setEditDraft({ ...editDraft, shadow_catcher_enabled: e.target.checked })} /> ) : ( t.shadow_catcher_enabled ? ( On ) : ( ) )} {isEditing ? ( setEditDraft({ ...editDraft, camera_orbit: e.target.checked })} /> ) : ( t.camera_orbit ? ( Cam ) : ( Obj ) )}
{t.original_filename}
{isEditing ? ( setEditDraft({ ...editDraft, is_active: e.target.checked })} /> ) : ( t.is_active ? ( ) : ( ) )} {isEditing ? (
) : (
)}
No render templates configured. Click "Add Template" to create one.

Templates define pre-designed .blend studio setups. When rendering, the system matches templates by Category + Output Type with fallback cascade.

) }