chore: Move router usage out of useCanvasOperation and useWorkflowHelpers (no-changelog) (#16041)

This commit is contained in:
Charlie Kolb
2025-06-05 13:51:07 +02:00
committed by GitHub
parent 4a6bcffc70
commit 2724089078
37 changed files with 877 additions and 889 deletions

View File

@@ -1,10 +1,7 @@
import {
HTTP_REQUEST_NODE_TYPE,
MODAL_CONFIRM,
NON_ACTIVATABLE_TRIGGER_NODE_TYPES,
PLACEHOLDER_EMPTY_WORKFLOW_ID,
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
VIEWS,
} from '@/constants';
import type {
@@ -21,7 +18,6 @@ import type {
IRunExecutionData,
IWebhookDescription,
IWorkflowDataProxyAdditionalKeys,
IWorkflowSettings,
NodeParameterValue,
Workflow,
} from 'n8n-workflow';
@@ -32,19 +28,14 @@ import type {
INodeTypesMaxCount,
INodeUi,
ITag,
IUpdateInformation,
IWorkflowData,
IWorkflowDataCreate,
IWorkflowDataUpdate,
IWorkflowDb,
NotificationOptions,
TargetItem,
WorkflowTitleStatus,
XYPosition,
} from '@/Interface';
import { useMessage } from '@/composables/useMessage';
import { useToast } from '@/composables/useToast';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import get from 'lodash/get';
@@ -53,19 +44,12 @@ import { useEnvironmentsStore } from '@/stores/environments.ee.store';
import { useRootStore } from '@n8n/stores/useRootStore';
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useTemplatesStore } from '@/stores/templates.store';
import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { getSourceItems } from '@/utils/pairedItemUtils';
import { getCredentialTypeName, isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useCanvasStore } from '@/stores/canvas.store';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { tryToParseNumber } from '@/utils/typesUtils';
import { useI18n } from '@n8n/i18n';
import type { useRouter } from 'vue-router';
import { useTelemetry } from '@/composables/useTelemetry';
import { useProjectsStore } from '@/stores/projects.store';
import { useTagsStore } from '@/stores/tags.store';
import { useWorkflowsEEStore } from '@/stores/workflows.ee.store';
@@ -450,11 +434,9 @@ export function executeData(
return executeData;
}
export function useWorkflowHelpers(options: { router: ReturnType<typeof useRouter> }) {
const router = options.router;
export function useWorkflowHelpers() {
const nodeTypesStore = useNodeTypesStore();
const rootStore = useRootStore();
const templatesStore = useTemplatesStore();
const workflowsStore = useWorkflowsStore();
const workflowsEEStore = useWorkflowsEEStore();
const uiStore = useUIStore();
@@ -462,10 +444,7 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
const projectsStore = useProjectsStore();
const tagsStore = useTagsStore();
const toast = useToast();
const message = useMessage();
const i18n = useI18n();
const telemetry = useTelemetry();
const documentTitle = useDocumentTitle();
const setDocumentTitle = (workflowName: string, status: WorkflowTitleStatus) => {
@@ -814,319 +793,6 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
}
}
function isNodeActivatable(node: INode): boolean {
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: string,
request: IWorkflowDataUpdate,
): Promise<Partial<NotificationOptions> | undefined> {
const missingActivatableTriggerNode =
request.nodes !== undefined && !request.nodes.some(isNodeActivatable);
if (missingActivatableTriggerNode) {
// Automatically deactivate if all activatable triggers are removed
return {
title: i18n.baseText('workflows.deactivated'),
message: i18n.baseText('workflowActivator.thisWorkflowHasNoTriggerNodes'),
type: 'info',
};
}
const conflictData = await checkConflictingWebhooks(workflowId);
if (conflictData) {
// Workflow should not be active if there is live webhook with the same path
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',
};
}
return undefined;
}
async function saveCurrentWorkflow(
{ id, name, tags }: { id?: string; name?: string; tags?: string[] } = {},
redirect = true,
forceSave = false,
): Promise<boolean> {
const readOnlyEnv = useSourceControlStore().preferences.branchReadOnly;
if (readOnlyEnv) {
return false;
}
const isLoading = useCanvasStore().isLoading;
const currentWorkflow = id || (router.currentRoute.value.params.name as string);
const parentFolderId = router.currentRoute.value.query.parentFolderId as string;
if (!currentWorkflow || ['new', PLACEHOLDER_EMPTY_WORKFLOW_ID].includes(currentWorkflow)) {
return await saveAsNewWorkflow({ name, tags, parentFolderId }, redirect);
}
// Workflow exists already so update it
try {
if (!forceSave && isLoading) {
return true;
}
uiStore.addActiveAction('workflowSaving');
const workflowDataRequest: IWorkflowDataUpdate = await getWorkflowDataToSave();
// This can happen if the user has another workflow in the browser history and navigates
// via the browser back button, encountering our warning dialog with the new route already set
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 !== undefined) {
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) {
workflowsStore.setWorkflowName({ newName: workflowData.name, setStateDirty: false });
}
if (tags) {
const createdTags = (workflowData.tags || []) as ITag[];
const tagIds = createdTags.map((tag: ITag): string => tag.id);
workflowsStore.setWorkflowTagIds(tagIds);
}
uiStore.stateIsDirty = false;
uiStore.removeActiveAction('workflowSaving');
void 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;
const overwrite = 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',
),
},
);
if (overwrite === MODAL_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,
data,
}: {
name?: string;
tags?: string[];
resetWebhookUrls?: boolean;
openInNewWindow?: boolean;
resetNodeIds?: boolean;
parentFolderId?: string;
data?: IWorkflowDataCreate;
} = {},
redirect = true,
): Promise<boolean> {
try {
uiStore.addActiveAction('workflowSaving');
const workflowDataRequest: IWorkflowDataCreate = data || (await getWorkflowDataToSave());
const changedNodes = {} as IDataObject;
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;
}
const workflowData = await workflowsStore.createNewWorkflow(workflowDataRequest);
workflowsStore.addWorkflow(workflowData);
if (openInNewWindow) {
const routeData = router.resolve({
name: VIEWS.WORKFLOW,
params: { name: workflowData.id },
});
window.open(routeData.href, '_blank');
uiStore.removeActiveAction('workflowSaving');
return true;
}
// workflow should not be active if there is live webhook with the same path
if (workflowData.active) {
const conflict = await checkConflictingWebhooks(workflowData.id);
if (conflict) {
workflowData.active = false;
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',
});
}
}
workflowsStore.setActive(workflowData.active || false);
workflowsStore.setWorkflowId(workflowData.id);
workflowsStore.setWorkflowVersionId(workflowData.versionId);
workflowsStore.setWorkflowName({ newName: workflowData.name, setStateDirty: false });
workflowsStore.setWorkflowSettings((workflowData.settings as IWorkflowSettings) || {});
uiStore.stateIsDirty = false;
Object.keys(changedNodes).forEach((nodeName) => {
const changes = {
key: 'webhookId',
value: changedNodes[nodeName],
name: nodeName,
} as IUpdateInformation;
workflowsStore.setNodeValue(changes);
});
const createdTags = (workflowData.tags || []) as ITag[];
const tagIds = createdTags.map((tag: ITag): string => tag.id);
workflowsStore.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;
void useExternalHooks().run('workflow.afterUpdate', { workflowData });
getCurrentWorkflow(true); // refresh cache
return true;
} catch (e) {
uiStore.removeActiveAction('workflowSaving');
toast.showMessage({
title: i18n.baseText('workflowHelpers.showMessage.title'),
message: (e as Error).message,
type: 'error',
});
return false;
}
}
// Updates the position of all the nodes that the top-left node
// is at the given position
function updateNodePositions(
@@ -1292,8 +958,6 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
getWebhookUrl,
resolveExpression,
updateWorkflow,
saveCurrentWorkflow,
saveAsNewWorkflow,
updateNodePositions,
removeForeignCredentialsFromWorkflow,
getWorkflowProjectRole,