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:
parent
f3798c6988
commit
c8d9a3a2ec
@ -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,
|
||||
);
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user