Now able to delete a workflow template.

Also refactors the task list sort order to no longer use the predcessors.
This commit is contained in:
Colin Dawson 2026-03-12 23:46:34 +00:00
parent f3798c6988
commit c8d9a3a2ec
3 changed files with 162 additions and 33 deletions

View File

@ -90,7 +90,7 @@ const WotkflowTemplateManager: React.FC = () => {
};
const onDelete = async (item?: ReadWorkflowTemplate) => {
const response = await workflowTemplatesService.deleteTemplateVersion(
const response = await workflowTemplatesService.deleteTemplate(
item?.id,
item?.guid,
);

View File

@ -1,25 +1,62 @@
import { TaskDefinition } from "../services/WorkflowTemplateService";
function getOutcomeTargets(task: TaskDefinition): string[] {
const raw = task.config.outcomeActions as unknown;
if (Array.isArray(raw)) {
return raw
.map((entry) => {
const value = entry as Partial<{ task: string | null }>;
return value.task;
})
.filter((target): target is string => typeof target === "string");
}
if (raw && typeof raw === "object") {
return Object.values(raw as Record<string, string | null>)
.filter((target): target is string => typeof target === "string")
.map((target) => target);
}
return [];
}
function buildOutcomeAdjacency(
tasks: TaskDefinition[],
): Map<string, Set<string>> {
const allIds = new Set(tasks.map((task) => task.config.guid as string));
const adjacency = new Map<string, Set<string>>();
tasks.forEach((task) => {
const sourceId = task.config.guid as string;
const targets = new Set<string>();
getOutcomeTargets(task).forEach((targetId) => {
if (targetId !== sourceId && allIds.has(targetId)) {
targets.add(targetId);
}
});
adjacency.set(sourceId, targets);
});
return adjacency;
}
export const getAllDescendants = (
guid: string,
tasks: TaskDefinition[],
): Set<string> => {
const descendants = new Set<string>();
const adjacency = buildOutcomeAdjacency(tasks);
const visit = (current: string) => {
for (const t of tasks) {
const preds = (t.config.predecessors as string[]) ?? [];
// If t depends on current, it's a child
if (
preds.includes(current) &&
!descendants.has(t.config.guid as string)
) {
const childGuid = t.config.guid as string;
(adjacency.get(current) ?? new Set<string>()).forEach((childGuid) => {
if (!descendants.has(childGuid)) {
descendants.add(childGuid);
visit(childGuid); // recursively find grandchildren
}
}
});
};
visit(guid);
@ -29,37 +66,120 @@ export const getAllDescendants = (
export const sortTasksTopologically = (
tasks: TaskDefinition[],
): TaskDefinition[] => {
// Build adjacency list: task -> its predecessors
const preds = new Map<string, string[]>();
// Build adjacency list from outcome transitions: task -> possible next tasks.
const adjacency = buildOutcomeAdjacency(tasks);
const byId = new Map<string, TaskDefinition>();
const indexById = new Map<string, number>();
const indegree = new Map<string, number>();
const incoming = new Map<string, Set<string>>();
tasks.forEach((t) => {
const guid = t.config.guid as string;
byId.set(guid, t);
preds.set(guid, (t.config.predecessors as string[]) ?? []);
tasks.forEach((task, index) => {
const guid = task.config.guid as string;
byId.set(guid, task);
indexById.set(guid, index);
indegree.set(guid, 0);
incoming.set(guid, new Set<string>());
});
adjacency.forEach((targets, sourceId) => {
targets.forEach((targetId) => {
indegree.set(targetId, (indegree.get(targetId) ?? 0) + 1);
incoming.get(targetId)?.add(sourceId);
});
});
const insertByOriginalOrder = (queue: string[], guid: string) => {
const targetIndex = indexById.get(guid) ?? Number.MAX_SAFE_INTEGER;
let insertAt = queue.length;
for (let i = 0; i < queue.length; i += 1) {
const queueIndex = indexById.get(queue[i]) ?? Number.MAX_SAFE_INTEGER;
if (targetIndex < queueIndex) {
insertAt = i;
break;
}
}
queue.splice(insertAt, 0, guid);
};
const queue: string[] = [];
tasks.forEach((task) => {
const guid = task.config.guid as string;
if ((indegree.get(guid) ?? 0) === 0) {
insertByOriginalOrder(queue, guid);
}
});
const result: TaskDefinition[] = [];
const visited = new Set<string>();
const temp = new Set<string>(); // for cycle detection (should never trigger now)
while (queue.length > 0) {
const current = queue.shift() as string;
result.push(byId.get(current)!);
const visit = (guid: string) => {
if (visited.has(guid)) return;
if (temp.has(guid)) throw new Error("Cycle detected unexpectedly");
(adjacency.get(current) ?? new Set<string>()).forEach((targetId) => {
const nextInDegree = (indegree.get(targetId) ?? 0) - 1;
indegree.set(targetId, nextInDegree);
if (nextInDegree === 0) {
insertByOriginalOrder(queue, targetId);
}
});
}
temp.add(guid);
// Cycle fallback: append remaining tasks in a stable, deterministic order.
if (result.length < tasks.length) {
const resultIds = new Set(result.map((task) => task.config.guid as string));
const remainingIds = new Set(
tasks
.map((task) => task.config.guid as string)
.filter((guid) => !resultIds.has(guid)),
);
for (const p of preds.get(guid) ?? []) {
visit(p);
while (remainingIds.size > 0) {
const candidates = Array.from(remainingIds);
candidates.sort((aGuid, bGuid) => {
const aIncoming = incoming.get(aGuid) ?? new Set<string>();
const bIncoming = incoming.get(bGuid) ?? new Set<string>();
let aFromResolved = 0;
let bFromResolved = 0;
let aFromRemaining = 0;
let bFromRemaining = 0;
aIncoming.forEach((sourceId) => {
if (remainingIds.has(sourceId)) {
aFromRemaining += 1;
} else {
aFromResolved += 1;
}
});
bIncoming.forEach((sourceId) => {
if (remainingIds.has(sourceId)) {
bFromRemaining += 1;
} else {
bFromResolved += 1;
}
});
// Prefer tasks that are most reachable from already-ordered tasks.
if (aFromResolved !== bFromResolved) {
return bFromResolved - aFromResolved;
}
// Then prefer tasks with fewer unresolved dependencies.
if (aFromRemaining !== bFromRemaining) {
return aFromRemaining - bFromRemaining;
}
return (indexById.get(aGuid) ?? 0) - (indexById.get(bGuid) ?? 0);
});
const nextGuid = candidates[0];
remainingIds.delete(nextGuid);
result.push(byId.get(nextGuid)!);
}
temp.delete(guid);
visited.add(guid);
result.push(byId.get(guid)!);
};
// Visit all tasks
tasks.forEach((t) => visit(t.config.guid as string));
}
return result;
};

View File

@ -75,6 +75,14 @@ export async function getTemplates(
return response?.data;
}
export async function deleteTemplate(id?: bigint, guid?: string): Promise<any> {
const generalIdRef = MakeGeneralIdRef(id, guid);
return await httpService.delete(apiEndpoint + "/template", {
data: generalIdRef,
});
}
export async function getTemplateVersions(
page: number,
pageSize: number,
@ -162,6 +170,7 @@ export async function getTaskMetadata(
const templateVersionsService = {
getTemplates,
deleteTemplate,
getTemplateVersions,
getTemplateVersion,
postTemplateVersion,