feat: refactor workflow editor authoring surfaces

This commit is contained in:
2026-04-08 21:44:08 +02:00
parent fe46dabfc5
commit 042f62fe55
25 changed files with 4877 additions and 1823 deletions
@@ -0,0 +1,247 @@
import type { StepCategory, WorkflowNodeDefinition, WorkflowNodeFamily } from '../../api/workflows'
export type WorkflowNodeFamilyFilter = 'all' | WorkflowNodeFamily
export type WorkflowGraphFamily = WorkflowNodeFamily | 'mixed'
export type WorkflowNodeKindFilter = 'all' | 'legacy' | 'bridge' | 'graph'
export type WorkflowNodeLibraryGroup = 'legacy' | 'bridge' | 'graph'
export const CATEGORY_LABELS: Record<StepCategory, string> = {
input: 'Input',
processing: 'Processing',
rendering: 'Rendering',
output: 'Output',
}
export const CATEGORY_COLORS: Record<StepCategory, string> = {
input: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300',
processing: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300',
rendering: 'bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300',
output: 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300',
}
export const NODE_CATEGORY_ORDER: StepCategory[] = ['input', 'processing', 'rendering', 'output']
export const FAMILY_FILTER_LABELS: Record<WorkflowNodeFamilyFilter, string> = {
all: 'All Nodes',
cad_file: 'CAD Intake',
order_line: 'Order Rendering',
}
export const NODE_KIND_FILTER_LABELS: Record<WorkflowNodeKindFilter, string> = {
all: 'All Modes',
legacy: 'Legacy',
bridge: 'Bridge',
graph: 'Graph',
}
export const NODE_LIBRARY_GROUP_LABELS: Record<WorkflowNodeLibraryGroup, string> = {
legacy: 'Legacy Nodes',
bridge: 'Bridge Nodes',
graph: 'Graph Nodes',
}
export const NODE_LIBRARY_GROUP_STYLES: Record<WorkflowNodeLibraryGroup, string> = {
legacy: 'bg-slate-100 text-slate-700 dark:bg-slate-900/40 dark:text-slate-300',
bridge: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300',
graph: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300',
}
export const NODE_LIBRARY_GROUP_DESCRIPTIONS: Record<WorkflowNodeLibraryGroup, string> = {
legacy: 'Legacy-safe nodes that map cleanly to the existing production path.',
bridge: 'Compatibility nodes that still rely on bridge execution behavior.',
graph: 'Native graph runtime nodes for the non-legacy editor flow.',
}
export const FAMILY_FILTER_DESCRIPTIONS: Record<WorkflowNodeFamily, string> = {
cad_file: 'Start with a CAD file context and produce previews, caches, or derived assets.',
order_line: 'Start with an order line context and run production rendering/output steps.',
}
export const FAMILY_FILTER_STYLES: Record<WorkflowNodeFamily, string> = {
cad_file: 'bg-sky-100 text-sky-700 dark:bg-sky-900/40 dark:text-sky-300',
order_line: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300',
}
export const GRAPH_FAMILY_LABELS: Record<WorkflowGraphFamily, string> = {
cad_file: 'CAD Intake',
order_line: 'Order Rendering',
mixed: 'Mixed Family',
}
export const GRAPH_FAMILY_STYLES: Record<WorkflowGraphFamily, string> = {
cad_file: FAMILY_FILTER_STYLES.cad_file,
order_line: FAMILY_FILTER_STYLES.order_line,
mixed: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300',
}
export type WorkflowNodeDefinitionMap = Record<string, WorkflowNodeDefinition>
const CAD_FILE_NODE_STEPS = new Set([
'resolve_step_path',
'occ_object_extract',
'occ_glb_export',
'stl_cache_generate',
'blender_render',
'threejs_render',
'thumbnail_save',
])
export function getNodeFamily(step: string, nodeDefinitionsByStep?: WorkflowNodeDefinitionMap): WorkflowNodeFamily {
return nodeDefinitionsByStep?.[step]?.family ?? (CAD_FILE_NODE_STEPS.has(step) ? 'cad_file' : 'order_line')
}
export function getDefinitionFamily(
definition: WorkflowNodeDefinition,
nodeDefinitionsByStep?: WorkflowNodeDefinitionMap,
): WorkflowNodeFamily {
return definition.family ?? getNodeFamily(definition.step, nodeDefinitionsByStep)
}
export function isDefinitionAllowedForGraphFamily(
definition: WorkflowNodeDefinition,
graphFamily: WorkflowGraphFamily,
nodeDefinitionsByStep?: WorkflowNodeDefinitionMap,
): boolean {
if (graphFamily === 'mixed') return true
return getDefinitionFamily(definition, nodeDefinitionsByStep) === graphFamily
}
export function compareNodeDefinitions(a: WorkflowNodeDefinition, b: WorkflowNodeDefinition) {
const categoryDelta = NODE_CATEGORY_ORDER.indexOf(a.category) - NODE_CATEGORY_ORDER.indexOf(b.category)
if (categoryDelta !== 0) return categoryDelta
return a.label.localeCompare(b.label)
}
export function getDefinitionModuleNamespace(definition: WorkflowNodeDefinition): string {
const [namespace] = definition.module_key.split('.')
return namespace || 'workflow'
}
export function getDefinitionModuleLabel(definition: WorkflowNodeDefinition): string {
const namespace = getDefinitionModuleNamespace(definition)
return namespace
.split('_')
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ')
}
export function groupDefinitionsForStepSelect(definitions: WorkflowNodeDefinition[]) {
const groups = new Map<string, WorkflowNodeDefinition[]>()
for (const definition of [...definitions].sort(compareNodeDefinitions)) {
const family = getDefinitionFamily(definition)
const groupLabel = `${FAMILY_FILTER_LABELS[family]} · ${getDefinitionModuleLabel(definition)} · ${CATEGORY_LABELS[definition.category]}`
groups.set(groupLabel, [...(groups.get(groupLabel) ?? []), definition])
}
return Array.from(groups.entries()).map(([label, options]) => ({ label, options }))
}
export function groupDefinitionsByFamily(
definitions: WorkflowNodeDefinition[],
nodeDefinitionsByStep?: WorkflowNodeDefinitionMap,
) {
return {
cad_file: definitions
.filter(definition => getDefinitionFamily(definition, nodeDefinitionsByStep) === 'cad_file')
.sort(compareNodeDefinitions),
order_line: definitions
.filter(definition => getDefinitionFamily(definition, nodeDefinitionsByStep) === 'order_line')
.sort(compareNodeDefinitions),
} as Record<WorkflowNodeFamily, WorkflowNodeDefinition[]>
}
export function groupDefinitionsByModule(definitions: WorkflowNodeDefinition[]) {
const groups = new Map<
string,
{
namespace: string
label: string
definitions: WorkflowNodeDefinition[]
}
>()
for (const definition of [...definitions].sort(compareNodeDefinitions)) {
const namespace = getDefinitionModuleNamespace(definition)
const existing = groups.get(namespace)
if (existing) {
existing.definitions.push(definition)
continue
}
groups.set(namespace, {
namespace,
label: getDefinitionModuleLabel(definition),
definitions: [definition],
})
}
return Array.from(groups.values()).sort((a, b) => a.label.localeCompare(b.label))
}
export function getPrimaryLibraryGroup(definition: WorkflowNodeDefinition): WorkflowNodeLibraryGroup {
if (definition.execution_kind === 'native') {
return 'graph'
}
if (definition.legacy_compatible) {
return 'legacy'
}
return 'bridge'
}
export function matchesNodeKindFilter(
definition: WorkflowNodeDefinition,
filter: WorkflowNodeKindFilter,
): boolean {
if (filter === 'all') return true
if (filter === 'legacy') return definition.legacy_compatible
if (filter === 'bridge') return definition.execution_kind === 'bridge'
return definition.execution_kind === 'native'
}
export function getDefinitionSearchText(definition: WorkflowNodeDefinition): string {
return [
definition.label,
definition.step,
definition.module_key,
getDefinitionModuleLabel(definition),
definition.description,
CATEGORY_LABELS[definition.category],
FAMILY_FILTER_LABELS[getDefinitionFamily(definition)],
definition.execution_kind === 'bridge' ? 'bridge' : 'graph',
definition.legacy_compatible ? 'legacy' : '',
definition.artifact_roles_consumed.join(' '),
definition.artifact_roles_produced.join(' '),
]
.join(' ')
.toLowerCase()
}
export function getDefinitionBadges(definition: WorkflowNodeDefinition) {
const badges: { label: string; className: string }[] = []
if (definition.legacy_compatible) {
badges.push({
label: 'Legacy',
className: NODE_LIBRARY_GROUP_STYLES.legacy,
})
}
badges.push({
label: definition.execution_kind === 'bridge' ? 'Bridge' : 'Graph',
className:
definition.execution_kind === 'bridge'
? NODE_LIBRARY_GROUP_STYLES.bridge
: NODE_LIBRARY_GROUP_STYLES.graph,
})
return badges
}
export function groupDefinitionsByPrimaryLibraryGroup(definitions: WorkflowNodeDefinition[]) {
return {
legacy: definitions.filter(definition => getPrimaryLibraryGroup(definition) === 'legacy').sort(compareNodeDefinitions),
bridge: definitions.filter(definition => getPrimaryLibraryGroup(definition) === 'bridge').sort(compareNodeDefinitions),
graph: definitions.filter(definition => getPrimaryLibraryGroup(definition) === 'graph').sort(compareNodeDefinitions),
} as Record<WorkflowNodeLibraryGroup, WorkflowNodeDefinition[]>
}