mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
chore: Move router usage out of useCanvasOperation and useWorkflowHelpers (no-changelog) (#16041)
This commit is contained in:
@@ -5,7 +5,7 @@ import LogsPanel from '@/components/CanvasChat/future/LogsPanel.vue';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { createTestingPinia, type TestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { createRouter, createWebHistory, useRouter } from 'vue-router';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { h, nextTick } from 'vue';
|
||||
import {
|
||||
@@ -281,8 +281,7 @@ describe('LogsPanel', () => {
|
||||
});
|
||||
|
||||
it('should still show logs for a removed node', async () => {
|
||||
const router = useRouter();
|
||||
const operations = useCanvasOperations({ router });
|
||||
const operations = useCanvasOperations();
|
||||
|
||||
logsStore.toggleOpen(true);
|
||||
workflowsStore.setWorkflow(deepCopy(aiChatWorkflow));
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useWorkflowSaving } from '@/composables/useWorkflowSaving';
|
||||
|
||||
const props = defineProps<{
|
||||
modalName: string;
|
||||
@@ -27,7 +28,8 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowSaving = useWorkflowSaving({ router });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const { showMessage, showError } = useToast();
|
||||
const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
@@ -103,7 +105,7 @@ const save = async (): Promise<void> => {
|
||||
);
|
||||
}
|
||||
|
||||
const saved = await workflowHelpers.saveAsNewWorkflow({
|
||||
const saved = await workflowSaving.saveAsNewWorkflow({
|
||||
name: workflowName,
|
||||
data: workflowToUpdate,
|
||||
tags: currentTagIds.value,
|
||||
|
||||
@@ -59,6 +59,7 @@ import { useFoldersStore } from '@/stores/folders.store';
|
||||
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
|
||||
import { type BaseTextKey, useI18n } from '@n8n/i18n';
|
||||
import { ProjectTypes } from '@/types/projects.types';
|
||||
import { useWorkflowSaving } from '@/composables/useWorkflowSaving';
|
||||
|
||||
const props = defineProps<{
|
||||
readOnly?: boolean;
|
||||
@@ -95,7 +96,8 @@ const telemetry = useTelemetry();
|
||||
const message = useMessage();
|
||||
const toast = useToast();
|
||||
const documentTitle = useDocumentTitle();
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowSaving = useWorkflowSaving({ router });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const pageRedirectionHelper = usePageRedirectionHelper();
|
||||
|
||||
const isTagsEditEnabled = ref(false);
|
||||
@@ -290,7 +292,7 @@ async function onSaveButtonClick() {
|
||||
const name = props.name;
|
||||
const tags = props.tags as string[];
|
||||
|
||||
const saved = await workflowHelpers.saveCurrentWorkflow({
|
||||
const saved = await workflowSaving.saveCurrentWorkflow({
|
||||
id,
|
||||
name,
|
||||
tags,
|
||||
@@ -347,7 +349,7 @@ async function onTagsBlur() {
|
||||
}
|
||||
tagsSaving.value = true;
|
||||
|
||||
const saved = await workflowHelpers.saveCurrentWorkflow({ tags });
|
||||
const saved = await workflowSaving.saveCurrentWorkflow({ tags });
|
||||
telemetry.track('User edited workflow tags', {
|
||||
workflow_id: props.id,
|
||||
new_tag_count: tags.length,
|
||||
@@ -390,7 +392,7 @@ async function onNameSubmit(name: string) {
|
||||
|
||||
uiStore.addActiveAction('workflowSaving');
|
||||
const id = getWorkflowId();
|
||||
const saved = await workflowHelpers.saveCurrentWorkflow({ name });
|
||||
const saved = await workflowSaving.saveCurrentWorkflow({ name });
|
||||
if (saved) {
|
||||
showCreateWorkflowSuccessToast(id);
|
||||
workflowHelpers.setDocumentTitle(newName, 'IDLE');
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
} from '@/constants';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
@@ -21,9 +20,8 @@ const props = defineProps<{
|
||||
nodeTypeDescription: INodeTypeDescription | null;
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
const clipboard = useClipboard();
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const toast = useToast();
|
||||
const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
@@ -64,7 +64,6 @@ import { isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
|
||||
import { N8nIcon, N8nInput, N8nInputNumber, N8nOption, N8nSelect } from '@n8n/design-system';
|
||||
import type { EventBus } from '@n8n/utils/event-bus';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
import { captureMessage } from '@sentry/vue';
|
||||
import { completeExpressionSyntax, shouldConvertToExpression } from '@/utils/expressions';
|
||||
@@ -124,8 +123,7 @@ const externalHooks = useExternalHooks();
|
||||
const i18n = useI18n();
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const { debounce } = useDebounce();
|
||||
const router = useRouter();
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
const credentialsStore = useCredentialsStore();
|
||||
|
||||
@@ -39,7 +39,6 @@ import { computedWithControl } from '@vueuse/core';
|
||||
import get from 'lodash/get';
|
||||
import set from 'lodash/set';
|
||||
import { N8nIcon, N8nIconButton, N8nInputLabel, N8nNotice, N8nText } from '@n8n/design-system';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
const LazyFixedCollectionParameter = defineAsyncComponent(
|
||||
@@ -75,8 +74,7 @@ const ndvStore = useNDVStore();
|
||||
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const asyncLoadingError = ref(false);
|
||||
const router = useRouter();
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const i18n = useI18n();
|
||||
|
||||
const { activeNode } = storeToRefs(ndvStore);
|
||||
|
||||
@@ -43,7 +43,6 @@ import {
|
||||
useCssModule,
|
||||
watch,
|
||||
} from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import ResourceLocatorDropdown from './ResourceLocatorDropdown.vue';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { onClickOutside, type VueInstance } from '@vueuse/core';
|
||||
@@ -124,8 +123,7 @@ const emit = defineEmits<{
|
||||
modalOpenerClick: [];
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const { callDebounced } = useDebounce();
|
||||
const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
@@ -8,7 +8,6 @@ import { renderComponent } from '@/__tests__/render';
|
||||
import { waitFor } from '@testing-library/vue';
|
||||
import { userEvent } from '@testing-library/user-event';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
|
||||
@@ -40,7 +39,7 @@ const nodes = [
|
||||
const mockResolveExpression = () => {
|
||||
const mock = vi.fn();
|
||||
vi.spyOn(workflowHelpers, 'useWorkflowHelpers').mockReturnValueOnce({
|
||||
...workflowHelpers.useWorkflowHelpers({ router: useRouter() }),
|
||||
...workflowHelpers.useWorkflowHelpers(),
|
||||
resolveExpression: mock,
|
||||
});
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ const workflowsStore = useWorkflowsStore();
|
||||
const ndvStore = useNDVStore();
|
||||
|
||||
const router = useRouter();
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ import { OPEN_AI_API_CREDENTIAL_TYPE } from 'n8n-workflow';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
|
||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const props = defineProps<{
|
||||
isArchived: boolean;
|
||||
@@ -37,8 +36,7 @@ const workflowActivate = useWorkflowActivate();
|
||||
|
||||
const uiStore = useUIStore();
|
||||
|
||||
const router = useRouter();
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
|
||||
const i18n = useI18n();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useVueFlow } from '@vue-flow/core';
|
||||
import { useDebounce } from '@vueuse/core';
|
||||
import { computed, type ComputedRef } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
export function useNodeSettingsInCanvas(): ComputedRef<number | undefined> {
|
||||
const settingsStore = useSettingsStore();
|
||||
@@ -15,8 +14,7 @@ export function useNodeSettingsInCanvas(): ComputedRef<number | undefined> {
|
||||
return computed(() => undefined);
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
const { editableWorkflow } = useCanvasOperations({ router });
|
||||
const { editableWorkflow } = useCanvasOperations();
|
||||
const viewFlow = useVueFlow({ id: editableWorkflow.value.id });
|
||||
const zoom = computed(() => viewFlow.viewport.value.zoom);
|
||||
const debouncedZoom = useDebounce(zoom, 100);
|
||||
|
||||
@@ -39,7 +39,7 @@ const i18n = useI18n();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const logsStore = useLogsStore();
|
||||
const { runEntireWorkflow } = useRunWorkflow({ router });
|
||||
const { startChat } = useCanvasOperations({ router });
|
||||
const { startChat } = useCanvasOperations();
|
||||
|
||||
const isChatOpen = computed(() => logsStore.isOpen);
|
||||
const isExecuting = computed(() => workflowsStore.isWorkflowRunning);
|
||||
|
||||
@@ -7,9 +7,9 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { PLACEHOLDER_EMPTY_WORKFLOW_ID, WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants';
|
||||
import type { IWorkflowSettings } from 'n8n-workflow';
|
||||
import { deepCopy } from 'n8n-workflow';
|
||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useWorkflowSaving } from '@/composables/useWorkflowSaving';
|
||||
|
||||
interface IWorkflowSaveSettings {
|
||||
saveFailedExecutions: boolean;
|
||||
@@ -28,7 +28,7 @@ const props = withDefaults(
|
||||
|
||||
const i18n = useI18n();
|
||||
const router = useRouter();
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowSaving = useWorkflowSaving({ router });
|
||||
const locale = useI18n();
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
@@ -177,7 +177,7 @@ async function onSaveWorkflowClick(): Promise<void> {
|
||||
if (!currentId) {
|
||||
return;
|
||||
}
|
||||
const saved = await workflowHelpers.saveCurrentWorkflow({
|
||||
const saved = await workflowSaving.saveCurrentWorkflow({
|
||||
id: currentId,
|
||||
name: workflowName.value,
|
||||
tags: currentWorkflowTagIds.value,
|
||||
|
||||
@@ -8,7 +8,6 @@ import type {
|
||||
INode,
|
||||
} from 'n8n-workflow';
|
||||
import { useWorkflowHelpers } from './useWorkflowHelpers';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { executionDataToJson, getMainAuthField, getNodeAuthOptions } from '@/utils/nodeTypesUtils';
|
||||
@@ -30,7 +29,7 @@ export const useAIAssistantHelpers = () => {
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
|
||||
const workflowHelpers = useWorkflowHelpers({ router: useRouter() });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const locale = useI18n();
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
mockNode,
|
||||
mockNodeTypeDescription,
|
||||
} from '@/__tests__/mocks';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
@@ -55,14 +54,6 @@ import type { CanvasLayoutEvent } from './useCanvasLayout';
|
||||
import { useTelemetry } from './useTelemetry';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
|
||||
vi.mock('vue-router', async (importOriginal) => {
|
||||
const actual = await importOriginal<{}>();
|
||||
return {
|
||||
...actual,
|
||||
useRouter: () => ({}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('n8n-workflow', async (importOriginal) => {
|
||||
const actual = await importOriginal<{}>();
|
||||
return {
|
||||
@@ -105,8 +96,6 @@ vi.mock('@/composables/useToast', () => {
|
||||
});
|
||||
|
||||
describe('useCanvasOperations', () => {
|
||||
const router = useRouter();
|
||||
|
||||
const workflowId = 'test';
|
||||
const initialState = {
|
||||
[STORES.NODE_TYPES]: {},
|
||||
@@ -144,7 +133,7 @@ describe('useCanvasOperations', () => {
|
||||
|
||||
nodeTypesStore.nodeTypes = { [type]: { [version]: expectedDescription } };
|
||||
|
||||
const { requireNodeTypeDescription } = useCanvasOperations({ router });
|
||||
const { requireNodeTypeDescription } = useCanvasOperations();
|
||||
const result = requireNodeTypeDescription(type, version);
|
||||
|
||||
expect(result).toBe(expectedDescription);
|
||||
@@ -157,7 +146,7 @@ describe('useCanvasOperations', () => {
|
||||
|
||||
nodeTypesStore.nodeTypes = { [type]: { 2: expectedDescription } };
|
||||
|
||||
const { requireNodeTypeDescription } = useCanvasOperations({ router });
|
||||
const { requireNodeTypeDescription } = useCanvasOperations();
|
||||
const result = requireNodeTypeDescription(type);
|
||||
|
||||
expect(result).toBe(expectedDescription);
|
||||
@@ -166,7 +155,7 @@ describe('useCanvasOperations', () => {
|
||||
it("should return placeholder node type description if node type doesn't exist", () => {
|
||||
const type = 'nonexistentType';
|
||||
|
||||
const { requireNodeTypeDescription } = useCanvasOperations({ router });
|
||||
const { requireNodeTypeDescription } = useCanvasOperations();
|
||||
const result = requireNodeTypeDescription(type);
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -185,7 +174,7 @@ describe('useCanvasOperations', () => {
|
||||
|
||||
describe('addNode', () => {
|
||||
it('should create node with default version when version is undefined', () => {
|
||||
const { addNode } = useCanvasOperations({ router });
|
||||
const { addNode } = useCanvasOperations();
|
||||
const result = addNode(
|
||||
{
|
||||
name: 'example',
|
||||
@@ -199,7 +188,7 @@ describe('useCanvasOperations', () => {
|
||||
});
|
||||
|
||||
it('should create node with default position when position is not provided', () => {
|
||||
const { addNode } = useCanvasOperations({ router });
|
||||
const { addNode } = useCanvasOperations();
|
||||
const result = addNode(
|
||||
{
|
||||
type: 'type',
|
||||
@@ -212,7 +201,7 @@ describe('useCanvasOperations', () => {
|
||||
});
|
||||
|
||||
it('should create node with provided position when position is provided', () => {
|
||||
const { addNode } = useCanvasOperations({ router });
|
||||
const { addNode } = useCanvasOperations();
|
||||
const result = addNode(
|
||||
{
|
||||
type: 'type',
|
||||
@@ -241,7 +230,7 @@ describe('useCanvasOperations', () => {
|
||||
credentialB,
|
||||
]);
|
||||
|
||||
const { addNode } = useCanvasOperations({ router });
|
||||
const { addNode } = useCanvasOperations();
|
||||
const result = addNode(
|
||||
{
|
||||
type: 'type',
|
||||
@@ -256,7 +245,7 @@ describe('useCanvasOperations', () => {
|
||||
const ndvStore = useNDVStore();
|
||||
const nodeTypeDescription = mockNodeTypeDescription({ name: 'type' });
|
||||
|
||||
const { addNode } = useCanvasOperations({ router });
|
||||
const { addNode } = useCanvasOperations();
|
||||
addNode(
|
||||
{
|
||||
type: 'type',
|
||||
@@ -274,7 +263,7 @@ describe('useCanvasOperations', () => {
|
||||
const ndvStore = useNDVStore();
|
||||
const nodeTypeDescription = mockNodeTypeDescription({ name: STICKY_NODE_TYPE });
|
||||
|
||||
const { addNode } = useCanvasOperations({ router });
|
||||
const { addNode } = useCanvasOperations();
|
||||
addNode(
|
||||
{
|
||||
type: STICKY_NODE_TYPE,
|
||||
@@ -294,7 +283,7 @@ describe('useCanvasOperations', () => {
|
||||
const node = createTestNode({ position: [100, 100] });
|
||||
const nodeTypeDescription = mockNodeTypeDescription();
|
||||
|
||||
const { resolveNodePosition } = useCanvasOperations({ router });
|
||||
const { resolveNodePosition } = useCanvasOperations();
|
||||
const position = resolveNodePosition(node, nodeTypeDescription);
|
||||
|
||||
expect(position).toEqual([100, 100]);
|
||||
@@ -316,7 +305,7 @@ describe('useCanvasOperations', () => {
|
||||
uiStore.lastInteractedWithNodeHandle = 'inputs/main/0';
|
||||
uiStore.lastCancelledConnectionPosition = [200, 200];
|
||||
|
||||
const { resolveNodePosition } = useCanvasOperations({ router });
|
||||
const { resolveNodePosition } = useCanvasOperations();
|
||||
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
|
||||
|
||||
expect(position).toEqual([200, 160]);
|
||||
@@ -341,7 +330,7 @@ describe('useCanvasOperations', () => {
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
workflowObject.getNode = vi.fn().mockReturnValue(node);
|
||||
|
||||
const { resolveNodePosition } = useCanvasOperations({ router });
|
||||
const { resolveNodePosition } = useCanvasOperations();
|
||||
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
|
||||
|
||||
expect(position).toEqual([320, 100]);
|
||||
@@ -372,7 +361,7 @@ describe('useCanvasOperations', () => {
|
||||
.mockReturnValueOnce([NodeConnectionTypes.AiTool])
|
||||
.mockReturnValueOnce([NodeConnectionTypes.AiTool]);
|
||||
|
||||
const { resolveNodePosition } = useCanvasOperations({ router });
|
||||
const { resolveNodePosition } = useCanvasOperations();
|
||||
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
|
||||
|
||||
expect(position).toEqual([460, 100]);
|
||||
@@ -388,7 +377,7 @@ describe('useCanvasOperations', () => {
|
||||
createTestNode({ id: 'trigger', position: [100, 100] }),
|
||||
];
|
||||
|
||||
const { resolveNodePosition, lastClickPosition } = useCanvasOperations({ router });
|
||||
const { resolveNodePosition, lastClickPosition } = useCanvasOperations();
|
||||
lastClickPosition.value = [300, 300];
|
||||
|
||||
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
|
||||
@@ -400,7 +389,7 @@ describe('useCanvasOperations', () => {
|
||||
const node = createTestNode({ id: '0' });
|
||||
const nodeTypeDescription = mockNodeTypeDescription();
|
||||
|
||||
const { resolveNodePosition } = useCanvasOperations({ router });
|
||||
const { resolveNodePosition } = useCanvasOperations();
|
||||
const position = resolveNodePosition({ ...node, position: undefined }, nodeTypeDescription);
|
||||
|
||||
expect(position).toEqual([0, 0]);
|
||||
@@ -417,7 +406,7 @@ describe('useCanvasOperations', () => {
|
||||
const startRecordingUndoSpy = vi.spyOn(historyStore, 'startRecordingUndo');
|
||||
const stopRecordingUndoSpy = vi.spyOn(historyStore, 'stopRecordingUndo');
|
||||
|
||||
const { updateNodesPosition } = useCanvasOperations({ router });
|
||||
const { updateNodesPosition } = useCanvasOperations();
|
||||
updateNodesPosition(events, { trackHistory: true, trackBulk: true });
|
||||
|
||||
expect(startRecordingUndoSpy).toHaveBeenCalled();
|
||||
@@ -445,7 +434,7 @@ describe('useCanvasOperations', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const { updateNodesPosition } = useCanvasOperations({ router });
|
||||
const { updateNodesPosition } = useCanvasOperations();
|
||||
updateNodesPosition(events);
|
||||
|
||||
expect(setNodePositionByIdSpy).toHaveBeenCalledTimes(2);
|
||||
@@ -459,7 +448,7 @@ describe('useCanvasOperations', () => {
|
||||
const startRecordingUndoSpy = vi.spyOn(historyStore, 'startRecordingUndo');
|
||||
const stopRecordingUndoSpy = vi.spyOn(historyStore, 'stopRecordingUndo');
|
||||
|
||||
const { updateNodesPosition } = useCanvasOperations({ router });
|
||||
const { updateNodesPosition } = useCanvasOperations();
|
||||
updateNodesPosition(events, { trackHistory: false, trackBulk: false });
|
||||
|
||||
expect(startRecordingUndoSpy).not.toHaveBeenCalled();
|
||||
@@ -484,7 +473,7 @@ describe('useCanvasOperations', () => {
|
||||
const startRecordingUndoSpy = vi.spyOn(historyStore, 'startRecordingUndo');
|
||||
const stopRecordingUndoSpy = vi.spyOn(historyStore, 'stopRecordingUndo');
|
||||
|
||||
const { tidyUp } = useCanvasOperations({ router });
|
||||
const { tidyUp } = useCanvasOperations();
|
||||
tidyUp(event);
|
||||
|
||||
expect(startRecordingUndoSpy).toHaveBeenCalled();
|
||||
@@ -519,7 +508,7 @@ describe('useCanvasOperations', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const { tidyUp } = useCanvasOperations({ router });
|
||||
const { tidyUp } = useCanvasOperations();
|
||||
tidyUp(event);
|
||||
|
||||
expect(setNodePositionByIdSpy).toHaveBeenCalledTimes(2);
|
||||
@@ -540,7 +529,7 @@ describe('useCanvasOperations', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const { tidyUp } = useCanvasOperations({ router });
|
||||
const { tidyUp } = useCanvasOperations();
|
||||
tidyUp(event);
|
||||
|
||||
expect(useTelemetry().track).toHaveBeenCalledWith(
|
||||
@@ -569,7 +558,7 @@ describe('useCanvasOperations', () => {
|
||||
|
||||
workflowsStore.getNodeById.mockReturnValueOnce(node);
|
||||
|
||||
const { updateNodePosition } = useCanvasOperations({ router });
|
||||
const { updateNodePosition } = useCanvasOperations();
|
||||
updateNodePosition(id, position);
|
||||
|
||||
expect(workflowsStore.setNodePositionById).toHaveBeenCalledWith(id, [position.x, position.y]);
|
||||
@@ -585,7 +574,7 @@ describe('useCanvasOperations', () => {
|
||||
workflowsStore.getNodeById = vi.fn().mockReturnValue({ name: nodeName });
|
||||
uiStore.lastSelectedNode = '';
|
||||
|
||||
const { setNodeSelected } = useCanvasOperations({ router });
|
||||
const { setNodeSelected } = useCanvasOperations();
|
||||
setNodeSelected(nodeId);
|
||||
|
||||
expect(uiStore.lastSelectedNode).toBe(nodeName);
|
||||
@@ -598,7 +587,7 @@ describe('useCanvasOperations', () => {
|
||||
workflowsStore.getNodeById = vi.fn().mockReturnValue(undefined);
|
||||
uiStore.lastSelectedNode = 'Existing Node';
|
||||
|
||||
const { setNodeSelected } = useCanvasOperations({ router });
|
||||
const { setNodeSelected } = useCanvasOperations();
|
||||
setNodeSelected(nodeId);
|
||||
|
||||
expect(uiStore.lastSelectedNode).toBe('Existing Node');
|
||||
@@ -608,7 +597,7 @@ describe('useCanvasOperations', () => {
|
||||
const uiStore = useUIStore();
|
||||
uiStore.lastSelectedNode = 'Existing Node';
|
||||
|
||||
const { setNodeSelected } = useCanvasOperations({ router });
|
||||
const { setNodeSelected } = useCanvasOperations();
|
||||
setNodeSelected();
|
||||
|
||||
expect(uiStore.lastSelectedNode).toBe('');
|
||||
@@ -633,7 +622,7 @@ describe('useCanvasOperations', () => {
|
||||
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
|
||||
};
|
||||
|
||||
const { addNodes } = useCanvasOperations({ router });
|
||||
const { addNodes } = useCanvasOperations();
|
||||
await addNodes(nodes, {});
|
||||
|
||||
expect(workflowsStore.addNode).toHaveBeenCalledTimes(2);
|
||||
@@ -670,7 +659,7 @@ describe('useCanvasOperations', () => {
|
||||
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
|
||||
};
|
||||
|
||||
const { addNodes } = useCanvasOperations({ router });
|
||||
const { addNodes } = useCanvasOperations();
|
||||
await addNodes(nodes, { position: [50, 60] });
|
||||
|
||||
expect(workflowsStore.addNode).toHaveBeenCalledTimes(2);
|
||||
@@ -711,7 +700,7 @@ describe('useCanvasOperations', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const { addNodes } = useCanvasOperations({ router });
|
||||
const { addNodes } = useCanvasOperations();
|
||||
await addNodes(nodes, {});
|
||||
|
||||
expect(setNodePositionByIdSpy).toHaveBeenCalledTimes(2);
|
||||
@@ -736,7 +725,7 @@ describe('useCanvasOperations', () => {
|
||||
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
|
||||
};
|
||||
|
||||
const { addNodes } = useCanvasOperations({ router });
|
||||
const { addNodes } = useCanvasOperations();
|
||||
const added = await addNodes(nodes, {});
|
||||
expect(added.length).toBe(2);
|
||||
});
|
||||
@@ -756,7 +745,7 @@ describe('useCanvasOperations', () => {
|
||||
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
|
||||
};
|
||||
|
||||
const { addNodes } = useCanvasOperations({ router });
|
||||
const { addNodes } = useCanvasOperations();
|
||||
await addNodes(nodes, { keepPristine: false });
|
||||
|
||||
expect(uiStore.stateIsDirty).toEqual(true);
|
||||
@@ -777,7 +766,7 @@ describe('useCanvasOperations', () => {
|
||||
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
|
||||
};
|
||||
|
||||
const { addNodes } = useCanvasOperations({ router });
|
||||
const { addNodes } = useCanvasOperations();
|
||||
await addNodes(nodes, { keepPristine: true });
|
||||
|
||||
expect(uiStore.stateIsDirty).toEqual(false);
|
||||
@@ -792,7 +781,7 @@ describe('useCanvasOperations', () => {
|
||||
workflowsStore.getNodeById.mockReturnValueOnce(node);
|
||||
const removeNodeByIdSpy = vi.spyOn(workflowsStore, 'removeNodeById');
|
||||
|
||||
const { revertAddNode } = useCanvasOperations({ router });
|
||||
const { revertAddNode } = useCanvasOperations();
|
||||
await revertAddNode(node.name);
|
||||
|
||||
expect(removeNodeByIdSpy).toHaveBeenCalledWith(node.id);
|
||||
@@ -818,7 +807,7 @@ describe('useCanvasOperations', () => {
|
||||
|
||||
workflowsStore.getNodeById.mockReturnValue(node);
|
||||
|
||||
const { deleteNode } = useCanvasOperations({ router });
|
||||
const { deleteNode } = useCanvasOperations();
|
||||
deleteNode(id, { trackHistory: true });
|
||||
|
||||
expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(id);
|
||||
@@ -847,7 +836,7 @@ describe('useCanvasOperations', () => {
|
||||
|
||||
workflowsStore.getNodeById.mockReturnValue(node);
|
||||
|
||||
const { deleteNode } = useCanvasOperations({ router });
|
||||
const { deleteNode } = useCanvasOperations();
|
||||
deleteNode(id, { trackHistory: false });
|
||||
|
||||
expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(id);
|
||||
@@ -916,7 +905,7 @@ describe('useCanvasOperations', () => {
|
||||
|
||||
workflowsStore.getNodeById.mockReturnValue(nodes[1]);
|
||||
|
||||
const { deleteNode } = useCanvasOperations({ router });
|
||||
const { deleteNode } = useCanvasOperations();
|
||||
deleteNode(nodes[1].id);
|
||||
|
||||
expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(nodes[1].id);
|
||||
@@ -992,7 +981,7 @@ describe('useCanvasOperations', () => {
|
||||
|
||||
workflowsStore.getNodeById.mockReturnValue(nodes[1]);
|
||||
|
||||
const { deleteNode } = useCanvasOperations({ router });
|
||||
const { deleteNode } = useCanvasOperations();
|
||||
deleteNode(nodes[1].id);
|
||||
|
||||
expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(nodes[1].id);
|
||||
@@ -1013,7 +1002,7 @@ describe('useCanvasOperations', () => {
|
||||
parameters: {},
|
||||
});
|
||||
|
||||
const { revertDeleteNode } = useCanvasOperations({ router });
|
||||
const { revertDeleteNode } = useCanvasOperations();
|
||||
revertDeleteNode(node);
|
||||
|
||||
expect(workflowsStore.addNode).toHaveBeenCalledWith(node);
|
||||
@@ -1033,7 +1022,7 @@ describe('useCanvasOperations', () => {
|
||||
workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: oldName });
|
||||
ndvStore.activeNodeName = oldName;
|
||||
|
||||
const { renameNode } = useCanvasOperations({ router });
|
||||
const { renameNode } = useCanvasOperations();
|
||||
await renameNode(oldName, newName);
|
||||
|
||||
expect(workflowObject.renameNode).toHaveBeenCalledWith(oldName, newName);
|
||||
@@ -1047,7 +1036,7 @@ describe('useCanvasOperations', () => {
|
||||
workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: oldName });
|
||||
ndvStore.activeNodeName = oldName;
|
||||
|
||||
const { renameNode } = useCanvasOperations({ router });
|
||||
const { renameNode } = useCanvasOperations();
|
||||
await renameNode(oldName, oldName);
|
||||
|
||||
expect(ndvStore.activeNodeName).toBe(oldName);
|
||||
@@ -1067,7 +1056,7 @@ describe('useCanvasOperations', () => {
|
||||
workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: currentName });
|
||||
ndvStore.activeNodeName = currentName;
|
||||
|
||||
const { revertRenameNode } = useCanvasOperations({ router });
|
||||
const { revertRenameNode } = useCanvasOperations();
|
||||
await revertRenameNode(currentName, oldName);
|
||||
|
||||
expect(ndvStore.activeNodeName).toBe(oldName);
|
||||
@@ -1080,7 +1069,7 @@ describe('useCanvasOperations', () => {
|
||||
workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: oldName });
|
||||
ndvStore.activeNodeName = oldName;
|
||||
|
||||
const { revertRenameNode } = useCanvasOperations({ router });
|
||||
const { revertRenameNode } = useCanvasOperations();
|
||||
await revertRenameNode(oldName, oldName);
|
||||
|
||||
expect(ndvStore.activeNodeName).toBe(oldName);
|
||||
@@ -1096,7 +1085,7 @@ describe('useCanvasOperations', () => {
|
||||
workflowsStore.getNodeById = vi.fn().mockReturnValue({ name: nodeName });
|
||||
ndvStore.activeNodeName = '';
|
||||
|
||||
const { setNodeActive } = useCanvasOperations({ router });
|
||||
const { setNodeActive } = useCanvasOperations();
|
||||
setNodeActive(nodeId);
|
||||
|
||||
expect(ndvStore.activeNodeName).toBe(nodeName);
|
||||
@@ -1109,7 +1098,7 @@ describe('useCanvasOperations', () => {
|
||||
workflowsStore.getNodeById = vi.fn().mockReturnValue(undefined);
|
||||
ndvStore.activeNodeName = 'Existing Node';
|
||||
|
||||
const { setNodeActive } = useCanvasOperations({ router });
|
||||
const { setNodeActive } = useCanvasOperations();
|
||||
setNodeActive(nodeId);
|
||||
|
||||
expect(ndvStore.activeNodeName).toBe('Existing Node');
|
||||
@@ -1121,7 +1110,7 @@ describe('useCanvasOperations', () => {
|
||||
|
||||
workflowsStore.getNodeById.mockImplementation(() => node);
|
||||
|
||||
const { setNodeActive } = useCanvasOperations({ router });
|
||||
const { setNodeActive } = useCanvasOperations();
|
||||
setNodeActive(node.id);
|
||||
|
||||
expect(workflowsStore.setNodePristine).toHaveBeenCalledWith(node.name, false);
|
||||
@@ -1134,7 +1123,7 @@ describe('useCanvasOperations', () => {
|
||||
const nodeName = 'Node 1';
|
||||
ndvStore.activeNodeName = '';
|
||||
|
||||
const { setNodeActiveByName } = useCanvasOperations({ router });
|
||||
const { setNodeActiveByName } = useCanvasOperations();
|
||||
setNodeActiveByName(nodeName);
|
||||
|
||||
expect(ndvStore.activeNodeName).toBe(nodeName);
|
||||
@@ -1150,7 +1139,7 @@ describe('useCanvasOperations', () => {
|
||||
];
|
||||
workflowsStore.getNodesByIds.mockReturnValue(nodes);
|
||||
|
||||
const { toggleNodesDisabled } = useCanvasOperations({ router });
|
||||
const { toggleNodesDisabled } = useCanvasOperations();
|
||||
toggleNodesDisabled([nodes[0].id, nodes[1].id], {
|
||||
trackHistory: true,
|
||||
trackBulk: true,
|
||||
@@ -1173,7 +1162,7 @@ describe('useCanvasOperations', () => {
|
||||
workflowsStore.getNodeByName.mockReturnValue(node);
|
||||
const updateNodePropertiesSpy = vi.spyOn(workflowsStore, 'updateNodeProperties');
|
||||
|
||||
const { revertToggleNodeDisabled } = useCanvasOperations({ router });
|
||||
const { revertToggleNodeDisabled } = useCanvasOperations();
|
||||
revertToggleNodeDisabled(nodeName);
|
||||
|
||||
expect(updateNodePropertiesSpy).toHaveBeenCalledWith({
|
||||
@@ -1249,7 +1238,7 @@ describe('useCanvasOperations', () => {
|
||||
workflowsStore.getNodeById.mockReturnValueOnce(nodes[0]).mockReturnValueOnce(nodes[1]);
|
||||
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeType);
|
||||
|
||||
const { addConnections } = useCanvasOperations({ router });
|
||||
const { addConnections } = useCanvasOperations();
|
||||
await addConnections(connections);
|
||||
|
||||
expect(workflowsStore.addConnection).toHaveBeenCalledWith({
|
||||
@@ -1272,7 +1261,7 @@ describe('useCanvasOperations', () => {
|
||||
const uiStore = mockedStore(useUIStore);
|
||||
const connections: CanvasConnection[] = [];
|
||||
|
||||
const { addConnections } = useCanvasOperations({ router });
|
||||
const { addConnections } = useCanvasOperations();
|
||||
await addConnections(connections, { keepPristine: false });
|
||||
|
||||
expect(uiStore.stateIsDirty).toBe(true);
|
||||
@@ -1282,7 +1271,7 @@ describe('useCanvasOperations', () => {
|
||||
const uiStore = mockedStore(useUIStore);
|
||||
const connections: CanvasConnection[] = [];
|
||||
|
||||
const { addConnections } = useCanvasOperations({ router });
|
||||
const { addConnections } = useCanvasOperations();
|
||||
await addConnections(connections, { keepPristine: true });
|
||||
|
||||
expect(uiStore.stateIsDirty).toBe(false);
|
||||
@@ -1297,7 +1286,7 @@ describe('useCanvasOperations', () => {
|
||||
|
||||
workflowsStore.getNodeById.mockReturnValueOnce(undefined);
|
||||
|
||||
const { createConnection } = useCanvasOperations({ router });
|
||||
const { createConnection } = useCanvasOperations();
|
||||
createConnection(connection);
|
||||
|
||||
expect(workflowsStore.addConnection).not.toHaveBeenCalled();
|
||||
@@ -1313,7 +1302,7 @@ describe('useCanvasOperations', () => {
|
||||
.mockReturnValueOnce(createTestNode())
|
||||
.mockReturnValueOnce(undefined);
|
||||
|
||||
const { createConnection } = useCanvasOperations({ router });
|
||||
const { createConnection } = useCanvasOperations();
|
||||
createConnection(connection);
|
||||
|
||||
expect(workflowsStore.addConnection).not.toHaveBeenCalled();
|
||||
@@ -1361,7 +1350,7 @@ describe('useCanvasOperations', () => {
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
||||
const { createConnection, editableWorkflowObject } = useCanvasOperations({ router });
|
||||
const { createConnection, editableWorkflowObject } = useCanvasOperations();
|
||||
|
||||
editableWorkflowObject.value.nodes[nodeA.name] = nodeA;
|
||||
editableWorkflowObject.value.nodes[nodeB.name] = nodeB;
|
||||
@@ -1418,7 +1407,7 @@ describe('useCanvasOperations', () => {
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
||||
const { createConnection, editableWorkflowObject } = useCanvasOperations({ router });
|
||||
const { createConnection, editableWorkflowObject } = useCanvasOperations();
|
||||
|
||||
editableWorkflowObject.value.nodes[nodeA.name] = nodeA;
|
||||
editableWorkflowObject.value.nodes[nodeB.name] = nodeB;
|
||||
@@ -1441,7 +1430,7 @@ describe('useCanvasOperations', () => {
|
||||
workflowsStore.getNodeByName.mockReturnValue(testNode);
|
||||
workflowsStore.getNodeById.mockReturnValue(testNode);
|
||||
|
||||
const { revertCreateConnection } = useCanvasOperations({ router });
|
||||
const { revertCreateConnection } = useCanvasOperations();
|
||||
revertCreateConnection(connection);
|
||||
|
||||
expect(workflowsStore.removeConnection).toHaveBeenCalled();
|
||||
@@ -1492,7 +1481,7 @@ describe('useCanvasOperations', () => {
|
||||
})[nodeTypeName],
|
||||
);
|
||||
|
||||
const { isConnectionAllowed } = useCanvasOperations({ router });
|
||||
const { isConnectionAllowed } = useCanvasOperations();
|
||||
expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -1539,7 +1528,7 @@ describe('useCanvasOperations', () => {
|
||||
})[nodeTypeName],
|
||||
);
|
||||
|
||||
const { isConnectionAllowed } = useCanvasOperations({ router });
|
||||
const { isConnectionAllowed } = useCanvasOperations();
|
||||
expect(isConnectionAllowed(sourceNode, targetNode, sourceHandle, targetHandle)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -1579,7 +1568,7 @@ describe('useCanvasOperations', () => {
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
||||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
|
||||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
|
||||
|
||||
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
|
||||
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
|
||||
@@ -1630,7 +1619,7 @@ describe('useCanvasOperations', () => {
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
||||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
|
||||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
|
||||
|
||||
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
|
||||
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
|
||||
@@ -1690,7 +1679,7 @@ describe('useCanvasOperations', () => {
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
||||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
|
||||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
|
||||
|
||||
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
|
||||
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
|
||||
@@ -1750,7 +1739,7 @@ describe('useCanvasOperations', () => {
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
||||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
|
||||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
|
||||
|
||||
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
|
||||
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
|
||||
@@ -1810,7 +1799,7 @@ describe('useCanvasOperations', () => {
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
||||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
|
||||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
|
||||
|
||||
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
|
||||
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
|
||||
@@ -1871,7 +1860,7 @@ describe('useCanvasOperations', () => {
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
||||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
|
||||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
|
||||
|
||||
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
|
||||
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
|
||||
@@ -1929,7 +1918,7 @@ describe('useCanvasOperations', () => {
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
||||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
|
||||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
|
||||
|
||||
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
|
||||
editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
|
||||
@@ -1972,7 +1961,7 @@ describe('useCanvasOperations', () => {
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
||||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations({ router });
|
||||
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
|
||||
|
||||
editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
|
||||
nodeTypesStore.getNodeType = vi.fn(
|
||||
@@ -1995,7 +1984,7 @@ describe('useCanvasOperations', () => {
|
||||
.mockReturnValueOnce(undefined)
|
||||
.mockReturnValueOnce(createTestNode());
|
||||
|
||||
const { deleteConnection } = useCanvasOperations({ router });
|
||||
const { deleteConnection } = useCanvasOperations();
|
||||
deleteConnection(connection);
|
||||
|
||||
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||||
@@ -2009,7 +1998,7 @@ describe('useCanvasOperations', () => {
|
||||
.mockReturnValueOnce(createTestNode())
|
||||
.mockReturnValueOnce(undefined);
|
||||
|
||||
const { deleteConnection } = useCanvasOperations({ router });
|
||||
const { deleteConnection } = useCanvasOperations();
|
||||
deleteConnection(connection);
|
||||
|
||||
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||||
@@ -2039,7 +2028,7 @@ describe('useCanvasOperations', () => {
|
||||
|
||||
workflowsStore.getNodeById.mockReturnValueOnce(nodeA).mockReturnValueOnce(nodeB);
|
||||
|
||||
const { deleteConnection } = useCanvasOperations({ router });
|
||||
const { deleteConnection } = useCanvasOperations();
|
||||
deleteConnection(connection);
|
||||
|
||||
expect(workflowsStore.removeConnection).toHaveBeenCalledWith({
|
||||
@@ -2060,7 +2049,7 @@ describe('useCanvasOperations', () => {
|
||||
{ node: 'targetNode', type: NodeConnectionTypes.Main, index: 2 },
|
||||
];
|
||||
|
||||
const { revertDeleteConnection } = useCanvasOperations({ router });
|
||||
const { revertDeleteConnection } = useCanvasOperations();
|
||||
revertDeleteConnection(connection);
|
||||
|
||||
expect(workflowsStore.addConnection).toHaveBeenCalledWith({ connection });
|
||||
@@ -2073,7 +2062,7 @@ describe('useCanvasOperations', () => {
|
||||
const nonexistentId = 'nonexistent';
|
||||
workflowsStore.getNodeById.mockReturnValue(undefined);
|
||||
|
||||
const { revalidateNodeInputConnections } = useCanvasOperations({ router });
|
||||
const { revalidateNodeInputConnections } = useCanvasOperations();
|
||||
revalidateNodeInputConnections(nonexistentId);
|
||||
|
||||
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||||
@@ -2088,7 +2077,7 @@ describe('useCanvasOperations', () => {
|
||||
workflowsStore.getNodeById.mockReturnValue(node);
|
||||
nodeTypesStore.getNodeType = () => null;
|
||||
|
||||
const { revalidateNodeInputConnections } = useCanvasOperations({ router });
|
||||
const { revalidateNodeInputConnections } = useCanvasOperations();
|
||||
revalidateNodeInputConnections(nodeId);
|
||||
|
||||
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||||
@@ -2145,7 +2134,7 @@ describe('useCanvasOperations', () => {
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
||||
const { revalidateNodeInputConnections } = useCanvasOperations({ router });
|
||||
const { revalidateNodeInputConnections } = useCanvasOperations();
|
||||
revalidateNodeInputConnections(targetNodeId);
|
||||
|
||||
await nextTick();
|
||||
@@ -2209,7 +2198,7 @@ describe('useCanvasOperations', () => {
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
||||
const { revalidateNodeInputConnections } = useCanvasOperations({ router });
|
||||
const { revalidateNodeInputConnections } = useCanvasOperations();
|
||||
revalidateNodeInputConnections(targetNodeId);
|
||||
|
||||
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||||
@@ -2222,7 +2211,7 @@ describe('useCanvasOperations', () => {
|
||||
const nonexistentId = 'nonexistent';
|
||||
workflowsStore.getNodeById.mockReturnValue(undefined);
|
||||
|
||||
const { revalidateNodeOutputConnections } = useCanvasOperations({ router });
|
||||
const { revalidateNodeOutputConnections } = useCanvasOperations();
|
||||
revalidateNodeOutputConnections(nonexistentId);
|
||||
|
||||
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||||
@@ -2237,7 +2226,7 @@ describe('useCanvasOperations', () => {
|
||||
workflowsStore.getNodeById.mockReturnValue(node);
|
||||
nodeTypesStore.getNodeType = () => null;
|
||||
|
||||
const { revalidateNodeOutputConnections } = useCanvasOperations({ router });
|
||||
const { revalidateNodeOutputConnections } = useCanvasOperations();
|
||||
revalidateNodeOutputConnections(nodeId);
|
||||
|
||||
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||||
@@ -2294,7 +2283,7 @@ describe('useCanvasOperations', () => {
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
||||
const { revalidateNodeOutputConnections } = useCanvasOperations({ router });
|
||||
const { revalidateNodeOutputConnections } = useCanvasOperations();
|
||||
revalidateNodeOutputConnections(sourceNodeId);
|
||||
|
||||
await nextTick();
|
||||
@@ -2358,7 +2347,7 @@ describe('useCanvasOperations', () => {
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
|
||||
const { revalidateNodeOutputConnections } = useCanvasOperations({ router });
|
||||
const { revalidateNodeOutputConnections } = useCanvasOperations();
|
||||
revalidateNodeOutputConnections(sourceNodeId);
|
||||
|
||||
expect(workflowsStore.removeConnection).not.toHaveBeenCalled();
|
||||
@@ -2368,7 +2357,7 @@ describe('useCanvasOperations', () => {
|
||||
describe('deleteConnectionsByNodeId', () => {
|
||||
it('should delete all connections for a given node ID', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const { deleteConnectionsByNodeId } = useCanvasOperations({ router });
|
||||
const { deleteConnectionsByNodeId } = useCanvasOperations();
|
||||
|
||||
const node1 = createTestNode({ id: 'node1', name: 'Node 1' });
|
||||
const node2 = createTestNode({ id: 'node2', name: 'Node 1' });
|
||||
@@ -2410,7 +2399,7 @@ describe('useCanvasOperations', () => {
|
||||
|
||||
it('should not delete connections if node ID does not exist', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const { deleteConnectionsByNodeId } = useCanvasOperations({ router });
|
||||
const { deleteConnectionsByNodeId } = useCanvasOperations();
|
||||
|
||||
const nodeId = 'nonexistent';
|
||||
workflowsStore.getNodeById.mockReturnValue(undefined);
|
||||
@@ -2422,7 +2411,7 @@ describe('useCanvasOperations', () => {
|
||||
|
||||
it('should delete all connections of a node with multiple connections', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const { deleteConnectionsByNodeId } = useCanvasOperations({ router });
|
||||
const { deleteConnectionsByNodeId } = useCanvasOperations();
|
||||
|
||||
const sourceNode = createTestNode({ id: 'source', name: 'Source Node' });
|
||||
const targetNode = createTestNode({ id: 'target', name: 'Target Node' });
|
||||
@@ -2516,7 +2505,7 @@ describe('useCanvasOperations', () => {
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
workflowsStore.getWorkflow.mockReturnValue(workflowObject);
|
||||
|
||||
const canvasOperations = useCanvasOperations({ router });
|
||||
const canvasOperations = useCanvasOperations();
|
||||
const duplicatedNodeIds = await canvasOperations.duplicateNodes(['1', '2']);
|
||||
|
||||
expect(duplicatedNodeIds.length).toBe(2);
|
||||
@@ -2540,7 +2529,7 @@ describe('useCanvasOperations', () => {
|
||||
workflowsStore.getNodesByIds.mockReturnValue(nodes);
|
||||
workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({});
|
||||
|
||||
const { copyNodes } = useCanvasOperations({ router });
|
||||
const { copyNodes } = useCanvasOperations();
|
||||
await copyNodes(['1', '2']);
|
||||
|
||||
expect(useClipboard().copy).toHaveBeenCalledTimes(1);
|
||||
@@ -2563,7 +2552,7 @@ describe('useCanvasOperations', () => {
|
||||
workflowsStore.getNodesByIds.mockReturnValue(nodes);
|
||||
workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({});
|
||||
|
||||
const { cutNodes } = useCanvasOperations({ router });
|
||||
const { cutNodes } = useCanvasOperations();
|
||||
await cutNodes(['1', '2']);
|
||||
expect(useClipboard().copy).toHaveBeenCalledTimes(1);
|
||||
expect(vi.mocked(useClipboard().copy).mock.calls).toMatchSnapshot();
|
||||
@@ -2578,7 +2567,7 @@ describe('useCanvasOperations', () => {
|
||||
it("should set webhookId if it doesn't already exist", () => {
|
||||
const node = mock<INodeUi>({ webhookId: undefined });
|
||||
|
||||
const { resolveNodeWebhook } = useCanvasOperations({ router });
|
||||
const { resolveNodeWebhook } = useCanvasOperations();
|
||||
resolveNodeWebhook(node, nodeTypeDescription);
|
||||
|
||||
expect(node.webhookId).toBeDefined();
|
||||
@@ -2587,7 +2576,7 @@ describe('useCanvasOperations', () => {
|
||||
it('should not set webhookId if it already exists', () => {
|
||||
const node = mock<INodeUi>({ webhookId: 'random-id' });
|
||||
|
||||
const { resolveNodeWebhook } = useCanvasOperations({ router });
|
||||
const { resolveNodeWebhook } = useCanvasOperations();
|
||||
resolveNodeWebhook(node, nodeTypeDescription);
|
||||
|
||||
expect(node.webhookId).toBe('random-id');
|
||||
@@ -2596,7 +2585,7 @@ describe('useCanvasOperations', () => {
|
||||
it("should not set webhookId if node description doesn't define any webhooks", () => {
|
||||
const node = mock<INodeUi>({ webhookId: undefined });
|
||||
|
||||
const { resolveNodeWebhook } = useCanvasOperations({ router });
|
||||
const { resolveNodeWebhook } = useCanvasOperations();
|
||||
resolveNodeWebhook(node, mock<INodeTypeDescription>({ webhooks: [] }));
|
||||
|
||||
expect(node.webhookId).toBeUndefined();
|
||||
@@ -2611,7 +2600,7 @@ describe('useCanvasOperations', () => {
|
||||
parameters: { path: '' },
|
||||
});
|
||||
|
||||
const { resolveNodeWebhook } = useCanvasOperations({ router });
|
||||
const { resolveNodeWebhook } = useCanvasOperations();
|
||||
resolveNodeWebhook(node, nodeTypeDescription);
|
||||
|
||||
expect(node.webhookId).toBe('random-id');
|
||||
@@ -2628,7 +2617,7 @@ describe('useCanvasOperations', () => {
|
||||
connections: {},
|
||||
});
|
||||
|
||||
const { initializeWorkspace } = useCanvasOperations({ router });
|
||||
const { initializeWorkspace } = useCanvasOperations();
|
||||
initializeWorkspace(workflow);
|
||||
|
||||
expect(workflowsStore.setNodes).toHaveBeenCalled();
|
||||
@@ -2659,7 +2648,7 @@ describe('useCanvasOperations', () => {
|
||||
connections: {},
|
||||
});
|
||||
|
||||
const { initializeWorkspace } = useCanvasOperations({ router });
|
||||
const { initializeWorkspace } = useCanvasOperations();
|
||||
initializeWorkspace(workflow);
|
||||
|
||||
expect(workflow.nodes[0].parameters).toEqual({ value: true });
|
||||
@@ -2679,7 +2668,7 @@ describe('useCanvasOperations', () => {
|
||||
};
|
||||
const includeNodeNames = new Set<string>(['node1', 'node2', 'node3']);
|
||||
|
||||
const { filterConnectionsByNodes } = useCanvasOperations({ router });
|
||||
const { filterConnectionsByNodes } = useCanvasOperations();
|
||||
const result = filterConnectionsByNodes(connections, includeNodeNames);
|
||||
|
||||
expect(result).toEqual(connections);
|
||||
@@ -2697,7 +2686,7 @@ describe('useCanvasOperations', () => {
|
||||
};
|
||||
const includeNodeNames = new Set<string>();
|
||||
|
||||
const { filterConnectionsByNodes } = useCanvasOperations({ router });
|
||||
const { filterConnectionsByNodes } = useCanvasOperations();
|
||||
const result = filterConnectionsByNodes(connections, includeNodeNames);
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -2717,7 +2706,7 @@ describe('useCanvasOperations', () => {
|
||||
};
|
||||
const includeNodeNames = new Set<string>(['node1']);
|
||||
|
||||
const { filterConnectionsByNodes } = useCanvasOperations({ router });
|
||||
const { filterConnectionsByNodes } = useCanvasOperations();
|
||||
const result = filterConnectionsByNodes(connections, includeNodeNames);
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -2732,7 +2721,7 @@ describe('useCanvasOperations', () => {
|
||||
const connections: INodeConnections = {};
|
||||
const includeNodeNames = new Set<string>(['node1']);
|
||||
|
||||
const { filterConnectionsByNodes } = useCanvasOperations({ router });
|
||||
const { filterConnectionsByNodes } = useCanvasOperations();
|
||||
const result = filterConnectionsByNodes(connections, includeNodeNames);
|
||||
|
||||
expect(result).toEqual({});
|
||||
@@ -2750,7 +2739,7 @@ describe('useCanvasOperations', () => {
|
||||
};
|
||||
const includeNodeNames = new Set<string>(['node1', 'node2', 'node3']);
|
||||
|
||||
const { filterConnectionsByNodes } = useCanvasOperations({ router });
|
||||
const { filterConnectionsByNodes } = useCanvasOperations();
|
||||
const result = filterConnectionsByNodes(connections, includeNodeNames);
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -2763,7 +2752,7 @@ describe('useCanvasOperations', () => {
|
||||
it('should initialize workspace and set execution data when execution is found', async () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const uiStore = mockedStore(useUIStore);
|
||||
const { openExecution } = useCanvasOperations({ router });
|
||||
const { openExecution } = useCanvasOperations();
|
||||
|
||||
const executionId = '123';
|
||||
const executionData: IExecutionResponse = {
|
||||
@@ -2788,7 +2777,7 @@ describe('useCanvasOperations', () => {
|
||||
it('should throw error when execution data is undefined', async () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const executionId = '123';
|
||||
const { openExecution } = useCanvasOperations({ router });
|
||||
const { openExecution } = useCanvasOperations();
|
||||
|
||||
workflowsStore.getExecution.mockResolvedValue(undefined);
|
||||
|
||||
@@ -2799,7 +2788,7 @@ describe('useCanvasOperations', () => {
|
||||
|
||||
it('should clear workflow pin data if execution mode is not manual', async () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const { openExecution } = useCanvasOperations({ router });
|
||||
const { openExecution } = useCanvasOperations();
|
||||
|
||||
const executionId = '123';
|
||||
const executionData: IExecutionResponse = {
|
||||
@@ -2820,7 +2809,7 @@ describe('useCanvasOperations', () => {
|
||||
});
|
||||
it('should show an error notification for failed executions', async () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const { openExecution } = useCanvasOperations({ router });
|
||||
const { openExecution } = useCanvasOperations();
|
||||
const toast = useToast();
|
||||
|
||||
const executionId = '123';
|
||||
@@ -2909,7 +2898,7 @@ describe('useCanvasOperations', () => {
|
||||
main: [[{ node: nodeA.name, type: NodeConnectionTypes.Main, index: 0 }]],
|
||||
});
|
||||
|
||||
const { connectAdjacentNodes } = useCanvasOperations({ router });
|
||||
const { connectAdjacentNodes } = useCanvasOperations();
|
||||
connectAdjacentNodes(nodeB.id, { trackHistory: true });
|
||||
|
||||
// Check that A was connected directly to C
|
||||
@@ -2979,7 +2968,7 @@ describe('useCanvasOperations', () => {
|
||||
main: [[{ node: nodeA.name, type: NodeConnectionTypes.Main, index: 0 }]],
|
||||
});
|
||||
|
||||
const { connectAdjacentNodes } = useCanvasOperations({ router });
|
||||
const { connectAdjacentNodes } = useCanvasOperations();
|
||||
connectAdjacentNodes(nodeB.id, { trackHistory: true });
|
||||
|
||||
// Check that A was connected directly to C
|
||||
@@ -3016,7 +3005,7 @@ describe('useCanvasOperations', () => {
|
||||
});
|
||||
workflowsStore.incomingConnectionsByNodeName.mockReturnValue({});
|
||||
|
||||
const { connectAdjacentNodes } = useCanvasOperations({ router });
|
||||
const { connectAdjacentNodes } = useCanvasOperations();
|
||||
connectAdjacentNodes(nodeB.id);
|
||||
|
||||
expect(workflowsStore.addConnection).not.toHaveBeenCalled();
|
||||
@@ -3044,7 +3033,7 @@ describe('useCanvasOperations', () => {
|
||||
main: [[{ node: nodeA.name, type: NodeConnectionTypes.Main, index: 0 }]],
|
||||
});
|
||||
|
||||
const { connectAdjacentNodes } = useCanvasOperations({ router });
|
||||
const { connectAdjacentNodes } = useCanvasOperations();
|
||||
connectAdjacentNodes(nodeB.id);
|
||||
|
||||
expect(workflowsStore.addConnection).not.toHaveBeenCalled();
|
||||
@@ -3086,7 +3075,7 @@ describe('useCanvasOperations', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const { importTemplate } = useCanvasOperations({ router });
|
||||
const { importTemplate } = useCanvasOperations();
|
||||
|
||||
const templateId = 'template-id';
|
||||
const templateName = 'template name';
|
||||
|
||||
@@ -102,7 +102,6 @@ import type {
|
||||
} from 'n8n-workflow';
|
||||
import { deepCopy, NodeConnectionTypes, NodeHelpers, TelemetryHelpers } from 'n8n-workflow';
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
import type { useRouter } from 'vue-router';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
import { useUniqueNodeName } from '@/composables/useUniqueNodeName';
|
||||
import { isPresent } from '../utils/typesUtils';
|
||||
@@ -140,7 +139,7 @@ type AddNodeOptions = AddNodesBaseOptions & {
|
||||
isAutoAdd?: boolean;
|
||||
};
|
||||
|
||||
export function useCanvasOperations({ router }: { router: ReturnType<typeof useRouter> }) {
|
||||
export function useCanvasOperations() {
|
||||
const rootStore = useRootStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const credentialsStore = useCredentialsStore();
|
||||
@@ -158,7 +157,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||
|
||||
const i18n = useI18n();
|
||||
const toast = useToast();
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const telemetry = useTelemetry();
|
||||
const externalHooks = useExternalHooks();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { START_NODE_TYPE } from '@/constants';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useCanvasOperations } from './useCanvasOperations';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
|
||||
@@ -13,8 +13,7 @@ export function useClearExecutionButtonVisible() {
|
||||
const workflowExecutionData = computed(() => workflowsStore.workflowExecutionData);
|
||||
const isWorkflowRunning = computed(() => workflowsStore.isWorkflowRunning);
|
||||
const isReadOnlyRoute = computed(() => !!route?.meta?.readOnlyCanvas);
|
||||
const router = useRouter();
|
||||
const { editableWorkflow } = useCanvasOperations({ router });
|
||||
const { editableWorkflow } = useCanvasOperations();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const isReadOnlyEnvironment = computed(() => sourceControlStore.preferences.branchReadOnly);
|
||||
const allTriggerNodesDisabled = computed(() =>
|
||||
|
||||
@@ -9,7 +9,6 @@ import { fireEvent, waitFor } from '@testing-library/vue';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { beforeEach, describe, vi } from 'vitest';
|
||||
import { defineComponent, h, ref, toValue } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useExpressionEditor } from './useExpressionEditor';
|
||||
|
||||
vi.mock('@/composables/useAutocompleteTelemetry', () => ({
|
||||
@@ -26,7 +25,7 @@ describe('useExpressionEditor', () => {
|
||||
const mockResolveExpression = () => {
|
||||
const mock = vi.fn();
|
||||
vi.spyOn(workflowHelpers, 'useWorkflowHelpers').mockReturnValueOnce({
|
||||
...workflowHelpers.useWorkflowHelpers({ router: useRouter() }),
|
||||
...workflowHelpers.useWorkflowHelpers(),
|
||||
resolveExpression: mock,
|
||||
});
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
import { EditorView, type ViewUpdate } from '@codemirror/view';
|
||||
import debounce from 'lodash/debounce';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useWorkflowsStore } from '../stores/workflows.store';
|
||||
import { useAutocompleteTelemetry } from './useAutocompleteTelemetry';
|
||||
@@ -62,8 +61,7 @@ export const useExpressionEditor = ({
|
||||
}) => {
|
||||
const ndvStore = useNDVStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const router = useRouter();
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const i18n = useI18n();
|
||||
const editor = ref<EditorView>();
|
||||
const hasFocus = ref(false);
|
||||
|
||||
@@ -15,12 +15,7 @@ import { type FrontendSettings } from '@n8n/api-types';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { NodeConnectionTypes, type IConnections, type IRunData } from 'n8n-workflow';
|
||||
import { defineComponent } from 'vue';
|
||||
import {
|
||||
createRouter,
|
||||
createWebHistory,
|
||||
useRouter,
|
||||
type RouteLocationNormalizedLoaded,
|
||||
} from 'vue-router';
|
||||
import { createRouter, createWebHistory, type RouteLocationNormalizedLoaded } from 'vue-router';
|
||||
|
||||
describe(useNodeDirtiness, () => {
|
||||
let nodeTypeStore: ReturnType<typeof useNodeTypesStore>;
|
||||
@@ -42,7 +37,7 @@ describe(useNodeDirtiness, () => {
|
||||
workflowsStore = useWorkflowsStore();
|
||||
settingsStore = useSettingsStore();
|
||||
historyHelper = useHistoryHelper({} as RouteLocationNormalizedLoaded);
|
||||
canvasOperations = useCanvasOperations({ router: useRouter() });
|
||||
canvasOperations = useCanvasOperations();
|
||||
uiStore = useUIStore();
|
||||
|
||||
nodeTypeStore.setNodeTypes(defaultNodeDescriptions);
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
||||
import { useWorkflowSaving } from '@/composables/useWorkflowSaving';
|
||||
|
||||
export type SimplifiedExecution = Pick<
|
||||
IExecutionResponse,
|
||||
@@ -84,7 +85,7 @@ export async function executionFinished(
|
||||
};
|
||||
} else {
|
||||
if (data.status === 'success') {
|
||||
handleExecutionFinishedSuccessfully(data.workflowId, options);
|
||||
handleExecutionFinishedSuccessfully(data.workflowId);
|
||||
successToastAlreadyShown = true;
|
||||
}
|
||||
|
||||
@@ -101,9 +102,9 @@ export async function executionFinished(
|
||||
if (execution.data?.waitTill !== undefined) {
|
||||
handleExecutionFinishedWithWaitTill(options);
|
||||
} else if (execution.status === 'error' || execution.status === 'canceled') {
|
||||
handleExecutionFinishedWithErrorOrCanceled(execution, runExecutionData, options);
|
||||
handleExecutionFinishedWithErrorOrCanceled(execution, runExecutionData);
|
||||
} else {
|
||||
handleExecutionFinishedWithOther(successToastAlreadyShown, options);
|
||||
handleExecutionFinishedWithOther(successToastAlreadyShown);
|
||||
}
|
||||
|
||||
setRunExecutionData(execution, runExecutionData);
|
||||
@@ -234,7 +235,8 @@ export function handleExecutionFinishedWithWaitTill(options: {
|
||||
}) {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const workflowHelpers = useWorkflowHelpers(options);
|
||||
const workflowSaving = useWorkflowSaving(options);
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const workflowObject = workflowsStore.getCurrentWorkflow();
|
||||
|
||||
const workflowSettings = workflowsStore.workflowSettings;
|
||||
@@ -247,7 +249,7 @@ export function handleExecutionFinishedWithWaitTill(options: {
|
||||
globalLinkActionsEventBus.emit('registerGlobalLinkAction', {
|
||||
key: 'open-settings',
|
||||
action: async () => {
|
||||
if (workflowsStore.isNewWorkflow) await workflowHelpers.saveAsNewWorkflow();
|
||||
if (workflowsStore.isNewWorkflow) await workflowSaving.saveAsNewWorkflow();
|
||||
uiStore.openModal(WORKFLOW_SETTINGS_MODAL_KEY);
|
||||
},
|
||||
});
|
||||
@@ -263,13 +265,12 @@ export function handleExecutionFinishedWithWaitTill(options: {
|
||||
export function handleExecutionFinishedWithErrorOrCanceled(
|
||||
execution: SimplifiedExecution,
|
||||
runExecutionData: IRunExecutionData,
|
||||
options: { router: ReturnType<typeof useRouter> },
|
||||
) {
|
||||
const toast = useToast();
|
||||
const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowHelpers = useWorkflowHelpers(options);
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const workflowObject = workflowsStore.getCurrentWorkflow();
|
||||
|
||||
workflowHelpers.setDocumentTitle(workflowObject.name as string, 'ERROR');
|
||||
@@ -339,12 +340,9 @@ export function handleExecutionFinishedWithErrorOrCanceled(
|
||||
* immediately, even though we still need to fetch and deserialize the
|
||||
* full execution data, to minimize perceived latency.
|
||||
*/
|
||||
export function handleExecutionFinishedSuccessfully(
|
||||
workflowId: string,
|
||||
options: { router: ReturnType<typeof useRouter> },
|
||||
) {
|
||||
export function handleExecutionFinishedSuccessfully(workflowId: string) {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowHelpers = useWorkflowHelpers(options);
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const toast = useToast();
|
||||
const i18n = useI18n();
|
||||
|
||||
@@ -359,14 +357,11 @@ export function handleExecutionFinishedSuccessfully(
|
||||
/**
|
||||
* Handle the case when the workflow execution finished successfully.
|
||||
*/
|
||||
export function handleExecutionFinishedWithOther(
|
||||
successToastAlreadyShown: boolean,
|
||||
options: { router: ReturnType<typeof useRouter> },
|
||||
) {
|
||||
export function handleExecutionFinishedWithOther(successToastAlreadyShown: boolean) {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const toast = useToast();
|
||||
const i18n = useI18n();
|
||||
const workflowHelpers = useWorkflowHelpers(options);
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const workflowObject = workflowsStore.getCurrentWorkflow();
|
||||
|
||||
|
||||
@@ -37,9 +37,9 @@ export async function executionRecovered(
|
||||
if (execution.data?.waitTill !== undefined) {
|
||||
handleExecutionFinishedWithWaitTill(options);
|
||||
} else if (execution.status === 'error' || execution.status === 'canceled') {
|
||||
handleExecutionFinishedWithErrorOrCanceled(execution, runExecutionData, options);
|
||||
handleExecutionFinishedWithErrorOrCanceled(execution, runExecutionData);
|
||||
} else {
|
||||
handleExecutionFinishedWithOther(false, options);
|
||||
handleExecutionFinishedWithOther(false);
|
||||
}
|
||||
|
||||
setRunExecutionData(execution, runExecutionData);
|
||||
|
||||
@@ -2,7 +2,6 @@ import { defineComponent, h, nextTick, ref, toValue } from 'vue';
|
||||
import { useResolvedExpression } from './useResolvedExpression';
|
||||
import * as workflowHelpers from '@/composables/useWorkflowHelpers';
|
||||
import { renderComponent } from '../__tests__/render';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
|
||||
@@ -25,7 +24,7 @@ async function renderTestComponent(...options: Parameters<typeof useResolvedExpr
|
||||
const mockResolveExpression = () => {
|
||||
const mock = vi.fn();
|
||||
vi.spyOn(workflowHelpers, 'useWorkflowHelpers').mockReturnValueOnce({
|
||||
...workflowHelpers.useWorkflowHelpers({ router: useRouter() }),
|
||||
...workflowHelpers.useWorkflowHelpers(),
|
||||
resolveExpression: mock,
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { isExpression as isExpressionUtil, stringifyExpressionResult } from '@/u
|
||||
import debounce from 'lodash/debounce';
|
||||
import { createResultError, createResultOk, type IDataObject, type Result } from 'n8n-workflow';
|
||||
import { computed, onMounted, ref, toRef, toValue, watch, type MaybeRefOrGetter } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useWorkflowHelpers, type ResolveParameterOptions } from './useWorkflowHelpers';
|
||||
|
||||
export function useResolvedExpression({
|
||||
@@ -22,8 +21,7 @@ export function useResolvedExpression({
|
||||
const ndvStore = useNDVStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
|
||||
const router = useRouter();
|
||||
const { resolveExpression } = useWorkflowHelpers({ router });
|
||||
const { resolveExpression } = useWorkflowHelpers();
|
||||
|
||||
const resolvedExpression = ref<unknown>(null);
|
||||
const resolvedExpressionString = ref('');
|
||||
|
||||
@@ -152,7 +152,7 @@ describe('useRunWorkflow({ router })', () => {
|
||||
agentRequestStore = useAgentRequestStore();
|
||||
|
||||
router = useRouter();
|
||||
workflowHelpers = useWorkflowHelpers({ router });
|
||||
workflowHelpers = useWorkflowHelpers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -236,7 +236,7 @@ describe('useRunWorkflow({ router })', () => {
|
||||
|
||||
vi.mocked(workflowsStore).isWorkflowActive = true;
|
||||
|
||||
vi.mocked(useWorkflowHelpers({ router })).getWorkflowDataToSave.mockResolvedValue({
|
||||
vi.mocked(useWorkflowHelpers()).getWorkflowDataToSave.mockResolvedValue({
|
||||
nodes: [
|
||||
{
|
||||
name: 'Slack',
|
||||
@@ -267,7 +267,7 @@ describe('useRunWorkflow({ router })', () => {
|
||||
|
||||
vi.mocked(workflowsStore).isWorkflowActive = true;
|
||||
|
||||
vi.mocked(useWorkflowHelpers({ router })).getWorkflowDataToSave.mockResolvedValue({
|
||||
vi.mocked(useWorkflowHelpers()).getWorkflowDataToSave.mockResolvedValue({
|
||||
nodes: [
|
||||
{
|
||||
name: 'Slack',
|
||||
|
||||
@@ -44,10 +44,12 @@ import { usePushConnectionStore } from '@/stores/pushConnection.store';
|
||||
import { useNodeDirtiness } from '@/composables/useNodeDirtiness';
|
||||
import { useCanvasOperations } from './useCanvasOperations';
|
||||
import { useAgentRequestStore } from '@n8n/stores/useAgentRequestStore';
|
||||
import { useWorkflowSaving } from './useWorkflowSaving';
|
||||
|
||||
export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof useRouter> }) {
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const workflowHelpers = useWorkflowHelpers({ router: useRunWorkflowOpts.router });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const workflowSaving = useWorkflowSaving({ router: useRunWorkflowOpts.router });
|
||||
const i18n = useI18n();
|
||||
const toast = useToast();
|
||||
const telemetry = useTelemetry();
|
||||
@@ -60,7 +62,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const executionsStore = useExecutionsStore();
|
||||
const { dirtinessByName } = useNodeDirtiness();
|
||||
const { startChat } = useCanvasOperations({ router: useRunWorkflowOpts.router });
|
||||
const { startChat } = useCanvasOperations();
|
||||
|
||||
function sortNodesByYPosition(nodes: string[]) {
|
||||
return [...nodes].sort((a, b) => {
|
||||
@@ -144,7 +146,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
|
||||
const runData = workflowsStore.getWorkflowRunData;
|
||||
|
||||
if (workflowsStore.isNewWorkflow) {
|
||||
await workflowHelpers.saveCurrentWorkflow();
|
||||
await workflowSaving.saveCurrentWorkflow();
|
||||
}
|
||||
|
||||
const workflowData = await workflowHelpers.getWorkflowDataToSave();
|
||||
|
||||
@@ -15,12 +15,14 @@ import { useToast } from '@/composables/useToast';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { ref } from 'vue';
|
||||
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
|
||||
import { useWorkflowSaving } from './useWorkflowSaving';
|
||||
|
||||
export function useWorkflowActivate() {
|
||||
const updatingWorkflowActivation = ref(false);
|
||||
|
||||
const router = useRouter();
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const workflowSaving = useWorkflowSaving({ router });
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const uiStore = useUIStore();
|
||||
const telemetry = useTelemetry();
|
||||
@@ -41,7 +43,7 @@ export function useWorkflowActivate() {
|
||||
|
||||
let currWorkflowId: string | undefined = workflowId;
|
||||
if (!currWorkflowId || currWorkflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) {
|
||||
const saved = await workflowHelpers.saveCurrentWorkflow();
|
||||
const saved = await workflowSaving.saveCurrentWorkflow();
|
||||
if (!saved) {
|
||||
updatingWorkflowActivation.value = false;
|
||||
return false; // Return false if save failed
|
||||
|
||||
@@ -37,7 +37,7 @@ export function useWorkflowExtraction() {
|
||||
const toast = useToast();
|
||||
const router = useRouter();
|
||||
const historyStore = useHistoryStore();
|
||||
const canvasOperations = useCanvasOperations({ router });
|
||||
const canvasOperations = useCanvasOperations();
|
||||
const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
|
||||
@@ -1,77 +1,21 @@
|
||||
import type {
|
||||
IExecutionResponse,
|
||||
IWorkflowData,
|
||||
IWorkflowDataUpdate,
|
||||
IWorkflowDb,
|
||||
} from '@/Interface';
|
||||
import type { IExecutionResponse, IWorkflowData, IWorkflowDb } from '@/Interface';
|
||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import router from '@/router';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useWorkflowsEEStore } from '@/stores/workflows.ee.store';
|
||||
import { useTagsStore } from '@/stores/tags.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { createTestNode, createTestWorkflow } from '@/__tests__/mocks';
|
||||
import { createTestWorkflow } from '@/__tests__/mocks';
|
||||
import { WEBHOOK_NODE_TYPE, type AssignmentCollectionValue } from 'n8n-workflow';
|
||||
import * as apiWebhooks from '@n8n/rest-api-client/api/webhooks';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import { nodeTypes } from '@/components/CanvasChat/__test__/data';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { CHAT_TRIGGER_NODE_TYPE } from '@/constants';
|
||||
|
||||
const getDuplicateTestWorkflow = (): IWorkflowDataUpdate => ({
|
||||
name: 'Duplicate webhook test',
|
||||
active: false,
|
||||
nodes: [
|
||||
{
|
||||
parameters: {
|
||||
path: '5340ae49-2c96-4492-9073-7744d2e52b8a',
|
||||
options: {},
|
||||
},
|
||||
id: 'c1e1b6e7-df13-41b1-95f6-42903b85e438',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 2,
|
||||
position: [680, 20],
|
||||
webhookId: '5340ae49-2c96-4492-9073-7744d2e52b8a',
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
path: 'aa5150d8-1d7d-4247-88d8-44c96fe3a37b',
|
||||
options: {},
|
||||
},
|
||||
id: 'aa5150d8-1d7d-4247-88d8-44c96fe3a37b',
|
||||
name: 'Webhook 2',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 2,
|
||||
position: [700, 40],
|
||||
webhookId: 'aa5150d8-1d7d-4247-88d8-44c96fe3a37b',
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
resume: 'webhook',
|
||||
options: {
|
||||
webhookSuffix: '/test',
|
||||
},
|
||||
},
|
||||
id: '979d8443-51b1-48e2-b239-acf399b66509',
|
||||
name: 'Wait',
|
||||
type: 'n8n-nodes-base.wait',
|
||||
typeVersion: 1.1,
|
||||
position: [900, 20],
|
||||
webhookId: '5340ae49-2c96-4492-9073-7744d2e52b8a',
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
});
|
||||
|
||||
describe('useWorkflowHelpers', () => {
|
||||
let workflowsStore: ReturnType<typeof mockedStore<typeof useWorkflowsStore>>;
|
||||
let workflowsEEStore: ReturnType<typeof useWorkflowsEEStore>;
|
||||
let tagsStore: ReturnType<typeof useTagsStore>;
|
||||
let uiStore: ReturnType<typeof useUIStore>;
|
||||
let nodeTypesStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;
|
||||
|
||||
beforeAll(() => {
|
||||
setActivePinia(createTestingPinia());
|
||||
@@ -99,7 +43,7 @@ describe('useWorkflowHelpers', () => {
|
||||
options: {},
|
||||
infoMessage: '',
|
||||
};
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const resolvedParameters =
|
||||
workflowHelpers.getNodeParametersWithResolvedExpressions(nodeParameters);
|
||||
expect(resolvedParameters.url).toHaveProperty('resolvedExpressionValue');
|
||||
@@ -123,7 +67,7 @@ describe('useWorkflowHelpers', () => {
|
||||
includeOtherFields: false,
|
||||
options: {},
|
||||
};
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const resolvedParameters =
|
||||
workflowHelpers.getNodeParametersWithResolvedExpressions(nodeParameters);
|
||||
expect(resolvedParameters).toHaveProperty('assignments');
|
||||
@@ -164,7 +108,7 @@ describe('useWorkflowHelpers', () => {
|
||||
looseTypeValidation: false,
|
||||
options: {},
|
||||
};
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const resolvedParameters = workflowHelpers.getNodeParametersWithResolvedExpressions(
|
||||
nodeParameters,
|
||||
) as typeof nodeParameters;
|
||||
@@ -193,7 +137,7 @@ describe('useWorkflowHelpers', () => {
|
||||
combineFilters: 'AND',
|
||||
options: {},
|
||||
};
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const resolvedParameters = workflowHelpers.getNodeParametersWithResolvedExpressions(
|
||||
nodeParameters,
|
||||
) as typeof nodeParameters;
|
||||
@@ -232,7 +176,7 @@ describe('useWorkflowHelpers', () => {
|
||||
combineFilters: 'AND',
|
||||
options: {},
|
||||
};
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const resolvedParameters = workflowHelpers.getNodeParametersWithResolvedExpressions(
|
||||
nodeParameters,
|
||||
) as typeof nodeParameters;
|
||||
@@ -242,55 +186,9 @@ describe('useWorkflowHelpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveAsNewWorkflow', () => {
|
||||
it('should respect `resetWebhookUrls: false` when duplicating workflows', async () => {
|
||||
const workflow = getDuplicateTestWorkflow();
|
||||
if (!workflow.nodes) {
|
||||
throw new Error('Missing nodes in test workflow');
|
||||
}
|
||||
const { saveAsNewWorkflow } = useWorkflowHelpers({ router });
|
||||
const webHookIdsPreSave = workflow.nodes.map((node) => node.webhookId);
|
||||
const pathsPreSave = workflow.nodes.map((node) => node.parameters.path);
|
||||
|
||||
await saveAsNewWorkflow({
|
||||
name: workflow.name,
|
||||
resetWebhookUrls: false,
|
||||
data: workflow,
|
||||
});
|
||||
|
||||
const webHookIdsPostSave = workflow.nodes.map((node) => node.webhookId);
|
||||
const pathsPostSave = workflow.nodes.map((node) => node.parameters.path);
|
||||
// Expect webhookIds and paths to be the same as in the original workflow
|
||||
expect(webHookIdsPreSave).toEqual(webHookIdsPostSave);
|
||||
expect(pathsPreSave).toEqual(pathsPostSave);
|
||||
});
|
||||
|
||||
it('should respect `resetWebhookUrls: true` when duplicating workflows', async () => {
|
||||
const workflow = getDuplicateTestWorkflow();
|
||||
if (!workflow.nodes) {
|
||||
throw new Error('Missing nodes in test workflow');
|
||||
}
|
||||
const { saveAsNewWorkflow } = useWorkflowHelpers({ router });
|
||||
const webHookIdsPreSave = workflow.nodes.map((node) => node.webhookId);
|
||||
const pathsPreSave = workflow.nodes.map((node) => node.parameters.path);
|
||||
|
||||
await saveAsNewWorkflow({
|
||||
name: workflow.name,
|
||||
resetWebhookUrls: true,
|
||||
data: workflow,
|
||||
});
|
||||
|
||||
const webHookIdsPostSave = workflow.nodes.map((node) => node.webhookId);
|
||||
const pathsPostSave = workflow.nodes.map((node) => node.parameters.path);
|
||||
// Now, expect webhookIds and paths to be different
|
||||
expect(webHookIdsPreSave).not.toEqual(webHookIdsPostSave);
|
||||
expect(pathsPreSave).not.toEqual(pathsPostSave);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initState', () => {
|
||||
it('should initialize workflow state with provided data', () => {
|
||||
const { initState } = useWorkflowHelpers({ router });
|
||||
const { initState } = useWorkflowHelpers();
|
||||
|
||||
const workflowData = createTestWorkflow({
|
||||
id: '1',
|
||||
@@ -344,7 +242,7 @@ describe('useWorkflowHelpers', () => {
|
||||
});
|
||||
|
||||
it('should handle missing `usedCredentials` and `sharedWithProjects` gracefully', () => {
|
||||
const { initState } = useWorkflowHelpers({ router });
|
||||
const { initState } = useWorkflowHelpers();
|
||||
|
||||
const workflowData = createTestWorkflow({
|
||||
id: '1',
|
||||
@@ -365,7 +263,7 @@ describe('useWorkflowHelpers', () => {
|
||||
});
|
||||
|
||||
it('should handle missing `tags` gracefully', () => {
|
||||
const { initState } = useWorkflowHelpers({ router });
|
||||
const { initState } = useWorkflowHelpers();
|
||||
|
||||
const workflowData = createTestWorkflow({
|
||||
id: '1',
|
||||
@@ -387,7 +285,7 @@ describe('useWorkflowHelpers', () => {
|
||||
|
||||
describe('checkConflictingWebhooks', () => {
|
||||
it('should return null if no conflicts', async () => {
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
uiStore.stateIsDirty = false;
|
||||
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue({
|
||||
nodes: [],
|
||||
@@ -396,7 +294,7 @@ describe('useWorkflowHelpers', () => {
|
||||
});
|
||||
|
||||
it('should return conflicting webhook data and workflow id is different', async () => {
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
uiStore.stateIsDirty = false;
|
||||
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue({
|
||||
nodes: [
|
||||
@@ -433,7 +331,7 @@ describe('useWorkflowHelpers', () => {
|
||||
});
|
||||
|
||||
it('should return null if webhook already exist but workflow id is the same', async () => {
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
uiStore.stateIsDirty = false;
|
||||
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue({
|
||||
nodes: [
|
||||
@@ -456,7 +354,7 @@ describe('useWorkflowHelpers', () => {
|
||||
});
|
||||
|
||||
it('should call getWorkflowDataToSave if state is dirty', async () => {
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
uiStore.stateIsDirty = true;
|
||||
vi.spyOn(workflowHelpers, 'getWorkflowDataToSave').mockResolvedValue({
|
||||
nodes: [],
|
||||
@@ -467,7 +365,7 @@ describe('useWorkflowHelpers', () => {
|
||||
|
||||
describe('executeData', () => {
|
||||
it('should return empty execute data if no parent nodes', () => {
|
||||
const { executeData } = useWorkflowHelpers({ router });
|
||||
const { executeData } = useWorkflowHelpers();
|
||||
|
||||
const parentNodes: string[] = [];
|
||||
const currentNode = 'Set';
|
||||
@@ -484,7 +382,7 @@ describe('useWorkflowHelpers', () => {
|
||||
});
|
||||
|
||||
it('should return the correct execution data with one parent node', () => {
|
||||
const { executeData } = useWorkflowHelpers({ router });
|
||||
const { executeData } = useWorkflowHelpers();
|
||||
|
||||
const parentNodes = ['Start'];
|
||||
const currentNode = 'Set';
|
||||
@@ -555,7 +453,7 @@ describe('useWorkflowHelpers', () => {
|
||||
});
|
||||
|
||||
it('should return the correct execution data with multiple parent nodes, only one with execution data', () => {
|
||||
const { executeData } = useWorkflowHelpers({ router });
|
||||
const { executeData } = useWorkflowHelpers();
|
||||
|
||||
const parentNodes = ['Parent A', 'Parent B'];
|
||||
const currentNode = 'Set';
|
||||
@@ -626,7 +524,7 @@ describe('useWorkflowHelpers', () => {
|
||||
});
|
||||
|
||||
it('should return the correct execution data with multiple parent nodes, all with execution data', () => {
|
||||
const { executeData } = useWorkflowHelpers({ router });
|
||||
const { executeData } = useWorkflowHelpers();
|
||||
|
||||
const parentNodes = ['Parent A', 'Parent B'];
|
||||
const currentNode = 'Set';
|
||||
@@ -722,7 +620,7 @@ describe('useWorkflowHelpers', () => {
|
||||
});
|
||||
|
||||
it('should return data from pinnedWorkflowData if available', () => {
|
||||
const { executeData } = useWorkflowHelpers({ router });
|
||||
const { executeData } = useWorkflowHelpers();
|
||||
|
||||
const parentNodes = ['ParentNode'];
|
||||
const currentNode = 'CurrentNode';
|
||||
@@ -741,7 +639,7 @@ describe('useWorkflowHelpers', () => {
|
||||
});
|
||||
|
||||
it('should return data from getWorkflowRunData if pinnedWorkflowData is not available', () => {
|
||||
const { executeData } = useWorkflowHelpers({ router });
|
||||
const { executeData } = useWorkflowHelpers();
|
||||
|
||||
const parentNodes = ['ParentNode'];
|
||||
const currentNode = 'CurrentNode';
|
||||
@@ -778,7 +676,7 @@ describe('useWorkflowHelpers', () => {
|
||||
});
|
||||
});
|
||||
it('should use provided parentRunIndex ', () => {
|
||||
const { executeData } = useWorkflowHelpers({ router });
|
||||
const { executeData } = useWorkflowHelpers();
|
||||
|
||||
const parentNodes = ['ParentNode'];
|
||||
const currentNode = 'CurrentNode';
|
||||
@@ -818,7 +716,7 @@ describe('useWorkflowHelpers', () => {
|
||||
});
|
||||
|
||||
it('should return empty data if neither pinnedWorkflowData nor getWorkflowRunData is available', () => {
|
||||
const { executeData } = useWorkflowHelpers({ router });
|
||||
const { executeData } = useWorkflowHelpers();
|
||||
|
||||
const parentNodes = ['ParentNode'];
|
||||
const currentNode = 'CurrentNode';
|
||||
@@ -835,58 +733,4 @@ describe('useWorkflowHelpers', () => {
|
||||
expect(result.source).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveCurrentWorkflow', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }));
|
||||
|
||||
workflowsStore = mockedStore(useWorkflowsStore);
|
||||
|
||||
nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||
nodeTypesStore.setNodeTypes(nodeTypes);
|
||||
});
|
||||
|
||||
it('should save the current workflow', async () => {
|
||||
const workflow = createTestWorkflow({
|
||||
id: 'w0',
|
||||
nodes: [createTestNode({ type: CHAT_TRIGGER_NODE_TYPE, disabled: false })],
|
||||
active: true,
|
||||
});
|
||||
|
||||
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue(workflow);
|
||||
vi.spyOn(workflowsStore, 'updateWorkflow').mockResolvedValue(workflow);
|
||||
|
||||
workflowsStore.setWorkflow(workflow);
|
||||
|
||||
const { saveCurrentWorkflow } = useWorkflowHelpers({ router });
|
||||
await saveCurrentWorkflow({ id: 'w0' });
|
||||
expect(workflowsStore.updateWorkflow).toHaveBeenCalledWith(
|
||||
'w0',
|
||||
expect.objectContaining({ id: 'w0', active: true }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should include active=false in the request if the workflow has no activatable trigger node', async () => {
|
||||
const workflow = createTestWorkflow({
|
||||
id: 'w1',
|
||||
nodes: [createTestNode({ type: CHAT_TRIGGER_NODE_TYPE, disabled: true })],
|
||||
active: true,
|
||||
});
|
||||
|
||||
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue(workflow);
|
||||
vi.spyOn(workflowsStore, 'updateWorkflow').mockResolvedValue(workflow);
|
||||
|
||||
workflowsStore.setWorkflow(workflow);
|
||||
|
||||
const { saveCurrentWorkflow } = useWorkflowHelpers({ router });
|
||||
await saveCurrentWorkflow({ id: 'w1' });
|
||||
expect(workflowsStore.updateWorkflow).toHaveBeenCalledWith(
|
||||
'w1',
|
||||
expect.objectContaining({ id: 'w1', active: false }),
|
||||
false,
|
||||
);
|
||||
expect(workflowsStore.setWorkflowInactive).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -6,9 +6,14 @@ import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import type { IWorkflowDataUpdate } from '@/Interface';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import { createTestNode, createTestWorkflow } from '@/__tests__/mocks';
|
||||
import { CHAT_TRIGGER_NODE_TYPE } from 'n8n-workflow';
|
||||
import { nodeTypes } from '@/components/CanvasChat/__test__/data';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
|
||||
const modalConfirmSpy = vi.fn();
|
||||
const saveCurrentWorkflowSpy = vi.fn();
|
||||
|
||||
vi.mock('@/composables/useMessage', () => {
|
||||
return {
|
||||
@@ -18,25 +23,81 @@ vi.mock('@/composables/useMessage', () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/composables/useWorkflowHelpers', () => {
|
||||
return {
|
||||
useWorkflowHelpers: () => ({
|
||||
saveCurrentWorkflow: saveCurrentWorkflowSpy,
|
||||
}),
|
||||
};
|
||||
const getDuplicateTestWorkflow = (): IWorkflowDataUpdate => ({
|
||||
name: 'Duplicate webhook test',
|
||||
active: false,
|
||||
nodes: [
|
||||
{
|
||||
parameters: {
|
||||
path: '5340ae49-2c96-4492-9073-7744d2e52b8a',
|
||||
options: {},
|
||||
},
|
||||
id: 'c1e1b6e7-df13-41b1-95f6-42903b85e438',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 2,
|
||||
position: [680, 20],
|
||||
webhookId: '5340ae49-2c96-4492-9073-7744d2e52b8a',
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
path: 'aa5150d8-1d7d-4247-88d8-44c96fe3a37b',
|
||||
options: {},
|
||||
},
|
||||
id: 'aa5150d8-1d7d-4247-88d8-44c96fe3a37b',
|
||||
name: 'Webhook 2',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 2,
|
||||
position: [700, 40],
|
||||
webhookId: 'aa5150d8-1d7d-4247-88d8-44c96fe3a37b',
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
resume: 'webhook',
|
||||
options: {
|
||||
webhookSuffix: '/test',
|
||||
},
|
||||
},
|
||||
id: '979d8443-51b1-48e2-b239-acf399b66509',
|
||||
name: 'Wait',
|
||||
type: 'n8n-nodes-base.wait',
|
||||
typeVersion: 1.1,
|
||||
position: [900, 20],
|
||||
webhookId: '5340ae49-2c96-4492-9073-7744d2e52b8a',
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
});
|
||||
|
||||
describe('promptSaveUnsavedWorkflowChanges', () => {
|
||||
beforeAll(() => {
|
||||
setActivePinia(createTestingPinia());
|
||||
});
|
||||
describe('useWorkflowSaving', () => {
|
||||
let workflowsStore: ReturnType<typeof mockedStore<typeof useWorkflowsStore>>;
|
||||
let nodeTypesStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
setActivePinia(createTestingPinia({ stubActions: false }));
|
||||
|
||||
workflowsStore = mockedStore(useWorkflowsStore);
|
||||
|
||||
nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||
nodeTypesStore.setNodeTypes(nodeTypes);
|
||||
});
|
||||
|
||||
describe('promptSaveUnsavedWorkflowChanges', () => {
|
||||
it('should prompt the user to save changes and proceed if confirmed', async () => {
|
||||
const { promptSaveUnsavedWorkflowChanges } = useWorkflowSaving({ router });
|
||||
const workflow = createTestWorkflow({
|
||||
id: 'w0',
|
||||
nodes: [createTestNode({ type: CHAT_TRIGGER_NODE_TYPE, disabled: false })],
|
||||
active: true,
|
||||
});
|
||||
|
||||
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue(workflow);
|
||||
vi.spyOn(workflowsStore, 'updateWorkflow').mockResolvedValue(workflow);
|
||||
|
||||
workflowsStore.setWorkflow(workflow);
|
||||
|
||||
const next = vi.fn();
|
||||
const confirm = vi.fn().mockResolvedValue(true);
|
||||
const cancel = vi.fn();
|
||||
@@ -48,16 +109,25 @@ describe('promptSaveUnsavedWorkflowChanges', () => {
|
||||
const npsSurveyStore = useNpsSurveyStore();
|
||||
vi.spyOn(npsSurveyStore, 'fetchPromptsData').mockResolvedValue();
|
||||
|
||||
saveCurrentWorkflowSpy.mockResolvedValue(true);
|
||||
|
||||
// Mock message.confirm
|
||||
modalConfirmSpy.mockResolvedValue(MODAL_CONFIRM);
|
||||
|
||||
const resolveSpy = vi.fn();
|
||||
const resolveMarker = Symbol();
|
||||
resolveSpy.mockReturnValue(resolveMarker);
|
||||
const mockRouter = {
|
||||
resolve: resolveSpy,
|
||||
currentRoute: { value: { params: { name: workflow.id }, query: { parentFolderId: '' } } },
|
||||
};
|
||||
|
||||
const { promptSaveUnsavedWorkflowChanges } = useWorkflowSaving({
|
||||
router: mockRouter as never,
|
||||
});
|
||||
|
||||
await promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
|
||||
|
||||
expect(modalConfirmSpy).toHaveBeenCalled();
|
||||
expect(npsSurveyStore.fetchPromptsData).toHaveBeenCalled();
|
||||
expect(saveCurrentWorkflowSpy).toHaveBeenCalledWith({}, false);
|
||||
expect(uiStore.stateIsDirty).toEqual(false);
|
||||
|
||||
expect(confirm).toHaveBeenCalled();
|
||||
@@ -66,7 +136,6 @@ describe('promptSaveUnsavedWorkflowChanges', () => {
|
||||
});
|
||||
|
||||
it('should not proceed if the user cancels the confirmation modal', async () => {
|
||||
const { promptSaveUnsavedWorkflowChanges } = useWorkflowSaving({ router });
|
||||
const next = vi.fn();
|
||||
const confirm = vi.fn();
|
||||
const cancel = vi.fn();
|
||||
@@ -78,7 +147,10 @@ describe('promptSaveUnsavedWorkflowChanges', () => {
|
||||
// Mock message.confirm
|
||||
modalConfirmSpy.mockResolvedValue(MODAL_CANCEL);
|
||||
|
||||
await promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
|
||||
const workflowSaving = useWorkflowSaving({ router });
|
||||
const saveCurrentWorkflowSpy = vi.spyOn(workflowSaving, 'saveCurrentWorkflow');
|
||||
|
||||
await workflowSaving.promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
|
||||
|
||||
expect(modalConfirmSpy).toHaveBeenCalled();
|
||||
expect(saveCurrentWorkflowSpy).not.toHaveBeenCalled();
|
||||
@@ -90,7 +162,6 @@ describe('promptSaveUnsavedWorkflowChanges', () => {
|
||||
});
|
||||
|
||||
it('should restore the route if the modal is closed and the workflow is not new', async () => {
|
||||
const { promptSaveUnsavedWorkflowChanges } = useWorkflowSaving({ router });
|
||||
const next = vi.fn();
|
||||
const confirm = vi.fn();
|
||||
const cancel = vi.fn();
|
||||
@@ -106,7 +177,9 @@ describe('promptSaveUnsavedWorkflowChanges', () => {
|
||||
// Mock message.confirm
|
||||
modalConfirmSpy.mockResolvedValue('close');
|
||||
|
||||
await promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
|
||||
const workflowSaving = useWorkflowSaving({ router });
|
||||
const saveCurrentWorkflowSpy = vi.spyOn(workflowSaving, 'saveCurrentWorkflow');
|
||||
await workflowSaving.promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
|
||||
|
||||
expect(modalConfirmSpy).toHaveBeenCalled();
|
||||
expect(saveCurrentWorkflowSpy).not.toHaveBeenCalled();
|
||||
@@ -123,7 +196,6 @@ describe('promptSaveUnsavedWorkflowChanges', () => {
|
||||
});
|
||||
|
||||
it('should close modal if workflow is not new', async () => {
|
||||
const { promptSaveUnsavedWorkflowChanges } = useWorkflowSaving({ router });
|
||||
const next = vi.fn();
|
||||
const confirm = vi.fn();
|
||||
const cancel = vi.fn();
|
||||
@@ -138,7 +210,9 @@ describe('promptSaveUnsavedWorkflowChanges', () => {
|
||||
// Mock message.confirm
|
||||
modalConfirmSpy.mockResolvedValue('close');
|
||||
|
||||
await promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
|
||||
const workflowSaving = useWorkflowSaving({ router });
|
||||
const saveCurrentWorkflowSpy = vi.spyOn(workflowSaving, 'saveCurrentWorkflow');
|
||||
await workflowSaving.promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
|
||||
|
||||
expect(modalConfirmSpy).toHaveBeenCalled();
|
||||
expect(saveCurrentWorkflowSpy).not.toHaveBeenCalled();
|
||||
@@ -150,7 +224,6 @@ describe('promptSaveUnsavedWorkflowChanges', () => {
|
||||
});
|
||||
|
||||
it('should proceed without prompting if there are no unsaved changes', async () => {
|
||||
const { promptSaveUnsavedWorkflowChanges } = useWorkflowSaving({ router });
|
||||
const next = vi.fn();
|
||||
const confirm = vi.fn();
|
||||
const cancel = vi.fn();
|
||||
@@ -159,7 +232,9 @@ describe('promptSaveUnsavedWorkflowChanges', () => {
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = false;
|
||||
|
||||
await promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
|
||||
const workflowSaving = useWorkflowSaving({ router });
|
||||
const saveCurrentWorkflowSpy = vi.spyOn(workflowSaving, 'saveCurrentWorkflow');
|
||||
await workflowSaving.promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
|
||||
|
||||
expect(modalConfirmSpy).not.toHaveBeenCalled();
|
||||
expect(saveCurrentWorkflowSpy).not.toHaveBeenCalled();
|
||||
@@ -171,7 +246,22 @@ describe('promptSaveUnsavedWorkflowChanges', () => {
|
||||
});
|
||||
|
||||
it('should handle save failure and restore the route', async () => {
|
||||
const { promptSaveUnsavedWorkflowChanges } = useWorkflowSaving({ router });
|
||||
const workflow = createTestWorkflow({
|
||||
id: 'w0',
|
||||
nodes: [createTestNode({ type: CHAT_TRIGGER_NODE_TYPE, disabled: false })],
|
||||
active: true,
|
||||
});
|
||||
|
||||
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue(workflow);
|
||||
vi.spyOn(workflowsStore, 'updateWorkflow').mockResolvedValue(workflow);
|
||||
|
||||
workflowsStore.setWorkflow(workflow);
|
||||
|
||||
const updateWorkflowSpy = vi.spyOn(workflowsStore, 'updateWorkflow');
|
||||
updateWorkflowSpy.mockImplementation(() => {
|
||||
throw new Error();
|
||||
});
|
||||
|
||||
const next = vi.fn();
|
||||
const confirm = vi.fn();
|
||||
const cancel = vi.fn();
|
||||
@@ -180,28 +270,116 @@ describe('promptSaveUnsavedWorkflowChanges', () => {
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = true;
|
||||
|
||||
const workflowStore = useWorkflowsStore();
|
||||
const MOCK_ID = 'existing-workflow-id';
|
||||
workflowStore.workflow.id = MOCK_ID;
|
||||
|
||||
saveCurrentWorkflowSpy.mockResolvedValue(false);
|
||||
|
||||
// Mock message.confirm
|
||||
modalConfirmSpy.mockResolvedValue(MODAL_CONFIRM);
|
||||
|
||||
await promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
|
||||
const resolveSpy = vi.fn();
|
||||
const resolveMarker = Symbol();
|
||||
resolveSpy.mockReturnValue(resolveMarker);
|
||||
const mockRouter = {
|
||||
resolve: resolveSpy,
|
||||
currentRoute: { value: { params: { name: workflow.id }, query: { parentFolderId: '' } } },
|
||||
};
|
||||
|
||||
const workflowSaving = useWorkflowSaving({ router: mockRouter as never });
|
||||
await workflowSaving.promptSaveUnsavedWorkflowChanges(next, { confirm, cancel });
|
||||
|
||||
expect(modalConfirmSpy).toHaveBeenCalled();
|
||||
expect(saveCurrentWorkflowSpy).toHaveBeenCalledWith({}, false);
|
||||
expect(updateWorkflowSpy).toBeCalled();
|
||||
expect(uiStore.stateIsDirty).toEqual(true);
|
||||
|
||||
expect(confirm).not.toHaveBeenCalled();
|
||||
expect(cancel).not.toHaveBeenCalled();
|
||||
expect(next).toHaveBeenCalledWith(
|
||||
router.resolve({
|
||||
name: VIEWS.WORKFLOW,
|
||||
params: { name: MOCK_ID },
|
||||
}),
|
||||
expect(next).toHaveBeenCalledWith(resolveMarker);
|
||||
});
|
||||
});
|
||||
describe('saveAsNewWorkflow', () => {
|
||||
it('should respect `resetWebhookUrls: false` when duplicating workflows', async () => {
|
||||
const workflow = getDuplicateTestWorkflow();
|
||||
if (!workflow.nodes) {
|
||||
throw new Error('Missing nodes in test workflow');
|
||||
}
|
||||
const { saveAsNewWorkflow } = useWorkflowSaving({ router });
|
||||
const webHookIdsPreSave = workflow.nodes.map((node) => node.webhookId);
|
||||
const pathsPreSave = workflow.nodes.map((node) => node.parameters.path);
|
||||
|
||||
await saveAsNewWorkflow({
|
||||
name: workflow.name,
|
||||
resetWebhookUrls: false,
|
||||
data: workflow,
|
||||
});
|
||||
|
||||
const webHookIdsPostSave = workflow.nodes.map((node) => node.webhookId);
|
||||
const pathsPostSave = workflow.nodes.map((node) => node.parameters.path);
|
||||
// Expect webhookIds and paths to be the same as in the original workflow
|
||||
expect(webHookIdsPreSave).toEqual(webHookIdsPostSave);
|
||||
expect(pathsPreSave).toEqual(pathsPostSave);
|
||||
});
|
||||
|
||||
it('should respect `resetWebhookUrls: true` when duplicating workflows', async () => {
|
||||
const workflow = getDuplicateTestWorkflow();
|
||||
if (!workflow.nodes) {
|
||||
throw new Error('Missing nodes in test workflow');
|
||||
}
|
||||
const { saveAsNewWorkflow } = useWorkflowSaving({ router });
|
||||
const webHookIdsPreSave = workflow.nodes.map((node) => node.webhookId);
|
||||
const pathsPreSave = workflow.nodes.map((node) => node.parameters.path);
|
||||
|
||||
await saveAsNewWorkflow({
|
||||
name: workflow.name,
|
||||
resetWebhookUrls: true,
|
||||
data: workflow,
|
||||
});
|
||||
|
||||
const webHookIdsPostSave = workflow.nodes.map((node) => node.webhookId);
|
||||
const pathsPostSave = workflow.nodes.map((node) => node.parameters.path);
|
||||
// Now, expect webhookIds and paths to be different
|
||||
expect(webHookIdsPreSave).not.toEqual(webHookIdsPostSave);
|
||||
expect(pathsPreSave).not.toEqual(pathsPostSave);
|
||||
});
|
||||
});
|
||||
describe('saveCurrentWorkflow', () => {
|
||||
it('should save the current workflow', async () => {
|
||||
const workflow = createTestWorkflow({
|
||||
id: 'w0',
|
||||
nodes: [createTestNode({ type: CHAT_TRIGGER_NODE_TYPE, disabled: false })],
|
||||
active: true,
|
||||
});
|
||||
|
||||
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue(workflow);
|
||||
vi.spyOn(workflowsStore, 'updateWorkflow').mockResolvedValue(workflow);
|
||||
|
||||
workflowsStore.setWorkflow(workflow);
|
||||
|
||||
const { saveCurrentWorkflow } = useWorkflowSaving({ router });
|
||||
await saveCurrentWorkflow({ id: 'w0' });
|
||||
expect(workflowsStore.updateWorkflow).toHaveBeenCalledWith(
|
||||
'w0',
|
||||
expect.objectContaining({ id: 'w0', active: true }),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should include active=false in the request if the workflow has no activatable trigger node', async () => {
|
||||
const workflow = createTestWorkflow({
|
||||
id: 'w1',
|
||||
nodes: [createTestNode({ type: CHAT_TRIGGER_NODE_TYPE, disabled: true })],
|
||||
active: true,
|
||||
});
|
||||
|
||||
vi.spyOn(workflowsStore, 'fetchWorkflow').mockResolvedValue(workflow);
|
||||
vi.spyOn(workflowsStore, 'updateWorkflow').mockResolvedValue(workflow);
|
||||
|
||||
workflowsStore.setWorkflow(workflow);
|
||||
|
||||
const { saveCurrentWorkflow } = useWorkflowSaving({ router });
|
||||
await saveCurrentWorkflow({ id: 'w1' });
|
||||
expect(workflowsStore.updateWorkflow).toHaveBeenCalledWith(
|
||||
'w1',
|
||||
expect.objectContaining({ id: 'w1', active: false }),
|
||||
false,
|
||||
);
|
||||
expect(workflowsStore.setWorkflowInactive).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,11 +7,29 @@ import {
|
||||
MODAL_CANCEL,
|
||||
MODAL_CLOSE,
|
||||
MODAL_CONFIRM,
|
||||
NON_ACTIVATABLE_TRIGGER_NODE_TYPES,
|
||||
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
||||
VIEWS,
|
||||
} from '@/constants';
|
||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { useCanvasStore } from '@/stores/canvas.store';
|
||||
import type {
|
||||
ITag,
|
||||
IUpdateInformation,
|
||||
IWorkflowDataCreate,
|
||||
IWorkflowDataUpdate,
|
||||
NotificationOptions,
|
||||
} from '@/Interface';
|
||||
import type { IDataObject, INode, IWorkflowSettings } from 'n8n-workflow';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useToast } from './useToast';
|
||||
import { useExternalHooks } from './useExternalHooks';
|
||||
import { useTelemetry } from './useTelemetry';
|
||||
import { useNodeHelpers } from './useNodeHelpers';
|
||||
import { tryToParseNumber } from '@/utils/typesUtils';
|
||||
import { useTemplatesStore } from '@/stores/templates.store';
|
||||
|
||||
export function useWorkflowSaving({ router }: { router: ReturnType<typeof useRouter> }) {
|
||||
const uiStore = useUIStore();
|
||||
@@ -19,8 +37,18 @@ export function useWorkflowSaving({ router }: { router: ReturnType<typeof useRou
|
||||
const message = useMessage();
|
||||
const i18n = useI18n();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const toast = useToast();
|
||||
const telemetry = useTelemetry();
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const templatesStore = useTemplatesStore();
|
||||
|
||||
const { saveCurrentWorkflow } = useWorkflowHelpers({ router });
|
||||
const {
|
||||
getWorkflowDataToSave,
|
||||
checkConflictingWebhooks,
|
||||
getWorkflowProjectRole,
|
||||
getCurrentWorkflow,
|
||||
} = useWorkflowHelpers();
|
||||
|
||||
async function promptSaveUnsavedWorkflowChanges(
|
||||
next: NavigationGuardNext,
|
||||
@@ -51,6 +79,7 @@ export function useWorkflowSaving({ router }: { router: ReturnType<typeof useRou
|
||||
switch (response) {
|
||||
case MODAL_CONFIRM:
|
||||
const saved = await saveCurrentWorkflow({}, false);
|
||||
|
||||
if (saved) {
|
||||
await npsSurveyStore.fetchPromptsData();
|
||||
uiStore.stateIsDirty = false;
|
||||
@@ -89,7 +118,320 @@ export function useWorkflowSaving({ router }: { router: ReturnType<typeof useRou
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
promptSaveUnsavedWorkflowChanges,
|
||||
saveCurrentWorkflow,
|
||||
saveAsNewWorkflow,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
import type { EditorView } from '@codemirror/view';
|
||||
import { EditorSelection, type TransactionSpec } from '@codemirror/state';
|
||||
import type { SyntaxNode, Tree } from '@lezer/common';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { DocMetadata } from 'n8n-workflow';
|
||||
import { escapeMappingString } from '@/utils/mappingUtils';
|
||||
|
||||
@@ -210,7 +209,7 @@ export function autocompletableNodeNames() {
|
||||
|
||||
const activeNodeName = activeNode.name;
|
||||
|
||||
const workflow = useWorkflowHelpers({ router: useRouter() }).getCurrentWorkflow();
|
||||
const workflow = useWorkflowHelpers().getCurrentWorkflow();
|
||||
const nonMainChildren = workflow.getChildNodes(activeNodeName, 'ALL_NON_MAIN');
|
||||
|
||||
// This is a tool node, look for the nearest node with main connections
|
||||
@@ -222,7 +221,7 @@ export function autocompletableNodeNames() {
|
||||
}
|
||||
|
||||
export function getPreviousNodes(nodeName: string) {
|
||||
const workflow = useWorkflowHelpers({ router: useRouter() }).getCurrentWorkflow();
|
||||
const workflow = useWorkflowHelpers().getCurrentWorkflow();
|
||||
return workflow
|
||||
.getParentNodesByDepth(nodeName)
|
||||
.map((node) => node.name)
|
||||
|
||||
@@ -88,7 +88,6 @@ import type { ProjectSharingData } from '@/types/projects.types';
|
||||
import type { PushPayload } from '@n8n/api-types';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useSettingsStore } from './settings.store';
|
||||
import { clearPopupWindowState, openFormPopupWindow } from '@/utils/executionUtils';
|
||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||
@@ -126,8 +125,7 @@ let cachedWorkflow: Workflow | null = null;
|
||||
export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
const uiStore = useUIStore();
|
||||
const telemetry = useTelemetry();
|
||||
const router = useRouter();
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const settingsStore = useSettingsStore();
|
||||
const rootStore = useRootStore();
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useCanvasOperations } from '@/composables/useCanvasOperations';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useEvaluationStore } from '@/stores/evaluation.store.ee';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
|
||||
@@ -25,11 +24,10 @@ const usageStore = useUsageStore();
|
||||
const evaluationStore = useEvaluationStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const telemetry = useTelemetry();
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
const locale = useI18n();
|
||||
|
||||
const { initializeWorkspace } = useCanvasOperations({ router });
|
||||
const { initializeWorkspace } = useCanvasOperations();
|
||||
|
||||
const evaluationsLicensed = computed(() => {
|
||||
return usageStore.workflowsWithEvaluationsLimit !== 0;
|
||||
|
||||
@@ -161,7 +161,8 @@ const externalHooks = useExternalHooks();
|
||||
const toast = useToast();
|
||||
const message = useMessage();
|
||||
const documentTitle = useDocumentTitle();
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowHelpers = useWorkflowHelpers();
|
||||
const workflowSaving = useWorkflowSaving({ router });
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
@@ -237,7 +238,7 @@ const {
|
||||
editableWorkflowObject,
|
||||
lastClickPosition,
|
||||
startChat,
|
||||
} = useCanvasOperations({ router });
|
||||
} = useCanvasOperations();
|
||||
const { extractWorkflow } = useWorkflowExtraction();
|
||||
const { applyExecutionData } = useExecutionDebugging();
|
||||
useClipboard({ onPaste: onClipboardPaste });
|
||||
@@ -818,7 +819,7 @@ async function onSaveWorkflow() {
|
||||
if (workflowIsSaved || workflowIsArchived) {
|
||||
return;
|
||||
}
|
||||
const saved = await workflowHelpers.saveCurrentWorkflow();
|
||||
const saved = await workflowSaving.saveCurrentWorkflow();
|
||||
if (saved) {
|
||||
canvasEventBus.emit('saved:workflow');
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ const router = useRouter();
|
||||
const toast = useToast();
|
||||
const { callDebounced } = useDebounce();
|
||||
|
||||
const { initializeWorkspace } = useCanvasOperations({ router });
|
||||
const { initializeWorkspace } = useCanvasOperations();
|
||||
|
||||
const loading = ref(false);
|
||||
const loadingMore = ref(false);
|
||||
|
||||
Reference in New Issue
Block a user