655 lines
22 KiB
TypeScript
655 lines
22 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef } from "react";
|
|
import {
|
|
normalizeWorkflowProfileSessionStartPolicy,
|
|
normalizeWorkflowProfileSessionEndPolicy,
|
|
type Workflow,
|
|
type WorkflowStep,
|
|
} from "@/lib/types/http";
|
|
import { generateUUID } from "@/lib/utils";
|
|
import { t } from "@/lib/i18n";
|
|
import { useToast } from "@/components/toast-provider";
|
|
import { useSerializedMutationQueue } from "./use-serialized-mutation-queue";
|
|
import type { WorkflowMutationGuardController } from "./workflow-mutation-guard";
|
|
import { applyWorkflowStepUpdates } from "./workflow-step-mutations";
|
|
import { areStepDraftsEqual, stepUpdatePayload } from "./workflow-step-equality";
|
|
export { areStepDraftsEqual } from "./workflow-step-equality";
|
|
import {
|
|
createWorkflowAction,
|
|
createWorkflowStepAction,
|
|
updateWorkflowAction,
|
|
updateWorkflowStepAction,
|
|
deleteWorkflowStepAction,
|
|
reorderWorkflowStepsAction,
|
|
listWorkflowStepsAction,
|
|
getStepTaskCount,
|
|
getWorkflowTaskCount,
|
|
bulkMoveTasks,
|
|
} from "@/app/actions/workspaces";
|
|
|
|
const TEMP_WORKFLOW_PREFIX = "temp-workflow-";
|
|
|
|
function fallbackErrorMessage(error: unknown): string {
|
|
return error instanceof Error ? error.message : t("common:requestFailed");
|
|
}
|
|
|
|
type WorkflowStepActionsParams = {
|
|
workflow: Workflow;
|
|
isNewWorkflow?: boolean;
|
|
/**
|
|
* Workflows synced from GitHub (`workflow.source === "github"`) are
|
|
* read-only in the UI; the backend also rejects step mutations with a 409.
|
|
* Gating here is defense-in-depth in case a disabled control is somehow
|
|
* still triggered.
|
|
*/
|
|
readOnly?: boolean;
|
|
workflowSteps: WorkflowStep[];
|
|
setWorkflowSteps: (updater: ((prev: WorkflowStep[]) => WorkflowStep[]) | WorkflowStep[]) => void;
|
|
refreshWorkflowSteps?: () => Promise<void>;
|
|
setStepToDelete: (id: string | null) => void;
|
|
setStepTaskCount: (count: number | null) => void;
|
|
setTargetStepForMigration: (id: string) => void;
|
|
setStepDeleteOpen: (open: boolean) => void;
|
|
toast: ReturnType<typeof useToast>["toast"];
|
|
mutationGuard?: WorkflowMutationGuardController;
|
|
};
|
|
|
|
type OpenStepDeleteDialogParams = {
|
|
stepId: string;
|
|
taskCount: number;
|
|
workflowSteps: WorkflowStep[];
|
|
setStepToDelete: (id: string | null) => void;
|
|
setStepTaskCount: (count: number | null) => void;
|
|
setTargetStepForMigration: (id: string) => void;
|
|
setStepDeleteOpen: (open: boolean) => void;
|
|
};
|
|
|
|
function openStepDeleteDialog({
|
|
stepId,
|
|
taskCount,
|
|
workflowSteps,
|
|
setStepToDelete,
|
|
setStepTaskCount,
|
|
setTargetStepForMigration,
|
|
setStepDeleteOpen,
|
|
}: OpenStepDeleteDialogParams) {
|
|
setStepToDelete(stepId);
|
|
setStepTaskCount(taskCount);
|
|
const otherSteps = workflowSteps.filter((s) => s.id !== stepId);
|
|
setTargetStepForMigration(otherSteps.length > 0 ? otherSteps[0].id : "");
|
|
setStepDeleteOpen(true);
|
|
}
|
|
|
|
// Seeded default for a brand-new step. Both fields are PERSISTED verbatim —
|
|
// `name` becomes the step's stored name and `color` its Tailwind class — so
|
|
// neither is translated; translating the name would write a localized string
|
|
// into the database.
|
|
// i18n-exempt: persisted workflow step name, same contract as DEFAULT_CUSTOM_STEPS.
|
|
const NEW_STEP_DEFAULTS = { name: "New Step", color: "bg-slate-500" } as const;
|
|
|
|
function createDraftStep(workflow: Workflow, position: number): WorkflowStep {
|
|
return {
|
|
id: `temp-step-${generateUUID()}`,
|
|
workflow_id: workflow.id,
|
|
...NEW_STEP_DEFAULTS,
|
|
position,
|
|
allow_manual_move: true,
|
|
complete_task_on_enter: false,
|
|
created_at: "",
|
|
updated_at: "",
|
|
};
|
|
}
|
|
|
|
function useWorkflowStepMutationQueue(toast: WorkflowStepActionsParams["toast"]) {
|
|
return useSerializedMutationQueue((errorTitle, error) => {
|
|
toast({ title: errorTitle, description: fallbackErrorMessage(error), variant: "error" });
|
|
});
|
|
}
|
|
|
|
function createRemoveWorkflowStepHandler(
|
|
params: WorkflowStepActionsParams,
|
|
guardMutation: WorkflowMutationGuardController["guardMutation"],
|
|
) {
|
|
const {
|
|
workflow,
|
|
readOnly = false,
|
|
workflowSteps,
|
|
setWorkflowSteps,
|
|
setStepToDelete,
|
|
setStepTaskCount,
|
|
setTargetStepForMigration,
|
|
setStepDeleteOpen,
|
|
toast,
|
|
} = params;
|
|
const isNewWorkflow = params.isNewWorkflow ?? workflow.id.startsWith(TEMP_WORKFLOW_PREFIX);
|
|
return async (stepId: string) => {
|
|
if (readOnly) return;
|
|
const dependentStep = workflowSteps.find(
|
|
(step) => step.session_target?.kind === "step" && step.session_target.step_id === stepId,
|
|
);
|
|
if (dependentStep) {
|
|
toast({
|
|
title: t("workflows:sessionTargetDependentMustBeRepaired"),
|
|
description: t("workflows:sessionTargetDependentRepairDescription", {
|
|
stepName: dependentStep.name,
|
|
}),
|
|
variant: "error",
|
|
});
|
|
return;
|
|
}
|
|
const proposedSteps = workflowSteps
|
|
.filter((step) => step.id !== stepId)
|
|
.map((step, position) => ({ ...step, position }));
|
|
if (isNewWorkflow) {
|
|
setWorkflowSteps(proposedSteps);
|
|
return;
|
|
}
|
|
await guardMutation({
|
|
proposedSteps,
|
|
operation: async () => {
|
|
if (stepId.startsWith("temp-")) {
|
|
setWorkflowSteps(proposedSteps);
|
|
return;
|
|
}
|
|
let taskCount: number;
|
|
try {
|
|
({ task_count: taskCount } = await getStepTaskCount(stepId));
|
|
} catch (error) {
|
|
toast({
|
|
title: t("workflows:failedToCheckWorkflowStepTasks"),
|
|
description: fallbackErrorMessage(error),
|
|
variant: "error",
|
|
});
|
|
return;
|
|
}
|
|
openStepDeleteDialog({
|
|
stepId,
|
|
taskCount,
|
|
workflowSteps,
|
|
setStepToDelete,
|
|
setStepTaskCount,
|
|
setTargetStepForMigration,
|
|
setStepDeleteOpen,
|
|
});
|
|
},
|
|
});
|
|
};
|
|
}
|
|
|
|
export function useWorkflowStepActions(params: WorkflowStepActionsParams) {
|
|
const { workflow, workflowSteps, setWorkflowSteps, toast, mutationGuard } = params;
|
|
const isNewWorkflow = params.isNewWorkflow ?? workflow.id.startsWith(TEMP_WORKFLOW_PREFIX);
|
|
const readOnly = params.readOnly ?? false;
|
|
const mutationQueue = useWorkflowStepMutationQueue(toast);
|
|
const runMutation = mutationQueue.run;
|
|
const guardMutation: WorkflowMutationGuardController["guardMutation"] =
|
|
mutationGuard?.guardMutation ?? (async ({ operation }) => operation());
|
|
|
|
const handleUpdateWorkflowStep = async (stepId: string, updates: Partial<WorkflowStep>) => {
|
|
if (readOnly) return;
|
|
const proposedSteps = applyWorkflowStepUpdates(workflowSteps, stepId, updates);
|
|
if (isNewWorkflow) {
|
|
setWorkflowSteps(() => proposedSteps);
|
|
return;
|
|
}
|
|
await guardMutation({
|
|
proposedSteps,
|
|
operation: async () => setWorkflowSteps(() => proposedSteps),
|
|
});
|
|
};
|
|
|
|
const handleAddWorkflowStep = async () => {
|
|
if (readOnly) return;
|
|
const draftStep = createDraftStep(workflow, workflowSteps.length);
|
|
const proposedSteps = [...workflowSteps, draftStep];
|
|
if (isNewWorkflow) {
|
|
setWorkflowSteps((previous) => [...previous, draftStep]);
|
|
return;
|
|
}
|
|
await guardMutation({
|
|
proposedSteps,
|
|
operation: async () => setWorkflowSteps((previous) => [...previous, draftStep]),
|
|
});
|
|
};
|
|
|
|
const handleRemoveWorkflowStep = createRemoveWorkflowStepHandler(params, guardMutation);
|
|
|
|
const handleReorderWorkflowSteps = async (reorderedSteps: WorkflowStep[]) => {
|
|
if (readOnly) return;
|
|
const proposedSteps = reorderedSteps.map((step, position) => ({ ...step, position }));
|
|
if (isNewWorkflow) {
|
|
setWorkflowSteps(proposedSteps);
|
|
return;
|
|
}
|
|
await guardMutation({
|
|
proposedSteps,
|
|
operation: async () => setWorkflowSteps(proposedSteps),
|
|
});
|
|
};
|
|
|
|
return {
|
|
handleUpdateWorkflowStep,
|
|
handleAddWorkflowStep,
|
|
handleRemoveWorkflowStep,
|
|
handleReorderWorkflowSteps,
|
|
status: mutationQueue.status,
|
|
retry: mutationQueue.retry,
|
|
runMutation,
|
|
};
|
|
}
|
|
|
|
export type WorkflowDraftSaveProgress = {
|
|
workflow?: Workflow;
|
|
stepIds: Map<string, string>;
|
|
templateStepsLoaded?: boolean;
|
|
};
|
|
|
|
export function createWorkflowDraftSaveProgress(): WorkflowDraftSaveProgress {
|
|
return { stepIds: new Map() };
|
|
}
|
|
|
|
type PersistWorkflowDraftParams = {
|
|
workflow: Workflow;
|
|
draftSteps: WorkflowStep[];
|
|
savedSteps: WorkflowStep[];
|
|
progress: WorkflowDraftSaveProgress;
|
|
};
|
|
|
|
export async function persistWorkflowDraft({
|
|
workflow,
|
|
draftSteps,
|
|
savedSteps,
|
|
progress,
|
|
}: PersistWorkflowDraftParams): Promise<{ workflow: Workflow; steps: WorkflowStep[] }> {
|
|
const isNewWorkflow = workflow.id.startsWith(TEMP_WORKFLOW_PREFIX);
|
|
const persistedWorkflow = await ensurePersistedWorkflow(workflow, progress);
|
|
const updatedWorkflow = await updateWorkflowAction(persistedWorkflow.id, {
|
|
name: workflow.name.trim(),
|
|
description: workflow.description ?? "",
|
|
prompt: workflow.prompt ?? "",
|
|
agent_profile_id: workflow.agent_profile_id ?? "",
|
|
});
|
|
progress.workflow = updatedWorkflow;
|
|
await reconcileTemplateSteps({ workflow, draftSteps, updatedWorkflow, progress, isNewWorkflow });
|
|
await createMissingSteps(updatedWorkflow.id, draftSteps, progress.stepIds);
|
|
const remappedSteps = remapWorkflowDraftSteps(draftSteps, updatedWorkflow.id, progress.stepIds);
|
|
await updateChangedSteps(remappedSteps, savedSteps);
|
|
if (remappedSteps.length > 0) {
|
|
await reorderWorkflowStepsAction(
|
|
updatedWorkflow.id,
|
|
remappedSteps.map((step) => step.id),
|
|
);
|
|
}
|
|
return { workflow: updatedWorkflow, steps: remappedSteps };
|
|
}
|
|
|
|
async function ensurePersistedWorkflow(
|
|
workflow: Workflow,
|
|
progress: WorkflowDraftSaveProgress,
|
|
): Promise<Workflow> {
|
|
if (progress.workflow) return progress.workflow;
|
|
const persisted = workflow.id.startsWith(TEMP_WORKFLOW_PREFIX)
|
|
? await createWorkflowAction({
|
|
workspace_id: workflow.workspace_id,
|
|
name: workflow.name.trim(),
|
|
description: workflow.description ?? undefined,
|
|
prompt: workflow.prompt ?? undefined,
|
|
workflow_template_id: workflow.workflow_template_id ?? undefined,
|
|
})
|
|
: workflow;
|
|
progress.workflow = persisted;
|
|
return persisted;
|
|
}
|
|
|
|
async function reconcileTemplateSteps({
|
|
workflow,
|
|
draftSteps,
|
|
updatedWorkflow,
|
|
progress,
|
|
isNewWorkflow,
|
|
}: {
|
|
workflow: Workflow;
|
|
draftSteps: WorkflowStep[];
|
|
updatedWorkflow: Workflow;
|
|
progress: WorkflowDraftSaveProgress;
|
|
isNewWorkflow: boolean;
|
|
}) {
|
|
if (!isNewWorkflow || !workflow.workflow_template_id || progress.templateStepsLoaded) return;
|
|
const templateSteps = (await listWorkflowStepsAction(updatedWorkflow.id)).steps ?? [];
|
|
mapTemplateStepIds(workflow.id, draftSteps, templateSteps, progress.stepIds);
|
|
const keptServerIds = new Set(progress.stepIds.values());
|
|
for (const serverStep of templateSteps) {
|
|
if (!keptServerIds.has(serverStep.id)) await deleteWorkflowStepAction(serverStep.id);
|
|
}
|
|
progress.templateStepsLoaded = true;
|
|
}
|
|
|
|
async function createMissingSteps(
|
|
workflowId: string,
|
|
draftSteps: WorkflowStep[],
|
|
mappings: Map<string, string>,
|
|
) {
|
|
for (const step of draftSteps) {
|
|
if (!step.id.startsWith("temp-") || mappings.has(step.id)) continue;
|
|
const created = await createWorkflowStepAction({
|
|
workflow_id: workflowId,
|
|
name: step.name,
|
|
position: step.position,
|
|
color: step.color,
|
|
stage_type: step.stage_type ?? "custom",
|
|
complete_task_on_enter: step.complete_task_on_enter ?? false,
|
|
cancel_triggers_turn_complete: step.cancel_triggers_turn_complete ?? false,
|
|
agent_profile_id: step.agent_profile_id,
|
|
profile_session_start_policy: normalizeWorkflowProfileSessionStartPolicy(
|
|
step.profile_session_start_policy,
|
|
),
|
|
profile_session_end_policy: normalizeWorkflowProfileSessionEndPolicy(
|
|
step.profile_session_end_policy,
|
|
),
|
|
session_target: undefined,
|
|
auto_advance_requires_signal: step.auto_advance_requires_signal ?? false,
|
|
});
|
|
mappings.set(step.id, created.id);
|
|
}
|
|
}
|
|
|
|
async function updateChangedSteps(remapped: WorkflowStep[], savedSteps: WorkflowStep[]) {
|
|
const savedById = new Map(savedSteps.map((step) => [step.id, step]));
|
|
const changed = remapped.filter((step) => {
|
|
const saved = savedById.get(step.id);
|
|
return !saved || !areStepDraftsEqual(step, saved);
|
|
});
|
|
|
|
const targetChanged = (step: WorkflowStep, saved: WorkflowStep) =>
|
|
JSON.stringify(step.session_target ?? null) !== JSON.stringify(saved.session_target ?? null);
|
|
const targetNeedsSourceProfileFirst = (step: WorkflowStep) => {
|
|
const target = step.session_target;
|
|
if (target?.kind !== "step") return false;
|
|
const sourceDraft = remapped.find((candidate) => candidate.id === target.step_id);
|
|
const sourceSaved = sourceDraft ? savedById.get(sourceDraft.id) : undefined;
|
|
return sourceSaved?.agent_profile_id === "" && sourceDraft?.agent_profile_id !== "";
|
|
};
|
|
|
|
// Existing targets must be detached before their source profile is removed.
|
|
// A replacement target also clears the mutually-exclusive direct profile in
|
|
// the same request, so the controller never observes an invalid half-state.
|
|
for (const step of changed) {
|
|
const saved = savedById.get(step.id);
|
|
if (!saved || !targetChanged(step, saved) || saved.session_target == null) continue;
|
|
const attachAfterSourceProfile = targetNeedsSourceProfileFirst(step);
|
|
await updateWorkflowStepAction(step.id, {
|
|
session_target: attachAfterSourceProfile ? null : (step.session_target ?? null),
|
|
...(!attachAfterSourceProfile && step.session_target != null ? { agent_profile_id: "" } : {}),
|
|
});
|
|
}
|
|
|
|
// A newly attached target can refer to a source whose direct profile is also
|
|
// being enabled in this save. Persist all other changes first, then attach
|
|
// those targets once the source profile is valid on the server.
|
|
const targetAdditions = changed.filter((step) => {
|
|
const saved = savedById.get(step.id);
|
|
return (
|
|
(saved?.session_target == null && step.session_target != null) ||
|
|
targetNeedsSourceProfileFirst(step)
|
|
);
|
|
});
|
|
const targetAdditionIDs = new Set(targetAdditions.map((step) => step.id));
|
|
for (const step of changed) {
|
|
if (!targetAdditionIDs.has(step.id)) {
|
|
await updateWorkflowStepAction(step.id, stepUpdatePayload(step));
|
|
}
|
|
}
|
|
for (const step of targetAdditions) {
|
|
await updateWorkflowStepAction(step.id, stepUpdatePayload(step));
|
|
}
|
|
}
|
|
|
|
function mapTemplateStepIds(
|
|
clientWorkflowId: string,
|
|
draftSteps: WorkflowStep[],
|
|
serverSteps: WorkflowStep[],
|
|
mappings: Map<string, string>,
|
|
) {
|
|
const serverByPosition = new Map(serverSteps.map((step) => [step.position, step.id]));
|
|
const prefix = `temp-template-step-${clientWorkflowId}-`;
|
|
for (const step of draftSteps) {
|
|
if (!step.id.startsWith(prefix)) continue;
|
|
const originalPosition = Number(step.id.slice(prefix.length));
|
|
const serverId = serverByPosition.get(originalPosition);
|
|
if (serverId) mappings.set(step.id, serverId);
|
|
}
|
|
}
|
|
|
|
function remapDraftStep(
|
|
step: WorkflowStep,
|
|
persistedWorkflowId: string,
|
|
position: number,
|
|
mappings: Map<string, string>,
|
|
): WorkflowStep {
|
|
const remapId = (id: string | null | undefined) => (id ? (mappings.get(id) ?? id) : id);
|
|
return {
|
|
...step,
|
|
id: mappings.get(step.id) ?? step.id,
|
|
workflow_id: persistedWorkflowId as WorkflowStep["workflow_id"],
|
|
position,
|
|
pull_from_step_id: remapId(step.pull_from_step_id),
|
|
session_target:
|
|
step.session_target?.kind === "step"
|
|
? { kind: "step", step_id: remapId(step.session_target.step_id)! }
|
|
: (step.session_target ?? null),
|
|
events: remapStepReferences(step.events, mappings),
|
|
};
|
|
}
|
|
|
|
export function remapWorkflowDraftSteps(
|
|
steps: WorkflowStep[],
|
|
persistedWorkflowId: string,
|
|
mappings: Map<string, string>,
|
|
): WorkflowStep[] {
|
|
return steps.map((step, position) =>
|
|
remapDraftStep(step, persistedWorkflowId, position, mappings),
|
|
);
|
|
}
|
|
|
|
function remapStepReferences<T>(value: T, mappings: Map<string, string>): T {
|
|
if (Array.isArray(value)) return value.map((item) => remapStepReferences(item, mappings)) as T;
|
|
if (!value || typeof value !== "object") return value;
|
|
const mapped = Object.entries(value).map(([key, item]) => [
|
|
key,
|
|
key === "step_id" && typeof item === "string"
|
|
? (mappings.get(item) ?? item)
|
|
: remapStepReferences(item, mappings),
|
|
]);
|
|
return Object.fromEntries(mapped) as T;
|
|
}
|
|
|
|
type WorkflowDeleteHandlersParams = {
|
|
workflow: Workflow;
|
|
/** See `WorkflowStepActionsParams.readOnly` — defense-in-depth for synced workflows. */
|
|
readOnly?: boolean;
|
|
otherWorkflows: Workflow[];
|
|
wfDel: {
|
|
setDeleteOpen: (v: boolean) => void;
|
|
setWorkflowTaskCount: (v: number | null) => void;
|
|
setWorkflowDeleteLoading: (v: boolean) => void;
|
|
setTargetWorkflowId: (v: string) => void;
|
|
setTargetWorkflowSteps: (v: WorkflowStep[]) => void;
|
|
setTargetStepId: (v: string) => void;
|
|
targetWorkflowId: string;
|
|
targetStepId: string;
|
|
setMigrateLoading: (v: boolean) => void;
|
|
};
|
|
deleteWorkflowRun: () => Promise<unknown>;
|
|
toast: ReturnType<typeof useToast>["toast"];
|
|
};
|
|
|
|
export function useWorkflowDeleteHandlers({
|
|
workflow,
|
|
readOnly = false,
|
|
otherWorkflows,
|
|
wfDel,
|
|
deleteWorkflowRun,
|
|
toast,
|
|
}: WorkflowDeleteHandlersParams) {
|
|
useEffect(() => {
|
|
if (!wfDel.targetWorkflowId) {
|
|
wfDel.setTargetWorkflowSteps([]);
|
|
wfDel.setTargetStepId("");
|
|
return;
|
|
}
|
|
let cancelled = false;
|
|
listWorkflowStepsAction(wfDel.targetWorkflowId)
|
|
.then((res) => {
|
|
if (!cancelled) {
|
|
const steps = res.steps ?? [];
|
|
wfDel.setTargetWorkflowSteps(steps);
|
|
wfDel.setTargetStepId(steps.length > 0 ? steps[0].id : "");
|
|
}
|
|
})
|
|
.catch(() => {
|
|
if (!cancelled) wfDel.setTargetWorkflowSteps([]);
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [wfDel.targetWorkflowId]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
const handleDeleteWorkflowClick = async () => {
|
|
if (readOnly) return;
|
|
wfDel.setWorkflowDeleteLoading(true);
|
|
try {
|
|
const { task_count } = await getWorkflowTaskCount(workflow.id);
|
|
wfDel.setWorkflowTaskCount(task_count);
|
|
if (task_count > 0 && otherWorkflows.length > 0)
|
|
wfDel.setTargetWorkflowId(otherWorkflows[0].id);
|
|
wfDel.setDeleteOpen(true);
|
|
} catch (error) {
|
|
toast({
|
|
title: t("workflows:failedToCheckWorkflowTasks"),
|
|
description: fallbackErrorMessage(error),
|
|
variant: "error",
|
|
});
|
|
} finally {
|
|
wfDel.setWorkflowDeleteLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleDeleteWorkflow = async () => {
|
|
// A background sync can flip the workflow read-only while the delete
|
|
// dialog is already open; re-check at confirm time.
|
|
if (readOnly) return;
|
|
try {
|
|
await deleteWorkflowRun();
|
|
wfDel.setDeleteOpen(false);
|
|
} catch (error) {
|
|
toast({
|
|
title: t("workflows:failedToDeleteWorkflow"),
|
|
description: fallbackErrorMessage(error),
|
|
variant: "error",
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleMigrateAndDeleteWorkflow = async () => {
|
|
if (readOnly) return;
|
|
if (!wfDel.targetWorkflowId || !wfDel.targetStepId) return;
|
|
wfDel.setMigrateLoading(true);
|
|
try {
|
|
await bulkMoveTasks({
|
|
source_workflow_id: workflow.id,
|
|
target_workflow_id: wfDel.targetWorkflowId,
|
|
target_step_id: wfDel.targetStepId,
|
|
});
|
|
await deleteWorkflowRun();
|
|
wfDel.setDeleteOpen(false);
|
|
} catch (error) {
|
|
toast({
|
|
title: t("workflows:failedToMigrateTasks"),
|
|
description: fallbackErrorMessage(error),
|
|
variant: "error",
|
|
});
|
|
} finally {
|
|
wfDel.setMigrateLoading(false);
|
|
}
|
|
};
|
|
|
|
return { handleDeleteWorkflowClick, handleDeleteWorkflow, handleMigrateAndDeleteWorkflow };
|
|
}
|
|
|
|
type StepDeleteHandlersParams = {
|
|
workflow: Workflow;
|
|
stepDel: {
|
|
stepToDelete: string | null;
|
|
targetStepForMigration: string;
|
|
setStepMigrateLoading: (v: boolean) => void;
|
|
setStepDeletePending: (v: boolean) => void;
|
|
setStepDeleteOpen: (v: boolean) => void;
|
|
setStepToDelete: (v: string | null) => void;
|
|
};
|
|
refreshWorkflowSteps: () => Promise<void>;
|
|
runMutation: (operation: () => Promise<void>, errorTitle: string) => Promise<boolean>;
|
|
};
|
|
|
|
export function useStepDeleteHandlers({
|
|
workflow,
|
|
stepDel,
|
|
refreshWorkflowSteps,
|
|
runMutation,
|
|
}: StepDeleteHandlersParams) {
|
|
const deletePendingRef = useRef(false);
|
|
|
|
const runStepDelete = async (operation: () => Promise<void>, errorTitle: string) => {
|
|
if (deletePendingRef.current) return;
|
|
let mutationCompleted = false;
|
|
deletePendingRef.current = true;
|
|
stepDel.setStepDeletePending(true);
|
|
try {
|
|
const saved = await runMutation(async () => {
|
|
stepDel.setStepMigrateLoading(true);
|
|
try {
|
|
if (!mutationCompleted) {
|
|
await operation();
|
|
mutationCompleted = true;
|
|
}
|
|
await refreshWorkflowSteps();
|
|
stepDel.setStepToDelete(null);
|
|
stepDel.setStepDeleteOpen(false);
|
|
} finally {
|
|
stepDel.setStepMigrateLoading(false);
|
|
}
|
|
}, errorTitle);
|
|
if (!saved) {
|
|
stepDel.setStepToDelete(null);
|
|
stepDel.setStepDeleteOpen(false);
|
|
}
|
|
} finally {
|
|
deletePendingRef.current = false;
|
|
stepDel.setStepDeletePending(false);
|
|
}
|
|
};
|
|
|
|
const handleMigrateAndDeleteStep = async () => {
|
|
if (!stepDel.stepToDelete || !stepDel.targetStepForMigration) return;
|
|
await runStepDelete(async () => {
|
|
await bulkMoveTasks({
|
|
source_workflow_id: workflow.id,
|
|
source_step_id: stepDel.stepToDelete!,
|
|
target_workflow_id: workflow.id,
|
|
target_step_id: stepDel.targetStepForMigration,
|
|
});
|
|
await deleteWorkflowStepAction(stepDel.stepToDelete!);
|
|
}, t("workflows:failedToMigrateTasks"));
|
|
};
|
|
|
|
const handleDeleteStepAndTasks = async () => {
|
|
if (!stepDel.stepToDelete) return;
|
|
await runStepDelete(
|
|
() => deleteWorkflowStepAction(stepDel.stepToDelete!),
|
|
t("workflows:failedToDeleteStep"),
|
|
);
|
|
};
|
|
|
|
return { handleMigrateAndDeleteStep, handleDeleteStepAndTasks };
|
|
}
|