n8n-editor-ui
Version:
Workflow Editor UI for n8n
251 lines (250 loc) • 10.7 kB
JavaScript
import { gt as useI18n } from "./_MapCache-B159LOu5.js";
import { t as useMessage } from "./useMessage-C1bozAQq.js";
import { Dn as useSourceControlStore, Fi as useUIStore, Gn as useCanvasStore, Sn as injectWorkflowState, Ti as tryToParseNumber, at as useNodeTypesStore, mn as useWorkflowHelpers, o as useWorkflowsStore, t as useTelemetry, xn as useNodeHelpers } from "./useTelemetry-C797TaYa.js";
import { t as useToast } from "./useToast-ZaUhbG9w.js";
import { $o as MODAL_CANCEL, Do as getResourcePermissions, In as NON_ACTIVATABLE_TRIGGER_NODE_TYPES, Io as VIEWS, es as MODAL_CLOSE, s as PLACEHOLDER_EMPTY_WORKFLOW_ID, ts as MODAL_CONFIRM } from "./constants-CX1ozgFz.js";
import { t as useExternalHooks } from "./useExternalHooks-CYMS6JpD.js";
import { t as useNpsSurveyStore } from "./npsSurvey.store-BuKohzKX.js";
import { t as useTemplatesStore } from "./templates.store-CSx5YxpE.js";
import { t as useFocusPanelStore } from "./focusPanel.store-Dys3muoU.js";
function useWorkflowSaving({ router, workflowState: providedWorkflowState }) {
const uiStore = useUIStore();
const npsSurveyStore = useNpsSurveyStore();
const message = useMessage();
const i18n = useI18n();
const workflowsStore = useWorkflowsStore();
const workflowState = providedWorkflowState ?? injectWorkflowState();
const focusPanelStore = useFocusPanelStore();
const nodeTypesStore = useNodeTypesStore();
const toast = useToast();
const telemetry = useTelemetry();
const nodeHelpers = useNodeHelpers();
const templatesStore = useTemplatesStore();
const { getWorkflowDataToSave, checkConflictingWebhooks, getWorkflowProjectRole } = useWorkflowHelpers();
async function promptSaveUnsavedWorkflowChanges(next, { confirm = async () => true, cancel = async () => {} } = {}) {
if (!uiStore.stateIsDirty || workflowsStore.workflow.isArchived || !getResourcePermissions(workflowsStore.workflow.scopes).workflow.update) {
next();
return;
}
switch (await message.confirm(i18n.baseText("generic.unsavedWork.confirmMessage.message"), {
title: i18n.baseText("generic.unsavedWork.confirmMessage.headline"),
type: "warning",
confirmButtonText: i18n.baseText("generic.unsavedWork.confirmMessage.confirmButtonText"),
cancelButtonText: i18n.baseText("generic.unsavedWork.confirmMessage.cancelButtonText"),
showClose: true
})) {
case MODAL_CONFIRM:
if (await saveCurrentWorkflow({}, false)) {
await npsSurveyStore.fetchPromptsData();
uiStore.stateIsDirty = false;
next(await confirm());
} else stayOnCurrentWorkflow(next);
return;
case MODAL_CANCEL:
await cancel();
uiStore.stateIsDirty = false;
next();
return;
case MODAL_CLOSE:
if (workflowsStore.workflow.id !== "__EMPTY__") stayOnCurrentWorkflow(next);
return;
}
}
function stayOnCurrentWorkflow(next) {
next(router.resolve({
name: VIEWS.WORKFLOW,
params: { name: workflowsStore.workflow.id }
}));
}
function isNodeActivatable(node) {
if (node.disabled) return false;
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
return nodeType !== null && nodeType.group.includes("trigger") && !NON_ACTIVATABLE_TRIGGER_NODE_TYPES.includes(node.type);
}
async function getWorkflowDeactivationInfo(workflowId, request) {
if (request.nodes !== void 0 && !request.nodes.some(isNodeActivatable)) return {
title: i18n.baseText("workflows.autodeactivated"),
message: i18n.baseText("workflowActivator.thisWorkflowHasNoTriggerNodes"),
type: "info"
};
const conflictData = await checkConflictingWebhooks(workflowId);
if (conflictData) return {
title: "Conflicting Webhook Path",
message: `Workflow set to inactive: Workflow set to inactive: Live webhook in another workflow uses same path as node '${conflictData.trigger.name}'.`,
type: "error"
};
}
function getQueryParam(query, key) {
const value = query[key];
if (Array.isArray(value)) return value[0] ?? void 0;
if (value === null) return void 0;
return value;
}
async function saveCurrentWorkflow({ id, name, tags } = {}, redirect = true, forceSave = false) {
if (useSourceControlStore().preferences.branchReadOnly) return false;
const isLoading = useCanvasStore().isLoading;
const currentWorkflow = id ?? getQueryParam(router.currentRoute.value.params, "name");
const parentFolderId = getQueryParam(router.currentRoute.value.query, "parentFolderId");
const uiContext = getQueryParam(router.currentRoute.value.query, "uiContext");
if (!currentWorkflow || ["new", "__EMPTY__"].includes(currentWorkflow)) return !!await saveAsNewWorkflow({
name,
tags,
parentFolderId,
uiContext
}, redirect);
try {
if (!forceSave && isLoading) return true;
uiStore.addActiveAction("workflowSaving");
const workflowDataRequest = await getWorkflowDataToSave();
if (workflowDataRequest.id !== currentWorkflow) throw new Error("Attempted to save a workflow different from the current workflow");
if (name) workflowDataRequest.name = name.trim();
if (tags) workflowDataRequest.tags = tags;
workflowDataRequest.versionId = workflowsStore.workflowVersionId;
const deactivateReason = await getWorkflowDeactivationInfo(currentWorkflow, workflowDataRequest);
if (deactivateReason !== void 0) {
workflowDataRequest.active = false;
if (workflowsStore.isWorkflowActive) {
toast.showMessage(deactivateReason);
workflowsStore.setWorkflowInactive(currentWorkflow);
}
}
const workflowData = await workflowsStore.updateWorkflow(currentWorkflow, workflowDataRequest, forceSave);
workflowsStore.setWorkflowVersionId(workflowData.versionId);
if (name) workflowState.setWorkflowName({
newName: workflowData.name,
setStateDirty: false
});
if (tags) {
const tagIds = (workflowData.tags || []).map((tag) => tag.id);
workflowState.setWorkflowTagIds(tagIds);
}
uiStore.stateIsDirty = false;
uiStore.removeActiveAction("workflowSaving");
useExternalHooks().run("workflow.afterUpdate", { workflowData });
return true;
} catch (error) {
console.error(error);
uiStore.removeActiveAction("workflowSaving");
if (error.errorCode === 100) {
telemetry.track("User attempted to save locked workflow", {
workflowId: currentWorkflow,
sharing_role: getWorkflowProjectRole(currentWorkflow)
});
const url = router.resolve({
name: VIEWS.WORKFLOW,
params: { name: currentWorkflow }
}).href;
if (await message.confirm(i18n.baseText("workflows.concurrentChanges.confirmMessage.message", { interpolate: { url } }), i18n.baseText("workflows.concurrentChanges.confirmMessage.title"), {
confirmButtonText: i18n.baseText("workflows.concurrentChanges.confirmMessage.confirmButtonText"),
cancelButtonText: i18n.baseText("workflows.concurrentChanges.confirmMessage.cancelButtonText")
}) === "confirm") return await saveCurrentWorkflow({
id,
name,
tags
}, redirect, true);
return false;
}
toast.showMessage({
title: i18n.baseText("workflowHelpers.showMessage.title"),
message: error.message,
type: "error"
});
return false;
}
}
async function saveAsNewWorkflow({ name, tags, resetWebhookUrls, resetNodeIds, openInNewWindow, parentFolderId, uiContext, data } = {}, redirect = true) {
try {
uiStore.addActiveAction("workflowSaving");
const workflowDataRequest = data || await getWorkflowDataToSave();
const changedNodes = {};
if (resetNodeIds) workflowDataRequest.nodes = workflowDataRequest.nodes.map((node) => {
nodeHelpers.assignNodeId(node);
return node;
});
if (resetWebhookUrls) workflowDataRequest.nodes = workflowDataRequest.nodes.map((node) => {
if (node.webhookId) {
const newId = nodeHelpers.assignWebhookId(node);
node.parameters.path = newId;
changedNodes[node.name] = node.webhookId;
}
return node;
});
if (name) workflowDataRequest.name = name.trim();
if (tags) workflowDataRequest.tags = tags;
if (parentFolderId) workflowDataRequest.parentFolderId = parentFolderId;
if (uiContext) workflowDataRequest.uiContext = uiContext;
const workflowData = await workflowsStore.createNewWorkflow(workflowDataRequest);
workflowsStore.addWorkflow(workflowData);
focusPanelStore.onNewWorkflowSave(workflowData.id);
if (openInNewWindow) {
const routeData = router.resolve({
name: VIEWS.WORKFLOW,
params: { name: workflowData.id }
});
window.open(routeData.href, "_blank");
uiStore.removeActiveAction("workflowSaving");
return workflowData.id;
}
if (workflowData.activeVersionId !== null) {
const conflict = await checkConflictingWebhooks(workflowData.id);
if (conflict) {
workflowData.active = false;
workflowData.activeVersionId = null;
toast.showMessage({
title: "Conflicting Webhook Path",
message: `Workflow set to inactive: Live webhook in another workflow uses same path as node '${conflict.trigger.name}'.`,
type: "error"
});
}
}
workflowState.setActive(workflowData.activeVersionId);
workflowState.setWorkflowId(workflowData.id);
workflowsStore.setWorkflowVersionId(workflowData.versionId);
workflowState.setWorkflowName({
newName: workflowData.name,
setStateDirty: false
});
workflowState.setWorkflowSettings(workflowData.settings || {});
uiStore.stateIsDirty = false;
Object.keys(changedNodes).forEach((nodeName) => {
const changes = {
key: "webhookId",
value: changedNodes[nodeName],
name: nodeName
};
workflowState.setNodeValue(changes);
});
const tagIds = (workflowData.tags || []).map((tag) => tag.id);
workflowState.setWorkflowTagIds(tagIds);
const templateId = router.currentRoute.value.query.templateId;
if (templateId) telemetry.track("User saved new workflow from template", {
template_id: tryToParseNumber(String(templateId)),
workflow_id: workflowData.id,
wf_template_repo_session_id: templatesStore.previousSessionId
});
if (redirect) await router.replace({
name: VIEWS.WORKFLOW,
params: { name: workflowData.id },
query: { action: "workflowSave" }
});
uiStore.removeActiveAction("workflowSaving");
uiStore.stateIsDirty = false;
useExternalHooks().run("workflow.afterUpdate", { workflowData });
return workflowData.id;
} catch (e) {
uiStore.removeActiveAction("workflowSaving");
toast.showMessage({
title: i18n.baseText("workflowHelpers.showMessage.title"),
message: e.message,
type: "error"
});
return null;
}
}
return {
promptSaveUnsavedWorkflowChanges,
saveCurrentWorkflow,
saveAsNewWorkflow
};
}
export { useWorkflowSaving as t };