diff --git a/frontend/src/components/admin/OutputTypeTable.tsx b/frontend/src/components/admin/OutputTypeTable.tsx index f1066a6..2199c00 100644 --- a/frontend/src/components/admin/OutputTypeTable.tsx +++ b/frontend/src/components/admin/OutputTypeTable.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect } from 'react' +import React, { useState, useRef, useEffect } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Pencil, Trash2, Plus, Check, X, ChevronDown, Copy } from 'lucide-react' import { toast } from 'sonner' @@ -218,6 +218,452 @@ export default function OutputTypeTable() { return renderer === 'blender' } + // Render the edit form grid (shared between edit-row and add-row) + function renderEditFormGrid( + mode: 'edit' | 'add', + ot: OutputType | null, + ) { + // For edit mode, use editDraft + ot fallback. For add mode, use form state. + const isEdit = mode === 'edit' && ot !== null + + // Getter helpers + const val = (field: keyof typeof form) => { + if (isEdit) { + if (field === 'name') return editDraft.name ?? ot!.name + if (field === 'renderer') return editDraft.renderer ?? ot!.renderer + if (field === 'output_format') return editDraft.output_format ?? ot!.output_format + if (field === 'is_animation') return editDraft.is_animation ?? ot!.is_animation + if (field === 'transparent_bg') return editDraft.transparent_bg ?? ot!.transparent_bg + if (field === 'cycles_device') return editDraft.cycles_device ?? (ot!.cycles_device || '') + if (field === 'sort_order') return editDraft.sort_order ?? ot!.sort_order + if (field === 'pricing_tier_id') return editDraft.pricing_tier_id ?? ot!.pricing_tier_id ?? '' + if (field === 'material_override') return editDraft.material_override ?? ot!.material_override ?? '' + // render_settings fields (prefixed with _) + if (field === 'width') return (editDraft as any)._width ?? (ot!.render_settings?.width || '') + if (field === 'height') return (editDraft as any)._height ?? (ot!.render_settings?.height || '') + if (field === 'engine') return (editDraft as any)._engine ?? (ot!.render_settings?.engine || '') + if (field === 'samples') return (editDraft as any)._samples ?? (ot!.render_settings?.samples || '') + if (field === 'frame_count') return (editDraft as any)._frame_count ?? (ot!.render_settings?.frame_count || '') + if (field === 'fps') return (editDraft as any)._fps ?? (ot!.render_settings?.fps || '') + if (field === 'turntable_axis') return (editDraft as any)._turntable_axis ?? (ot!.render_settings?.turntable_axis || 'world_z') + if (field === 'bg_color') { + return (editDraft as any)._bg_color !== undefined + ? (editDraft as any)._bg_color as string + : (ot!.render_settings?.bg_color as string || '') + } + if (field === 'noise_threshold') return (editDraft as any)._noise_threshold ?? (ot!.render_settings?.noise_threshold as string || '') + if (field === 'denoiser') return (editDraft as any)._denoiser ?? (ot!.render_settings?.denoiser as string || '') + if (field === 'denoising_input_passes') return (editDraft as any)._denoising_input_passes ?? (ot!.render_settings?.denoising_input_passes as string || '') + if (field === 'denoising_prefilter') return (editDraft as any)._denoising_prefilter ?? (ot!.render_settings?.denoising_prefilter as string || '') + if (field === 'denoising_quality') return (editDraft as any)._denoising_quality ?? (ot!.render_settings?.denoising_quality as string || '') + if (field === 'denoising_use_gpu') return (editDraft as any)._denoising_use_gpu ?? (ot!.render_settings?.denoising_use_gpu as string || '') + return (form as any)[field] + } + return (form as any)[field] + } + + const set = (field: string, value: any) => { + if (isEdit) { + // render_settings fields get _ prefix in editDraft + const rsFields = ['width', 'height', 'engine', 'samples', 'frame_count', 'fps', 'turntable_axis', 'bg_color', 'noise_threshold', 'denoiser', 'denoising_input_passes', 'denoising_prefilter', 'denoising_quality', 'denoising_use_gpu'] + if (rsFields.includes(field)) { + setEditDraft({ ...editDraft, [`_${field}`]: value } as any) + } else { + setEditDraft({ ...editDraft, [field]: value } as any) + } + } else { + setForm({ ...form, [field]: value }) + } + } + + const currentRenderer = val('renderer') as string + const currentFormat = val('output_format') as string + const currentIsAnimation = val('is_animation') as boolean + const isBlender = showBlenderSettings(currentRenderer) + const showBg = showTransparentBg(currentRenderer, currentFormat) + const bgColor = val('bg_color') as string + const bgEnabled = bgColor !== '' + + const categoriesValue = isEdit + ? (editDraft.compatible_categories ?? ot!.compatible_categories) || [] + : form.compatible_categories + + return ( + <> + {/* Row 1: Name | Renderer | Format | Animation */} +
+
+ + set('name', e.target.value)} + /> +
+
+ + +
+
+ + +
+
+ + +
+
+ + {/* Row 2: Turntable | Background | Device | Engine */} +
+
+ + {currentIsAnimation ? ( +
+
+ set('frame_count', e.target.value)} + title="Frame count" + /> + f + set('fps', e.target.value)} + title="FPS" + /> + fps +
+ +
+ ) : ( + N/A (not animation) + )} +
+
+ + {showBg ? ( +
+ + + {bgEnabled && ( +
+ set('bg_color', e.target.value)} + /> + set('bg_color', e.target.value)} + /> +
+ )} +
+ ) : ( + N/A (not Blender) + )} +
+
+ + {isBlender ? ( + + ) : ( + N/A (not Blender) + )} +
+
+ + {isBlender ? ( + + ) : ( + N/A (not Blender) + )} +
+
+ + {/* Row 3: Samples | Resolution | Pricing Tier | Workflow */} +
+
+ + {isBlender ? ( + set('samples', e.target.value)} + /> + ) : ( + N/A (not Blender) + )} +
+
+ +
+ set('width', e.target.value)} + /> + x + set('height', e.target.value)} + /> +
+
+
+ + +
+
+ + {isEdit ? ( + + ) : ( + — (set after creation) + )} +
+
+ + {/* Row 4: Categories | Material Override | Sort Order | Active */} +
+
+ + set('compatible_categories', cats)} + /> +
+
+ + +
+
+ + set('sort_order', Number(e.target.value))} + /> +
+
+ + {isEdit ? ( + + ) : ( + — (active by default) + )} +
+
+ + {/* Row 5: Denoising settings (only for Blender) */} + {isBlender && ( +
+ +
+
+ + +
+
+ + set('noise_threshold', e.target.value)} + title="Noise threshold (adaptive sampling)" + /> +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ )} + + ) + } + return (
@@ -235,7 +681,7 @@ export default function OutputTypeTable() { Samples Denoise Categories - Resolution + Resolution Pricing Workflow Mat Override @@ -247,858 +693,267 @@ export default function OutputTypeTable() { {isLoading && ( - Loading… + Loading... )} {types?.map((ot) => ( - - {editingId === ot.id ? ( - <> - - setEditDraft({ ...editDraft, name: e.target.value })} - /> - - - - - - - - - setEditDraft({ ...editDraft, is_animation: e.target.checked })} - /> - - - {(editDraft.is_animation ?? ot.is_animation) ? ( -
-
- setEditDraft({ ...editDraft, _frame_count: e.target.value } as any)} - title="Frame count" - /> - f - setEditDraft({ ...editDraft, _fps: e.target.value } as any)} - title="FPS" - /> - fps -
- -
- ) : ( - - )} - - - {showTransparentBg(editDraft.renderer ?? ot.renderer, editDraft.output_format ?? ot.output_format) ? ( -
- - {(() => { - const cur = (editDraft as any)._bg_color !== undefined - ? (editDraft as any)._bg_color as string - : (ot.render_settings?.bg_color as string || '') - const enabled = cur !== '' - return ( - <> - - {enabled && ( -
- setEditDraft({ ...editDraft, _bg_color: e.target.value } as any)} - /> - setEditDraft({ ...editDraft, _bg_color: e.target.value } as any)} - /> -
- )} - - ) - })()} -
- ) : ( - - )} - - - {showBlenderSettings(editDraft.renderer ?? ot.renderer) ? ( - - ) : ( - - )} - - - {showBlenderSettings(editDraft.renderer ?? ot.renderer) ? ( - - ) : ( - - )} - - - {showBlenderSettings(editDraft.renderer ?? ot.renderer) ? ( - setEditDraft({ ...editDraft, _samples: e.target.value } as any)} - /> - ) : ( - - )} - - - {showBlenderSettings(editDraft.renderer ?? ot.renderer) ? ( -
- - setEditDraft({ ...editDraft, _noise_threshold: e.target.value } as any)} - title="Noise threshold (adaptive sampling)" - /> - - - - -
- ) : ( - - )} - - - setEditDraft({ ...editDraft, compatible_categories: cats })} - /> - - -
- setEditDraft({ ...editDraft, _width: e.target.value } as any)} - /> - x - setEditDraft({ ...editDraft, _height: e.target.value } as any)} - /> + + {/* Display row — always visible */} + + {ot.name} + {ot.renderer} + {ot.output_format} + + {ot.is_animation && ( + video + )} + + + {ot.is_animation ? ( +
+ + {(ot.render_settings?.frame_count as number) || 120}f / {(ot.render_settings?.fps as number) || 30}fps + + + 360° {({'world_z': 'World Z', 'world_x': 'World X', 'world_y': 'World Y'} as Record)[ot.render_settings?.turntable_axis as string] ?? 'World Z'} +
- - - - - - - - - - - - setEditDraft({ ...editDraft, sort_order: Number(e.target.value) })} - /> - - - setEditDraft({ ...editDraft, is_active: e.target.checked })} - /> - - - - - - - ) : ( - <> - {ot.name} - {ot.renderer} - {ot.output_format} - - {ot.is_animation && ( - video - )} - - - {ot.is_animation ? ( -
- - {(ot.render_settings?.frame_count as number) || 120}f / {(ot.render_settings?.fps as number) || 30}fps - - - 360° {({'world_z': 'World Z', 'world_x': 'World X', 'world_y': 'World Y'} as Record)[ot.render_settings?.turntable_axis as string] ?? 'World Z'} - -
- ) : ( - - )} - - - {showTransparentBg(ot.renderer, ot.output_format) && ( -
- {ot.transparent_bg && ( - alpha - )} - {!!ot.render_settings?.bg_color && ( -
- - {ot.render_settings.bg_color as string} -
- )} -
- )} - - - {showBlenderSettings(ot.renderer) ? ( - ot.cycles_device ? ( - - {ot.cycles_device.toUpperCase()} - - ) : ( - system - ) - ) : ( - - )} - - - {showBlenderSettings(ot.renderer) ? ( - ot.render_settings?.engine ? ( - - {ot.render_settings.engine === 'cycles' ? 'Cycles' : 'EEVEE'} - - ) : ( - default - ) - ) : ( - - )} - - - {showBlenderSettings(ot.renderer) ? ( - ot.render_settings?.samples ? ( - {ot.render_settings.samples as number} - ) : ( - default - ) - ) : ( - - )} - - - {showBlenderSettings(ot.renderer) ? ( -
- {ot.render_settings?.denoiser ? ( - - {ot.render_settings.denoiser === 'OPTIX' ? 'OptiX' : 'OIDN'} - - ) : null} - {ot.render_settings?.noise_threshold ? ( - t={ot.render_settings.noise_threshold as string} - ) : null} - {ot.render_settings?.denoising_prefilter ? ( - {ot.render_settings.denoising_prefilter as string} - ) : null} - {!ot.render_settings?.denoiser && !ot.render_settings?.noise_threshold && !ot.render_settings?.denoising_prefilter && ( - default - )} -
- ) : ( - - )} - - - {(!ot.compatible_categories || ot.compatible_categories.length === 0) ? ( - All - ) : ( -
- {ot.compatible_categories.map((c: string) => ( - - {ALL_CATEGORIES.find((ac) => ac.key === c)?.label || c} - - ))} -
- )} - - - {ot.render_settings?.width || ot.render_settings?.height - ? `${ot.render_settings.width || '?'}x${ot.render_settings.height || '?'}` - : default} - - - {ot.pricing_tier_name ? ( -
- - {ot.pricing_tier_name} - - {ot.price_per_item != null && ( - - {ot.price_per_item.toFixed(2)} - - )} -
- ) : ( - Category default - )} - - - {(() => { - const wf = workflows?.find((w) => w.id === ot.workflow_definition_id) - return wf ? ( - - {wf.name} - - ) : ( - - Legacy - - ) - })()} - - - {ot.material_override ? ( - - {ot.material_override.replace('SCHAEFFLER_', '').replace(/_/g, ' ')} + ) : ( + + )} + + + {showTransparentBg(ot.renderer, ot.output_format) && ( +
+ {ot.transparent_bg && ( + alpha + )} + {!!ot.render_settings?.bg_color && ( +
+ + {ot.render_settings.bg_color as string} +
+ )} +
+ )} + + + {showBlenderSettings(ot.renderer) ? ( + ot.cycles_device ? ( + + {ot.cycles_device.toUpperCase()} ) : ( - - )} - - {ot.sort_order} - - - {ot.is_active ? 'active' : 'inactive'} + system + ) + ) : ( + + )} + + + {showBlenderSettings(ot.renderer) ? ( + ot.render_settings?.engine ? ( + + {ot.render_settings.engine === 'cycles' ? 'Cycles' : 'EEVEE'} + + ) : ( + default + ) + ) : ( + + )} + + + {showBlenderSettings(ot.renderer) ? ( + ot.render_settings?.samples ? ( + {ot.render_settings.samples as number} + ) : ( + default + ) + ) : ( + + )} + + + {showBlenderSettings(ot.renderer) ? ( +
+ {ot.render_settings?.denoiser ? ( + + {ot.render_settings.denoiser === 'OPTIX' ? 'OptiX' : 'OIDN'} + + ) : null} + {ot.render_settings?.noise_threshold ? ( + t={ot.render_settings.noise_threshold as string} + ) : null} + {ot.render_settings?.denoising_prefilter ? ( + {ot.render_settings.denoising_prefilter as string} + ) : null} + {!ot.render_settings?.denoiser && !ot.render_settings?.noise_threshold && !ot.render_settings?.denoising_prefilter && ( + default + )} +
+ ) : ( + + )} + + + {(!ot.compatible_categories || ot.compatible_categories.length === 0) ? ( + All + ) : ( +
+ {ot.compatible_categories.map((c: string) => ( + + {ALL_CATEGORIES.find((ac) => ac.key === c)?.label || c} + + ))} +
+ )} + + + {ot.render_settings?.width || ot.render_settings?.height + ? `${ot.render_settings.width || '?'}x${ot.render_settings.height || '?'}` + : default} + + + {ot.pricing_tier_name ? ( +
+ + {ot.pricing_tier_name} + + {ot.price_per_item != null && ( + + {ot.price_per_item.toFixed(2)} + + )} +
+ ) : ( + Category default + )} + + + {(() => { + const wf = workflows?.find((w) => w.id === ot.workflow_definition_id) + return wf ? ( + + {wf.name} + + ) : ( + + Legacy + + ) + })()} + + + {ot.material_override ? ( + + {ot.material_override.replace('SCHAEFFLER_', '').replace(/_/g, ' ')} + ) : ( + + )} + + {ot.sort_order} + + + {ot.is_active ? 'active' : 'inactive'} + + + + + + + + + + {/* Expandable edit form row */} + {editingId === ot.id && ( + + + {renderEditFormGrid('edit', ot)} +
+ + +
- - - - - - + )} - +
))} - {/* Add row */} + {/* Add new — expandable form */} {showAdd && ( - - - setForm({ ...form, name: e.target.value })} - /> - - - - - - - - - setForm({ ...form, is_animation: e.target.checked })} - /> - - - {form.is_animation ? ( -
-
- setForm({ ...form, frame_count: e.target.value })} - title="Frame count" - /> - f - setForm({ ...form, fps: e.target.value })} - title="FPS" - /> - fps -
- + Cancel + +
- ) : ( - - )} - - - {showTransparentBg(form.renderer, form.output_format) ? ( -
- - - {form.bg_color && ( -
- setForm({ ...form, bg_color: e.target.value })} - /> - setForm({ ...form, bg_color: e.target.value })} - /> -
- )} -
- ) : ( - - )} - - - {showBlenderSettings(form.renderer) ? ( - - ) : ( - - )} - - - {showBlenderSettings(form.renderer) ? ( - - ) : ( - - )} - - - {showBlenderSettings(form.renderer) ? ( - setForm({ ...form, samples: e.target.value })} - /> - ) : ( - - )} - - - {showBlenderSettings(form.renderer) ? ( -
- - setForm({ ...form, noise_threshold: e.target.value })} - title="Noise threshold (adaptive sampling)" - /> - - - - -
- ) : ( - - )} - - - setForm({ ...form, compatible_categories: cats })} - /> - - -
- setForm({ ...form, width: e.target.value })} - /> - x - setForm({ ...form, height: e.target.value })} - /> -
- - - - - — - - - - - setForm({ ...form, sort_order: Number(e.target.value) })} - /> - - — - - - - - + + + )}