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 onDelete = async (item?: ReadWorkflowTemplate) => {
|
||||||
const response = await workflowTemplatesService.deleteTemplateVersion(
|
const response = await workflowTemplatesService.deleteTemplate(
|
||||||
item?.id,
|
item?.id,
|
||||||
item?.guid,
|
item?.guid,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,25 +1,62 @@
|
|||||||
import { TaskDefinition } from "../services/WorkflowTemplateService";
|
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 = (
|
export const getAllDescendants = (
|
||||||
guid: string,
|
guid: string,
|
||||||
tasks: TaskDefinition[],
|
tasks: TaskDefinition[],
|
||||||
): Set<string> => {
|
): Set<string> => {
|
||||||
const descendants = new Set<string>();
|
const descendants = new Set<string>();
|
||||||
|
const adjacency = buildOutcomeAdjacency(tasks);
|
||||||
|
|
||||||
const visit = (current: string) => {
|
const visit = (current: string) => {
|
||||||
for (const t of tasks) {
|
(adjacency.get(current) ?? new Set<string>()).forEach((childGuid) => {
|
||||||
const preds = (t.config.predecessors as string[]) ?? [];
|
if (!descendants.has(childGuid)) {
|
||||||
|
|
||||||
// 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;
|
|
||||||
descendants.add(childGuid);
|
descendants.add(childGuid);
|
||||||
visit(childGuid); // recursively find grandchildren
|
visit(childGuid); // recursively find grandchildren
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
visit(guid);
|
visit(guid);
|
||||||
@ -29,37 +66,120 @@ export const getAllDescendants = (
|
|||||||
export const sortTasksTopologically = (
|
export const sortTasksTopologically = (
|
||||||
tasks: TaskDefinition[],
|
tasks: TaskDefinition[],
|
||||||
): TaskDefinition[] => {
|
): TaskDefinition[] => {
|
||||||
// Build adjacency list: task -> its predecessors
|
// Build adjacency list from outcome transitions: task -> possible next tasks.
|
||||||
const preds = new Map<string, string[]>();
|
const adjacency = buildOutcomeAdjacency(tasks);
|
||||||
const byId = new Map<string, TaskDefinition>();
|
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) => {
|
tasks.forEach((task, index) => {
|
||||||
const guid = t.config.guid as string;
|
const guid = task.config.guid as string;
|
||||||
byId.set(guid, t);
|
byId.set(guid, task);
|
||||||
preds.set(guid, (t.config.predecessors as string[]) ?? []);
|
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 result: TaskDefinition[] = [];
|
||||||
const visited = new Set<string>();
|
while (queue.length > 0) {
|
||||||
const temp = new Set<string>(); // for cycle detection (should never trigger now)
|
const current = queue.shift() as string;
|
||||||
|
result.push(byId.get(current)!);
|
||||||
|
|
||||||
const visit = (guid: string) => {
|
(adjacency.get(current) ?? new Set<string>()).forEach((targetId) => {
|
||||||
if (visited.has(guid)) return;
|
const nextInDegree = (indegree.get(targetId) ?? 0) - 1;
|
||||||
if (temp.has(guid)) throw new Error("Cycle detected unexpectedly");
|
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) ?? []) {
|
while (remainingIds.size > 0) {
|
||||||
visit(p);
|
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;
|
return result;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -75,6 +75,14 @@ export async function getTemplates(
|
|||||||
return response?.data;
|
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(
|
export async function getTemplateVersions(
|
||||||
page: number,
|
page: number,
|
||||||
pageSize: number,
|
pageSize: number,
|
||||||
@ -162,6 +170,7 @@ export async function getTaskMetadata(
|
|||||||
|
|
||||||
const templateVersionsService = {
|
const templateVersionsService = {
|
||||||
getTemplates,
|
getTemplates,
|
||||||
|
deleteTemplate,
|
||||||
getTemplateVersions,
|
getTemplateVersions,
|
||||||
getTemplateVersion,
|
getTemplateVersion,
|
||||||
postTemplateVersion,
|
postTemplateVersion,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user