Skip to content

Commit

Permalink
fix(editor): Add template id to metadata when saving workflows from j…
Browse files Browse the repository at this point in the history
…son (#13172)
  • Loading branch information
mutdmour authored Feb 13, 2025
1 parent 4f8dd3d commit 2a92032
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 13 deletions.
76 changes: 75 additions & 1 deletion packages/editor-ui/src/composables/useCanvasOperations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
import { useCanvasOperations } from '@/composables/useCanvasOperations';
import type { CanvasConnection, CanvasNode } from '@/types';
import { CanvasConnectionMode } from '@/types';
import type { ICredentialsResponse, IExecutionResponse, INodeUi, IWorkflowDb } from '@/Interface';
import type {
ICredentialsResponse,
IExecutionResponse,
INodeUi,
IWorkflowDb,
IWorkflowTemplate,
IWorkflowTemplateNode,
} from '@/Interface';
import { RemoveNodeCommand } from '@/models/history';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useUIStore } from '@/stores/ui.store';
Expand Down Expand Up @@ -43,6 +50,7 @@ import type { Connection } from '@vue-flow/core';
import { useClipboard } from '@/composables/useClipboard';
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
import { nextTick } from 'vue';
import { useProjectsStore } from '@/stores/projects.store';

vi.mock('vue-router', async (importOriginal) => {
const actual = await importOriginal<{}>();
Expand Down Expand Up @@ -2848,6 +2856,72 @@ describe('useCanvasOperations', () => {
expect(workflowsStore.setPanelOpen).toHaveBeenCalledWith('chat', false);
});
});

describe('importTemplate', () => {
it('should import template to canvas', async () => {
const projectsStore = mockedStore(useProjectsStore);
projectsStore.currentProjectId = 'test-project-id';

const workflowsStore = mockedStore(useWorkflowsStore);
workflowsStore.convertTemplateNodeToNodeUi.mockImplementation((node) => ({
...node,
credentials: {},
}));

const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);

// Create nodes: A -> B (no outgoing from B)
const nodeA: IWorkflowTemplateNode = createTestNode({
id: 'X',
name: 'Node X',
position: [80, 80],
});
const nodeB: IWorkflowTemplateNode = createTestNode({
id: 'Y',
name: 'Node Y',
position: [180, 80],
});

const workflow: IWorkflowTemplate['workflow'] = {
nodes: [nodeA, nodeB],
connections: {
[nodeA.name]: {
main: [[{ node: nodeB.name, type: NodeConnectionType.Main, index: 0 }]],
},
},
};

const { importTemplate } = useCanvasOperations({ router });

const templateId = 'template-id';
const templateName = 'template name';
await importTemplate({
id: templateId,
name: templateName,
workflow,
});

expect(workflowsStore.setConnections).toHaveBeenCalledWith(workflow.connections);
expect(workflowsStore.addNode).toHaveBeenCalledWith({
...nodeA,
credentials: {},
disabled: false,
});
expect(workflowsStore.setNodePristine).toHaveBeenCalledWith(nodeA.name, true);
expect(workflowsStore.addNode).toHaveBeenCalledWith({
...nodeB,
credentials: {},
disabled: false,
});
expect(workflowsStore.setNodePristine).toHaveBeenCalledWith(nodeB.name, true);
expect(workflowsStore.getNewWorkflowData).toHaveBeenCalledWith(
templateName,
projectsStore.currentProjectId,
);
expect(workflowsStore.addToWorkflowMetadata).toHaveBeenCalledWith({ templateId });
});
});
});

function buildImportNodes() {
Expand Down
24 changes: 24 additions & 0 deletions packages/editor-ui/src/composables/useCanvasOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import type {
IWorkflowData,
IWorkflowDataUpdate,
IWorkflowDb,
IWorkflowTemplate,
WorkflowDataWithTemplateId,
XYPosition,
} from '@/Interface';
import { useDataSchema } from '@/composables/useDataSchema';
Expand Down Expand Up @@ -96,6 +98,7 @@ import type { useRouter } from 'vue-router';
import { useClipboard } from '@/composables/useClipboard';
import { useUniqueNodeName } from '@/composables/useUniqueNodeName';
import { isPresent } from '../utils/typesUtils';
import { useProjectsStore } from '@/stores/projects.store';

type AddNodeData = Partial<INodeUi> & {
type: string;
Expand Down Expand Up @@ -135,6 +138,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
const tagsStore = useTagsStore();
const nodeCreatorStore = useNodeCreatorStore();
const executionsStore = useExecutionsStore();
const projectsStore = useProjectsStore();

const i18n = useI18n();
const toast = useToast();
Expand Down Expand Up @@ -1950,6 +1954,25 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
telemetry.track('User clicked chat open button', payload);
}

async function importTemplate({
id,
name,
workflow,
}: {
id: string | number;
name?: string;
workflow: IWorkflowTemplate['workflow'] | WorkflowDataWithTemplateId;
}) {
const convertedNodes = workflow.nodes?.map(workflowsStore.convertTemplateNodeToNodeUi);

if (workflow.connections) {
workflowsStore.setConnections(workflow.connections);
}
await addNodes(convertedNodes ?? []);
await workflowsStore.getNewWorkflowData(name, projectsStore.currentProjectId);
workflowsStore.addToWorkflowMetadata({ templateId: `${id}` });
}

return {
lastClickPosition,
editableWorkflow,
Expand Down Expand Up @@ -1997,5 +2020,6 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
resolveNodeWebhook,
openExecution,
toggleChatOpen,
importTemplate,
};
}
17 changes: 5 additions & 12 deletions packages/editor-ui/src/views/NodeView.v2.vue
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ const {
duplicateNodes,
revertDeleteNode,
addNodes,
importTemplate,
revertAddNode,
createConnection,
revertCreateConnection,
Expand Down Expand Up @@ -476,16 +477,13 @@ async function openTemplateFromWorkflowJSON(workflow: WorkflowDataWithTemplateId
executionsStore.activeExecution = null;
isBlankRedirect.value = true;
const templateId = workflow.meta.templateId;
await router.replace({
name: VIEWS.NEW_WORKFLOW,
query: { templateId: workflow.meta.templateId },
query: { templateId },
});
const convertedNodes = workflow.nodes.map(workflowsStore.convertTemplateNodeToNodeUi);
workflowsStore.setConnections(workflow.connections);
await addNodes(convertedNodes);
await workflowsStore.getNewWorkflowData(workflow.name, projectsStore.currentProjectId);
await importTemplate({ id: templateId, name: workflow.name, workflow });
uiStore.stateIsDirty = true;
Expand Down Expand Up @@ -526,12 +524,7 @@ async function openWorkflowTemplate(templateId: string) {
isBlankRedirect.value = true;
await router.replace({ name: VIEWS.NEW_WORKFLOW, query: { templateId } });
const convertedNodes = data.workflow.nodes.map(workflowsStore.convertTemplateNodeToNodeUi);
workflowsStore.setConnections(data.workflow.connections);
await addNodes(convertedNodes);
await workflowsStore.getNewWorkflowData(data.name, projectsStore.currentProjectId);
workflowsStore.addToWorkflowMetadata({ templateId });
await importTemplate({ id: templateId, name: data.name, workflow: data.workflow });
uiStore.stateIsDirty = true;
Expand Down

0 comments on commit 2a92032

Please sign in to comment.