260228:1520 20260228:15:30 workflow update #4 Visual Builder
All checks were successful
Build and Deploy / deploy (push) Successful in 2m19s

This commit is contained in:
admin
2026-02-28 15:20:57 +07:00
parent 5378c0bd2a
commit 90d19941ef
3 changed files with 421 additions and 480 deletions

View File

@@ -63,82 +63,60 @@ interface VisualWorkflowBuilderProps {
function parseDSL(dsl: string): { nodes: Node[], edges: Edge[] } { function parseDSL(dsl: string): { nodes: Node[], edges: Edge[] } {
const nodes: Node[] = []; const nodes: Node[] = [];
const edges: Edge[] = []; const edges: Edge[] = [];
let yOffset = 100; let yOffset = 50;
try { try {
// Simple line-based parser for the demo YAML structure const parsedDsl = JSON.parse(dsl);
// name: Workflow const states = parsedDsl.states || [];
// steps:
// - name: Step1 ...
const lines = dsl.split('\n'); states.forEach((state: { id: string, name: string, type: string, role?: string, transitions?: { event: string, to: string }[] }) => {
let currentStep: Record<string, string> | null = null; const isCondition = state.type === 'CONDITION';
const steps: Record<string, string>[] = []; const isStart = state.type === 'START';
const isEnd = state.type === 'END';
// Very basic parser logic (replace with js-yaml in production) let nodeType = 'default';
let inSteps = false; let style = { ...nodeStyle };
for (const line of lines) {
const trimmed = line.trim(); if (isStart) {
if (trimmed.startsWith('steps:')) { nodeType = 'input';
inSteps = true; style = { ...nodeStyle, background: '#10b981', color: 'white', border: 'none' };
continue; } else if (isEnd) {
} nodeType = 'output';
if (inSteps && trimmed.startsWith('- name:')) { style = { ...nodeStyle, background: '#ef4444', color: 'white', border: 'none' };
if (currentStep) steps.push(currentStep); } else if (isCondition) {
currentStep = { name: trimmed.replace('- name:', '').trim() }; style = conditionNodeStyle;
} else if (inSteps && currentStep && trimmed.startsWith('next:')) { }
currentStep.next = trimmed.replace('next:', '').trim();
} else if (inSteps && currentStep && trimmed.startsWith('type:')) {
currentStep.type = trimmed.replace('type:', '').trim();
} else if (inSteps && currentStep && trimmed.startsWith('role:')) {
currentStep.role = trimmed.replace('role:', '').trim();
}
}
if (currentStep) steps.push(currentStep);
// Generate Nodes
nodes.push({ nodes.push({
id: 'start', id: state.id,
type: 'input', type: nodeType,
data: { label: 'Start' }, data: {
position: { x: 250, y: 0 }, label: isStart || isEnd ? state.name : `${state.name}\n(${state.role || 'No Role'})`,
style: { ...nodeStyle, background: '#10b981', color: 'white', border: 'none' } name: state.name,
}); role: state.role,
type: state.type || (isStart ? 'START' : isEnd ? 'END' : 'TASK')
steps.forEach((step) => { },
const isCondition = step.type === 'CONDITION';
nodes.push({
id: step.name,
data: { label: `${step.name}\n(${step.role || 'No Role'})`, name: step.name, role: step.role, type: step.type }, // Store role in data
position: { x: 250, y: yOffset }, position: { x: 250, y: yOffset },
style: isCondition ? conditionNodeStyle : { ...nodeStyle } style: style
});
yOffset += 100;
}); });
nodes.push({ if (state.transitions) {
id: 'end', state.transitions.forEach((trans: { event: string, to: string }) => {
type: 'output',
data: { label: 'End' },
position: { x: 250, y: yOffset },
style: { ...nodeStyle, background: '#ef4444', color: 'white', border: 'none' }
});
// Generate Edges
edges.push({ id: 'e-start-first', source: 'start', target: steps[0]?.name || 'end', markerEnd: { type: MarkerType.ArrowClosed } });
steps.forEach((step, index) => {
const nextStep = step.next || (index + 1 < steps.length ? steps[index + 1].name : 'end');
edges.push({ edges.push({
id: `e-${step.name}-${nextStep}`, id: `e-${state.id}-${trans.to}`,
source: step.name, source: state.id,
target: nextStep, target: trans.to,
label: trans.event,
markerEnd: { type: MarkerType.ArrowClosed } markerEnd: { type: MarkerType.ArrowClosed }
}); });
}); });
}
yOffset += 120;
});
} catch (e) { } catch (e) {
console.error("Failed to parse DSL", e); console.error("Failed to parse DSL as JSON", e);
} }
return { nodes, edges }; return { nodes, edges };
@@ -169,10 +147,12 @@ function VisualWorkflowBuilderContent({ initialNodes: propNodes, initialEdges: p
const addNode = (type: string, label: string) => { const addNode = (type: string, label: string) => {
const id = `${type}-${Date.now()}`; const id = `${type}-${Date.now()}`;
const nodeType = type === 'condition' ? 'CONDITION' : type === 'end' ? 'END' : type === 'start' ? 'START' : 'TASK';
const newNode: Node = { const newNode: Node = {
id, id,
position: { x: Math.random() * 400 + 100, y: Math.random() * 300 + 100 }, position: { x: Math.random() * 400 + 100, y: Math.random() * 300 + 100 },
data: { label: label, name: label, role: 'User', type: type === 'condition' ? 'CONDITION' : 'APPROVAL' }, data: { label: label, name: label, role: 'User', type: nodeType },
style: { ...nodeStyle }, style: { ...nodeStyle },
}; };
@@ -193,38 +173,26 @@ function VisualWorkflowBuilderContent({ initialNodes: propNodes, initialEdges: p
onSave?.(nodes, edges); onSave?.(nodes, edges);
}; };
// Mock DSL generation for demonstration // Generate JSON DSL
const generateDSL = () => { const generateDSL = () => {
const steps = nodes const states = nodes.map(n => {
.filter(n => n.type !== 'input' && n.type !== 'output') const outgoingEdges = edges.filter(e => e.source === n.id);
.map(n => ({ const transitions = outgoingEdges.map(e => ({
// name: n.data.label, // Removed duplicate event: e.label || 'PROCEED',
// Actually, we should probably separate name and label display. to: e.target
// For now, let's assume data.label IS the name, and we render it differently?
// Wait, ReactFlow Default Node renders 'label'.
// If I change label to "Name\nRole", then generateDSL will use "Name\nRole" as name.
// BAD.
// Fix: ReactFlow Node Component.
// custom Node?
// Quick fix: Keep label as Name. Render a CUSTOM NODE?
// Or just parsing: keep label as name.
// But user wants to SEE role.
// If I change label, I break name.
// Change: Use data.name for name, data.role for role.
// And label = `${name}\n(${role})`
// And here: use data.name if available, else label (cleaned).
name: n.data.name || n.data.label.split('\n')[0],
role: n.data.role,
type: n.data.type || 'APPROVAL', // Use stored type
next: edges.find(e => e.source === n.id)?.target || 'End'
})); }));
const dsl = `name: Visual Workflow return {
steps: id: n.id,
${steps.map(s => ` - name: ${s.name} name: n.data.name || n.data.label.split('\n')[0],
role: ${s.role || 'User'} type: n.data.type || 'TASK',
type: ${s.type} role: n.data.role,
next: ${s.next}`).join('\n')}`; transitions: transitions
};
});
const dslObj = { states };
const dsl = JSON.stringify(dslObj, null, 2);
console.log("Generated DSL:", dsl); console.log("Generated DSL:", dsl);
onDslChange?.(dsl); onDslChange?.(dsl);

File diff suppressed because one or more lines are too long

361
specs/deploy-deploy-38.txt Normal file

File diff suppressed because one or more lines are too long