mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-12 13:27:31 -08:00
feat(editor): Add capability to open workflow from template in new canvas (no-changelog) (#10011)
This commit is contained in:
parent
2d19aef540
commit
8171d75f5d
|
@ -8,6 +8,8 @@ import { MiniMap } from '@vue-flow/minimap';
|
||||||
import Node from './elements/nodes/CanvasNode.vue';
|
import Node from './elements/nodes/CanvasNode.vue';
|
||||||
import Edge from './elements/edges/CanvasEdge.vue';
|
import Edge from './elements/edges/CanvasEdge.vue';
|
||||||
import { onMounted, onUnmounted, ref, useCssModule } from 'vue';
|
import { onMounted, onUnmounted, ref, useCssModule } from 'vue';
|
||||||
|
import type { EventBus } from 'n8n-design-system';
|
||||||
|
import { createEventBus } from 'n8n-design-system';
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
|
||||||
|
@ -34,28 +36,24 @@ const props = withDefaults(
|
||||||
nodes: CanvasNode[];
|
nodes: CanvasNode[];
|
||||||
connections: CanvasConnection[];
|
connections: CanvasConnection[];
|
||||||
controlsPosition?: PanelPosition;
|
controlsPosition?: PanelPosition;
|
||||||
|
eventBus?: EventBus;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
id: 'canvas',
|
id: 'canvas',
|
||||||
nodes: () => [],
|
nodes: () => [],
|
||||||
connections: () => [],
|
connections: () => [],
|
||||||
controlsPosition: PanelPosition.BottomLeft,
|
controlsPosition: PanelPosition.BottomLeft,
|
||||||
|
eventBus: () => createEventBus(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { getSelectedEdges, getSelectedNodes, viewportRef, project } = useVueFlow({
|
const { getSelectedEdges, getSelectedNodes, viewportRef, fitView, project } = useVueFlow({
|
||||||
id: props.id,
|
id: props.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const hoveredEdges = ref<Record<string, boolean>>({});
|
/**
|
||||||
|
* Nodes
|
||||||
onMounted(() => {
|
*/
|
||||||
document.addEventListener('keydown', onKeyDown);
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
document.removeEventListener('keydown', onKeyDown);
|
|
||||||
});
|
|
||||||
|
|
||||||
function onNodeDragStop(e: NodeDragEvent) {
|
function onNodeDragStop(e: NodeDragEvent) {
|
||||||
e.nodes.forEach((node) => {
|
e.nodes.forEach((node) => {
|
||||||
|
@ -128,16 +126,11 @@ function onClickConnectionAdd(connection: Connection) {
|
||||||
emit('click:connection:add', connection);
|
emit('click:connection:add', connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRunNode(id: string) {
|
/**
|
||||||
emit('run:node', id);
|
* Connection hover
|
||||||
}
|
*/
|
||||||
|
|
||||||
function onKeyDown(e: KeyboardEvent) {
|
const hoveredEdges = ref<Record<string, boolean>>({});
|
||||||
if (e.key === 'Delete') {
|
|
||||||
getSelectedEdges.value.forEach(onDeleteConnection);
|
|
||||||
getSelectedNodes.value.forEach(({ id }) => onDeleteNode(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMouseEnterEdge(event: EdgeMouseEvent) {
|
function onMouseEnterEdge(event: EdgeMouseEvent) {
|
||||||
hoveredEdges.value[event.edge.id] = true;
|
hoveredEdges.value[event.edge.id] = true;
|
||||||
|
@ -147,6 +140,29 @@ function onMouseLeaveEdge(event: EdgeMouseEvent) {
|
||||||
hoveredEdges.value[event.edge.id] = false;
|
hoveredEdges.value[event.edge.id] = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executions
|
||||||
|
*/
|
||||||
|
|
||||||
|
function onRunNode(id: string) {
|
||||||
|
emit('run:node', id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keyboard events
|
||||||
|
*/
|
||||||
|
|
||||||
|
function onKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Delete') {
|
||||||
|
getSelectedEdges.value.forEach(onDeleteConnection);
|
||||||
|
getSelectedNodes.value.forEach(({ id }) => onDeleteNode(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View
|
||||||
|
*/
|
||||||
|
|
||||||
function onClickPane(event: MouseEvent) {
|
function onClickPane(event: MouseEvent) {
|
||||||
const bounds = viewportRef.value?.getBoundingClientRect() ?? { left: 0, top: 0 };
|
const bounds = viewportRef.value?.getBoundingClientRect() ?? { left: 0, top: 0 };
|
||||||
const position = project({
|
const position = project({
|
||||||
|
@ -156,6 +172,24 @@ function onClickPane(event: MouseEvent) {
|
||||||
|
|
||||||
emit('click:pane', position);
|
emit('click:pane', position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onFitView() {
|
||||||
|
await fitView();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle
|
||||||
|
*/
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('keydown', onKeyDown);
|
||||||
|
props.eventBus.on('fitView', onFitView);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
props.eventBus.off('fitView', onFitView);
|
||||||
|
document.removeEventListener('keydown', onKeyDown);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { computed, toRef, useCssModule } from 'vue';
|
||||||
import type { Workflow } from 'n8n-workflow';
|
import type { Workflow } from 'n8n-workflow';
|
||||||
import type { IWorkflowDb } from '@/Interface';
|
import type { IWorkflowDb } from '@/Interface';
|
||||||
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
||||||
|
import type { EventBus } from 'n8n-design-system';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
|
@ -15,6 +16,7 @@ const props = withDefaults(
|
||||||
workflow: IWorkflowDb;
|
workflow: IWorkflowDb;
|
||||||
workflowObject: Workflow;
|
workflowObject: Workflow;
|
||||||
fallbackNodes?: IWorkflowDb['nodes'];
|
fallbackNodes?: IWorkflowDb['nodes'];
|
||||||
|
eventBus?: EventBus;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
id: 'canvas',
|
id: 'canvas',
|
||||||
|
@ -46,6 +48,7 @@ const { nodes: mappedNodes, connections: mappedConnections } = useCanvasMapping(
|
||||||
v-if="workflow"
|
v-if="workflow"
|
||||||
:nodes="mappedNodes"
|
:nodes="mappedNodes"
|
||||||
:connections="mappedConnections"
|
:connections="mappedConnections"
|
||||||
|
:event-bus="eventBus"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -23,6 +23,7 @@ import type {
|
||||||
IUpdateInformation,
|
IUpdateInformation,
|
||||||
IWorkflowDataUpdate,
|
IWorkflowDataUpdate,
|
||||||
IWorkflowDb,
|
IWorkflowDb,
|
||||||
|
IWorkflowTemplate,
|
||||||
ToggleNodeCreatorOptions,
|
ToggleNodeCreatorOptions,
|
||||||
XYPosition,
|
XYPosition,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
|
@ -84,6 +85,9 @@ import CanvasStopCurrentExecutionButton from '@/components/canvas/elements/butto
|
||||||
import CanvasStopWaitingForWebhookButton from '@/components/canvas/elements/buttons/CanvasStopWaitingForWebhookButton.vue';
|
import CanvasStopWaitingForWebhookButton from '@/components/canvas/elements/buttons/CanvasStopWaitingForWebhookButton.vue';
|
||||||
import CanvasClearExecutionDataButton from '@/components/canvas/elements/buttons/CanvasClearExecutionDataButton.vue';
|
import CanvasClearExecutionDataButton from '@/components/canvas/elements/buttons/CanvasClearExecutionDataButton.vue';
|
||||||
import { nodeViewEventBus } from '@/event-bus';
|
import { nodeViewEventBus } from '@/event-bus';
|
||||||
|
import { tryToParseNumber } from '@/utils/typesUtils';
|
||||||
|
import { useTemplatesStore } from '@/stores/templates.store';
|
||||||
|
import { createEventBus } from 'n8n-design-system';
|
||||||
|
|
||||||
const NodeCreation = defineAsyncComponent(
|
const NodeCreation = defineAsyncComponent(
|
||||||
async () => await import('@/components/Node/NodeCreation.vue'),
|
async () => await import('@/components/Node/NodeCreation.vue'),
|
||||||
|
@ -126,6 +130,9 @@ const usersStore = useUsersStore();
|
||||||
const tagsStore = useTagsStore();
|
const tagsStore = useTagsStore();
|
||||||
const pushConnectionStore = usePushConnectionStore();
|
const pushConnectionStore = usePushConnectionStore();
|
||||||
const ndvStore = useNDVStore();
|
const ndvStore = useNDVStore();
|
||||||
|
const templatesStore = useTemplatesStore();
|
||||||
|
|
||||||
|
const canvasEventBus = createEventBus();
|
||||||
|
|
||||||
const lastClickPosition = ref<XYPosition>([450, 450]);
|
const lastClickPosition = ref<XYPosition>([450, 450]);
|
||||||
|
|
||||||
|
@ -267,9 +274,8 @@ async function initializeView() {
|
||||||
if (isBlankRedirect.value) {
|
if (isBlankRedirect.value) {
|
||||||
isBlankRedirect.value = false;
|
isBlankRedirect.value = false;
|
||||||
} else if (route.name === VIEWS.TEMPLATE_IMPORT) {
|
} else if (route.name === VIEWS.TEMPLATE_IMPORT) {
|
||||||
// @TODO Implement template import
|
const templateId = route.params.id;
|
||||||
// const templateId = route.params.id;
|
await openWorkflowTemplate(templateId.toString());
|
||||||
// await openWorkflowTemplate(templateId.toString());
|
|
||||||
} else {
|
} else {
|
||||||
historyStore.reset();
|
historyStore.reset();
|
||||||
|
|
||||||
|
@ -432,6 +438,75 @@ function makeNewWorkflowShareable() {
|
||||||
workflowsStore.workflow.scopes = scopes;
|
workflowsStore.workflow.scopes = scopes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Templates
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function openWorkflowTemplate(templateId: string) {
|
||||||
|
resetWorkspace();
|
||||||
|
|
||||||
|
canvasStore.startLoading();
|
||||||
|
canvasStore.setLoadingText(i18n.baseText('nodeView.loadingTemplate'));
|
||||||
|
|
||||||
|
workflowsStore.currentWorkflowExecutions = [];
|
||||||
|
executionsStore.activeExecution = null;
|
||||||
|
|
||||||
|
let data: IWorkflowTemplate | undefined;
|
||||||
|
try {
|
||||||
|
void externalHooks.run('template.requested', { templateId });
|
||||||
|
|
||||||
|
data = await templatesStore.getFixedWorkflowTemplate(templateId);
|
||||||
|
if (!data) {
|
||||||
|
throw new Error(
|
||||||
|
i18n.baseText('nodeView.workflowTemplateWithIdCouldNotBeFound', {
|
||||||
|
interpolate: { templateId },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.showError(error, i18n.baseText('nodeView.couldntImportWorkflow'));
|
||||||
|
await router.replace({ name: VIEWS.NEW_WORKFLOW });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
trackOpenWorkflowTemplate(templateId);
|
||||||
|
|
||||||
|
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 });
|
||||||
|
|
||||||
|
uiStore.stateIsDirty = true;
|
||||||
|
|
||||||
|
canvasEventBus.emit('fitView');
|
||||||
|
canvasStore.stopLoading();
|
||||||
|
|
||||||
|
void externalHooks.run('template.open', {
|
||||||
|
templateId,
|
||||||
|
templateName: data.name,
|
||||||
|
workflow: data.workflow,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function trackOpenWorkflowTemplate(templateId: string) {
|
||||||
|
telemetry.track(
|
||||||
|
'User inserted workflow template',
|
||||||
|
{
|
||||||
|
source: 'workflow',
|
||||||
|
template_id: tryToParseNumber(templateId),
|
||||||
|
wf_template_repo_session_id: templatesStore.previousSessionId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
withPostHog: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nodes
|
* Nodes
|
||||||
*/
|
*/
|
||||||
|
@ -1077,6 +1152,7 @@ onBeforeUnmount(() => {
|
||||||
:workflow="editableWorkflow"
|
:workflow="editableWorkflow"
|
||||||
:workflow-object="editableWorkflowObject"
|
:workflow-object="editableWorkflowObject"
|
||||||
:fallback-nodes="fallbackNodes"
|
:fallback-nodes="fallbackNodes"
|
||||||
|
:event-bus="canvasEventBus"
|
||||||
@update:node:position="onUpdateNodePosition"
|
@update:node:position="onUpdateNodePosition"
|
||||||
@update:node:active="onSetNodeActive"
|
@update:node:active="onSetNodeActive"
|
||||||
@update:node:selected="onSetNodeSelected"
|
@update:node:selected="onSetNodeSelected"
|
||||||
|
|
Loading…
Reference in a new issue