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