mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
fix(editor): Add template id to metadata when saving workflows from json (#13172)
This commit is contained in:
@@ -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';
|
||||
@@ -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<{}>();
|
||||
@@ -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() {
|
||||
|
||||
@@ -12,6 +12,8 @@ import type {
|
||||
IWorkflowData,
|
||||
IWorkflowDataUpdate,
|
||||
IWorkflowDb,
|
||||
IWorkflowTemplate,
|
||||
WorkflowDataWithTemplateId,
|
||||
XYPosition,
|
||||
} from '@/Interface';
|
||||
import { useDataSchema } from '@/composables/useDataSchema';
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
@@ -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,
|
||||
@@ -1997,5 +2020,6 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||
resolveNodeWebhook,
|
||||
openExecution,
|
||||
toggleChatOpen,
|
||||
importTemplate,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -187,6 +187,7 @@ const {
|
||||
duplicateNodes,
|
||||
revertDeleteNode,
|
||||
addNodes,
|
||||
importTemplate,
|
||||
revertAddNode,
|
||||
createConnection,
|
||||
revertCreateConnection,
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user