import React, { useState, useRef } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Pencil, Trash2, Plus, Check, X, Upload, Download, Copy } from 'lucide-react' import HelpTooltip from '../HelpTooltip' import { toast } from 'sonner' import { listRenderTemplates, createRenderTemplate, duplicateRenderTemplate, 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, workflow_input_schema_text: '[]', } function stringifyWorkflowInputSchema(value: unknown): string { try { return JSON.stringify(Array.isArray(value) ? value : [], null, 2) } catch { return '[]' } } function parseWorkflowInputSchemaText(rawValue: unknown): unknown[] { const text = typeof rawValue === 'string' ? rawValue.trim() : '' if (!text) return [] let parsed: unknown try { parsed = JSON.parse(text) } catch { throw new Error('Workflow input schema must be valid JSON') } if (!Array.isArray(parsed)) { throw new Error('Workflow input schema must be a JSON array') } return parsed } export default function RenderTemplateTable() { const qc = useQueryClient() const [showAdd, setShowAdd] = useState(false) const [form, setForm] = useState(EMPTY_FORM) const [addFile, setAddFile] = useState(null) const [cloneBlendFrom, setCloneBlendFrom] = useState('') const [editingId, setEditingId] = useState(null) const [editDraft, setEditDraft] = useState<(Partial & { workflow_input_schema_text?: string })>({}) 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 && !cloneBlendFrom) throw new Error('Please select a .blend file or choose an existing one') const fd = new FormData() fd.append('name', form.name.trim()) if (addFile) { fd.append('file', addFile) } else if (cloneBlendFrom) { fd.append('clone_blend_from', cloneBlendFrom) } 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)) fd.append('workflow_input_schema', JSON.stringify(parseWorkflowInputSchemaText(form.workflow_input_schema_text))) return createRenderTemplate(fd) }, onSuccess: () => { toast.success('Render template created') qc.invalidateQueries({ queryKey: ['render-templates'] }) setForm(EMPTY_FORM) setAddFile(null) setCloneBlendFrom('') setShowAdd(false) }, onError: (e: any) => toast.error(e.response?.data?.detail || e.message || '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 || e.message || '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'), }) const duplicateMut = useMutation({ mutationFn: (t: RenderTemplate) => duplicateRenderTemplate(t.id, { name: `${t.name} (copy)`, category_key: t.category_key, 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, output_type_ids: t.output_type_ids ?? [], workflow_input_schema: t.workflow_input_schema ?? [], }), onSuccess: () => { toast.success('Template duplicated') qc.invalidateQueries({ queryKey: ['render-templates'] }) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to duplicate'), }) 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, workflow_input_schema_text: stringifyWorkflowInputSchema(t.workflow_input_schema), is_active: t.is_active, }) } function saveEdit() { if (!editingId) return const data: Record = { ...editDraft } if (Object.prototype.hasOwnProperty.call(editDraft, 'workflow_input_schema_text')) { data.workflow_input_schema = parseWorkflowInputSchemaText(editDraft.workflow_input_schema_text) delete data.workflow_input_schema_text } updateMut.mutate({ id: editingId, data }) } // Render the edit form grid (shared between edit-row and add-row) function renderEditFormGrid( mode: 'edit' | 'add', t: RenderTemplate | null, ) { const isEdit = mode === 'edit' && t !== null // Value getters const val = (field: keyof typeof EMPTY_FORM | 'is_active' | 'output_type_ids') => { if (isEdit) { if (field === 'name') return editDraft.name ?? t!.name if (field === 'category_key') return editDraft.category_key ?? t!.category_key ?? '' if (field === 'output_type_ids') return editDraft.output_type_ids ?? t!.output_type_ids ?? [] if (field === 'target_collection') return editDraft.target_collection ?? t!.target_collection if (field === 'material_replace_enabled') return editDraft.material_replace_enabled ?? t!.material_replace_enabled if (field === 'lighting_only') return editDraft.lighting_only ?? t!.lighting_only if (field === 'shadow_catcher_enabled') return editDraft.shadow_catcher_enabled ?? t!.shadow_catcher_enabled if (field === 'camera_orbit') return editDraft.camera_orbit ?? t!.camera_orbit if (field === 'workflow_input_schema_text') { return editDraft.workflow_input_schema_text ?? stringifyWorkflowInputSchema(t!.workflow_input_schema) } if (field === 'is_active') return editDraft.is_active ?? t!.is_active return (editDraft as any)[field] ?? (t as any)[field] } return (form as any)[field] } const set = (field: string, value: any) => { if (isEdit) { setEditDraft({ ...editDraft, [field]: value } as any) } else { setForm({ ...form, [field]: value }) } } return ( <> {/* Row 1: Name | Category | Output Types */}
set('name', e.target.value)} />
{isEdit ? (
{outputTypes?.map((ot: OutputType) => { const ids = val('output_type_ids') as string[] const checked = ids.includes(ot.id) return ( ) })}
) : ( )}
{/* Row 2: Collection | Checkboxes */}
set('target_collection', e.target.value)} />
{/* Row 3: .blend File */}
{isEdit ? (
{t!.original_filename} {templates && templates.filter((o) => o.blend_file_path === t!.blend_file_path).length > 1 && ( ∗ shared )} Download
) : (
{!addFile && ( )}
)}