Skip to content

Commit 7baf885

Browse files
committed
feat: manage workflows
Signed-off-by: Simon Emms <simon@simonemms.com>
1 parent a1bcafc commit 7baf885

5 files changed

Lines changed: 704 additions & 15 deletions

File tree

src/lib/i18n/messages/en.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,18 @@
356356
"workflows": "Workflows",
357357
"addWorkflow": "+ Add workflow",
358358
"tasks": "Tasks",
359-
"controlFlow": "Control flow"
359+
"controlFlow": "Control flow",
360+
"workflow": {
361+
"delete": "Delete workflow",
362+
"deleteConfirmTitle": "Delete workflow",
363+
"deleteConfirmMessage": "Are you sure you want to delete this workflow? This cannot be undone.",
364+
"deleteLastBlocked": "At least one workflow must exist.",
365+
"deleteInUse": "This workflow is referenced by other tasks and cannot be deleted.",
366+
"deleteCancel": "Cancel",
367+
"deleteConfirm": "Delete",
368+
"rename": "Rename workflow",
369+
"renameInput": "Workflow name"
370+
}
360371
},
361372
"canvas": {
362373
"ariaLabel": "Workflow canvas",

src/lib/tasks/actions.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,135 @@ export function addWorkflow(file: WorkflowFile, name: string): WorkflowFile {
8888
};
8989
}
9090

91+
// Collect all workflow names that are referenced by any node in the file.
92+
// References come from two sources:
93+
// 1. SwitchBranch.thenWorkflowName — a `then:` redirect to a named workflow
94+
// 2. TaskNode with config.kind === 'run-workflow' — an explicit run call
95+
//
96+
// The returned set contains workflow *names* (not IDs).
97+
export function referencedWorkflowNames(file: WorkflowFile): Set<string> {
98+
const names = new Set<string>();
99+
100+
function walkGraph(graph: FlowGraph): void {
101+
for (const node of Object.values(graph.nodes)) {
102+
if (node.type === 'task') {
103+
if (node.config.kind === 'run-workflow') {
104+
names.add(node.config.name);
105+
}
106+
} else if (node.type === 'switch') {
107+
for (const branch of node.branches) {
108+
if (branch.thenWorkflowName) names.add(branch.thenWorkflowName);
109+
walkGraph(branch.graph);
110+
}
111+
} else if (node.type === 'fork') {
112+
for (const branch of node.branches) {
113+
walkGraph(branch.graph);
114+
}
115+
} else if (node.type === 'try') {
116+
walkGraph(node.tryGraph);
117+
if (node.catchGraph) walkGraph(node.catchGraph);
118+
} else if (node.type === 'loop') {
119+
walkGraph(node.bodyGraph);
120+
}
121+
}
122+
}
123+
124+
for (const workflow of Object.values(file.workflows)) {
125+
walkGraph(workflow.root);
126+
}
127+
128+
return names;
129+
}
130+
131+
// Update every reference to a workflow name throughout the entire file.
132+
// Called after renaming a workflow so that SwitchBranch `then:` redirects and
133+
// run-workflow task configs stay consistent with the new name.
134+
export function renameWorkflowReferences(
135+
file: WorkflowFile,
136+
oldName: string,
137+
newName: string,
138+
): WorkflowFile {
139+
function updateNode(node: Node): Node {
140+
if (node.type === 'task') {
141+
if (node.config.kind === 'run-workflow' && node.config.name === oldName) {
142+
return { ...node, config: { ...node.config, name: newName } };
143+
}
144+
return node;
145+
}
146+
if (node.type === 'switch') {
147+
const newBranches = node.branches.map((branch) => {
148+
const updatedGraph = updateGraph(branch.graph);
149+
const updatedName =
150+
branch.thenWorkflowName === oldName
151+
? newName
152+
: branch.thenWorkflowName;
153+
if (
154+
updatedGraph === branch.graph &&
155+
updatedName === branch.thenWorkflowName
156+
)
157+
return branch;
158+
return {
159+
...branch,
160+
graph: updatedGraph,
161+
thenWorkflowName: updatedName,
162+
};
163+
});
164+
if (newBranches.every((b, i) => b === node.branches[i])) return node;
165+
return { ...node, branches: newBranches };
166+
}
167+
if (node.type === 'fork') {
168+
const newBranches = node.branches.map((branch) => {
169+
const updatedGraph = updateGraph(branch.graph);
170+
return updatedGraph === branch.graph
171+
? branch
172+
: { ...branch, graph: updatedGraph };
173+
});
174+
if (newBranches.every((b, i) => b === node.branches[i])) return node;
175+
return { ...node, branches: newBranches };
176+
}
177+
if (node.type === 'try') {
178+
const newTryGraph = updateGraph(node.tryGraph);
179+
const newCatchGraph = node.catchGraph
180+
? updateGraph(node.catchGraph)
181+
: undefined;
182+
if (newTryGraph === node.tryGraph && newCatchGraph === node.catchGraph)
183+
return node;
184+
return { ...node, tryGraph: newTryGraph, catchGraph: newCatchGraph };
185+
}
186+
if (node.type === 'loop') {
187+
const newBodyGraph = updateGraph(node.bodyGraph);
188+
return newBodyGraph === node.bodyGraph
189+
? node
190+
: { ...node, bodyGraph: newBodyGraph };
191+
}
192+
return node;
193+
}
194+
195+
function updateGraph(graph: FlowGraph): FlowGraph {
196+
const newNodes: Record<string, Node> = {};
197+
let changed = false;
198+
for (const [id, node] of Object.entries(graph.nodes)) {
199+
const updated = updateNode(node);
200+
if (updated !== node) changed = true;
201+
newNodes[id] = updated;
202+
}
203+
return changed ? { ...graph, nodes: newNodes } : graph;
204+
}
205+
206+
const newWorkflows: WorkflowFile['workflows'] = {};
207+
let changed = false;
208+
for (const [id, wf] of Object.entries(file.workflows)) {
209+
const newRoot = updateGraph(wf.root);
210+
if (newRoot === wf.root) {
211+
newWorkflows[id] = wf;
212+
} else {
213+
changed = true;
214+
newWorkflows[id] = { ...wf, root: newRoot };
215+
}
216+
}
217+
return changed ? { ...file, workflows: newWorkflows } : file;
218+
}
219+
91220
export function removeWorkflow(file: WorkflowFile, id: string): WorkflowFile {
92221
if (file.order.length <= 1) {
93222
throw new Error('Cannot remove the last workflow from a file');

0 commit comments

Comments
 (0)