feat: per-line render overrides — override any output type setting at order time
Instead of duplicating output types for every variation (WebP vs PNG,
different resolution), keep one canonical output type and override
specific fields per order line via render_overrides JSONB.
Backend:
- render_overrides JSONB column on OrderLine (DB migration)
- Render task merges overrides with output type settings (format, width,
height, samples, engine, denoiser, transparent_bg, cycles_device)
- POST /orders/{id}/batch-render-overrides endpoint for bulk override
- PatchLineBody accepts render_overrides for per-line patching
Frontend:
- Batch render overrides section on OrderDetail: output format dropdown
(PNG/JPG/WebP) + resolution dropdown (512-4096)
- Clear button to remove overrides
MCP:
- create_order tool: accepts product_ids, output_type, render_overrides,
material_override — enables "render all products as WebP" via Claude
- set_render_overrides tool: batch override on existing orders
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,7 @@ export interface OrderLine {
|
||||
render_started_at: string | null
|
||||
render_completed_at: string | null
|
||||
material_override: string | null
|
||||
render_overrides: Record<string, unknown> | null
|
||||
notes: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
@@ -68,6 +69,7 @@ export interface OrderLineCreate {
|
||||
global_render_position_id?: string | null
|
||||
gewuenschte_bildnummer?: string | null
|
||||
material_override?: string | null
|
||||
render_overrides?: Record<string, unknown> | null
|
||||
notes?: string | null
|
||||
}
|
||||
|
||||
@@ -252,7 +254,7 @@ export async function batchMaterialOverride(orderId: string, materialOverride: s
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function patchOrderLine(orderId: string, lineId: string, data: { material_override?: string | null }) {
|
||||
export async function patchOrderLine(orderId: string, lineId: string, data: { material_override?: string | null; render_overrides?: Record<string, unknown> | null }) {
|
||||
const res = await api.patch<{ updated: boolean; line_id: string }>(
|
||||
`/orders/${orderId}/lines/${lineId}`,
|
||||
data
|
||||
@@ -260,6 +262,14 @@ export async function patchOrderLine(orderId: string, lineId: string, data: { ma
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function batchRenderOverrides(orderId: string, renderOverrides: Record<string, unknown> | null) {
|
||||
const res = await api.post<{ updated: number; render_overrides: Record<string, unknown> | null }>(
|
||||
`/orders/${orderId}/batch-render-overrides`,
|
||||
{ render_overrides: renderOverrides }
|
||||
)
|
||||
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`
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
XCircle, RotateCw, Info,
|
||||
} from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { getOrder, submitOrder, deleteOrder, unlinkCadFile, regenerateItemThumbnail, patchOrderItem, removeOrderLine, dispatchRenders, cancelLineRender, dispatchLineRender, cancelOrderRenders, splitMissingStep, generateLinesFromItems, downloadOrderRenders, rejectOrder, resubmitOrder, rejectOrderLine, patchOrderLine, batchMaterialOverride } from '../api/orders'
|
||||
import { getOrder, submitOrder, deleteOrder, unlinkCadFile, regenerateItemThumbnail, patchOrderItem, removeOrderLine, dispatchRenders, cancelLineRender, dispatchLineRender, cancelOrderRenders, splitMissingStep, generateLinesFromItems, downloadOrderRenders, rejectOrder, resubmitOrder, rejectOrderLine, patchOrderLine, batchMaterialOverride, batchRenderOverrides } from '../api/orders'
|
||||
import { checkOrderMaterials, listMaterials, type UnmappedMaterial, type Material } from '../api/materials'
|
||||
import UnmappedMaterialsDialog from '../components/orders/UnmappedMaterialsDialog'
|
||||
import type { OrderItem, OrderLine } from '../api/orders'
|
||||
@@ -146,6 +146,15 @@ export default function OrderDetailPage() {
|
||||
onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed'),
|
||||
})
|
||||
|
||||
const batchRenderOverridesMut = useMutation({
|
||||
mutationFn: (val: Record<string, unknown> | null) => batchRenderOverrides(id!, val),
|
||||
onSuccess: (data) => {
|
||||
toast.success(`Render overrides ${data.render_overrides ? 'set' : 'cleared'} on ${data.updated} lines`)
|
||||
qc.invalidateQueries({ queryKey: ['order', id] })
|
||||
},
|
||||
onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed'),
|
||||
})
|
||||
|
||||
const cancelAllMut = useMutation({
|
||||
mutationFn: () => cancelOrderRenders(id!),
|
||||
onSuccess: (data) => {
|
||||
@@ -652,6 +661,55 @@ export default function OrderDetailPage() {
|
||||
{batchOverrideMut.isPending && <Loader2 size={14} className="animate-spin text-accent" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Batch Render Overrides */}
|
||||
<div className="flex items-center justify-between gap-4 flex-wrap mt-3 pt-3 border-t border-border-light">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-content">Batch Render Overrides</p>
|
||||
<p className="text-xs text-content-muted mt-0.5">Override output format or resolution for all lines.</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
className="text-sm border border-border-default rounded-lg px-3 py-1.5"
|
||||
style={{ backgroundColor: 'var(--color-bg-surface)' }}
|
||||
value=""
|
||||
onChange={(e) => {
|
||||
const val = e.target.value
|
||||
if (val === '__clear__') batchRenderOverridesMut.mutate(null)
|
||||
else if (val === 'webp') batchRenderOverridesMut.mutate({ output_format: 'webp' })
|
||||
else if (val === 'png') batchRenderOverridesMut.mutate({ output_format: 'png' })
|
||||
else if (val === 'jpg') batchRenderOverridesMut.mutate({ output_format: 'jpg' })
|
||||
}}
|
||||
disabled={batchRenderOverridesMut.isPending}
|
||||
>
|
||||
<option value="">Output format...</option>
|
||||
<option value="__clear__">-- Clear all overrides --</option>
|
||||
<option value="png">PNG</option>
|
||||
<option value="jpg">JPG</option>
|
||||
<option value="webp">WebP</option>
|
||||
</select>
|
||||
<select
|
||||
className="text-sm border border-border-default rounded-lg px-3 py-1.5"
|
||||
style={{ backgroundColor: 'var(--color-bg-surface)' }}
|
||||
value=""
|
||||
onChange={(e) => {
|
||||
const val = e.target.value
|
||||
if (!val) return
|
||||
const [w, h] = val.split('x').map(Number)
|
||||
batchRenderOverridesMut.mutate({ width: w, height: h })
|
||||
}}
|
||||
disabled={batchRenderOverridesMut.isPending}
|
||||
>
|
||||
<option value="">Resolution...</option>
|
||||
<option value="512x512">512 x 512</option>
|
||||
<option value="1024x1024">1024 x 1024</option>
|
||||
<option value="1920x1920">1920 x 1920</option>
|
||||
<option value="2048x2048">2048 x 2048</option>
|
||||
<option value="4096x4096">4096 x 4096</option>
|
||||
</select>
|
||||
{batchRenderOverridesMut.isPending && <Loader2 size={14} className="animate-spin text-accent" />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user