mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -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 Edge from './elements/edges/CanvasEdge.vue';
|
||||
import { onMounted, onUnmounted, ref, useCssModule } from 'vue';
|
||||
import type { EventBus } from 'n8n-design-system';
|
||||
import { createEventBus } from 'n8n-design-system';
|
||||
|
||||
const $style = useCssModule();
|
||||
|
||||
|
@ -34,28 +36,24 @@ const props = withDefaults(
|
|||
nodes: CanvasNode[];
|
||||
connections: CanvasConnection[];
|
||||
controlsPosition?: PanelPosition;
|
||||
eventBus?: EventBus;
|
||||
}>(),
|
||||
{
|
||||
id: 'canvas',
|
||||
nodes: () => [],
|
||||
connections: () => [],
|
||||
controlsPosition: PanelPosition.BottomLeft,
|
||||
eventBus: () => createEventBus(),
|
||||
},
|
||||
);
|
||||
|
||||
const { getSelectedEdges, getSelectedNodes, viewportRef, project } = useVueFlow({
|
||||
const { getSelectedEdges, getSelectedNodes, viewportRef, fitView, project } = useVueFlow({
|
||||
id: props.id,
|
||||
});
|
||||
|
||||
const hoveredEdges = ref<Record<string, boolean>>({});
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', onKeyDown);
|
||||
});
|
||||
/**
|
||||
* Nodes
|
||||
*/
|
||||
|
||||
function onNodeDragStop(e: NodeDragEvent) {
|
||||
e.nodes.forEach((node) => {
|
||||
|
@ -128,16 +126,11 @@ function onClickConnectionAdd(connection: Connection) {
|
|||
emit('click:connection:add', connection);
|
||||
}
|
||||
|
||||
function onRunNode(id: string) {
|
||||
emit('run:node', id);
|
||||
}
|
||||
/**
|
||||
* Connection hover
|
||||
*/
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'Delete') {
|
||||
getSelectedEdges.value.forEach(onDeleteConnection);
|
||||
getSelectedNodes.value.forEach(({ id }) => onDeleteNode(id));
|
||||
}
|
||||
}
|
||||
const hoveredEdges = ref<Record<string, boolean>>({});
|
||||
|
||||
function onMouseEnterEdge(event: EdgeMouseEvent) {
|
||||
hoveredEdges.value[event.edge.id] = true;
|
||||
|
@ -147,6 +140,29 @@ function onMouseLeaveEdge(event: EdgeMouseEvent) {
|
|||
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) {
|
||||
const bounds = viewportRef.value?.getBoundingClientRect() ?? { left: 0, top: 0 };
|
||||
const position = project({
|
||||
|
@ -156,6 +172,24 @@ function onClickPane(event: MouseEvent) {
|
|||
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { computed, toRef, useCssModule } from 'vue';
|
|||
import type { Workflow } from 'n8n-workflow';
|
||||
import type { IWorkflowDb } from '@/Interface';
|
||||
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
||||
import type { EventBus } from 'n8n-design-system';
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
|
@ -15,6 +16,7 @@ const props = withDefaults(
|
|||
workflow: IWorkflowDb;
|
||||
workflowObject: Workflow;
|
||||
fallbackNodes?: IWorkflowDb['nodes'];
|
||||
eventBus?: EventBus;
|
||||
}>(),
|
||||
{
|
||||
id: 'canvas',
|
||||
|
@ -46,6 +48,7 @@ const { nodes: mappedNodes, connections: mappedConnections } = useCanvasMapping(
|
|||
v-if="workflow"
|
||||
:nodes="mappedNodes"
|
||||
:connections="mappedConnections"
|
||||
:event-bus="eventBus"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -23,6 +23,7 @@ import type {
|
|||
IUpdateInformation,
|
||||
IWorkflowDataUpdate,
|
||||
IWorkflowDb,
|
||||
IWorkflowTemplate,
|
||||
ToggleNodeCreatorOptions,
|
||||
XYPosition,
|
||||
} from '@/Interface';
|
||||
|
@ -84,6 +85,9 @@ import CanvasStopCurrentExecutionButton from '@/components/canvas/elements/butto
|
|||
import CanvasStopWaitingForWebhookButton from '@/components/canvas/elements/buttons/CanvasStopWaitingForWebhookButton.vue';
|
||||
import CanvasClearExecutionDataButton from '@/components/canvas/elements/buttons/CanvasClearExecutionDataButton.vue';
|
||||
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(
|
||||
async () => await import('@/components/Node/NodeCreation.vue'),
|
||||
|
@ -126,6 +130,9 @@ const usersStore = useUsersStore();
|
|||
const tagsStore = useTagsStore();
|
||||
const pushConnectionStore = usePushConnectionStore();
|
||||
const ndvStore = useNDVStore();
|
||||
const templatesStore = useTemplatesStore();
|
||||
|
||||
const canvasEventBus = createEventBus();
|
||||
|
||||
const lastClickPosition = ref<XYPosition>([450, 450]);
|
||||
|
||||
|
@ -267,9 +274,8 @@ async function initializeView() {
|
|||
if (isBlankRedirect.value) {
|
||||
isBlankRedirect.value = false;
|
||||
} else if (route.name === VIEWS.TEMPLATE_IMPORT) {
|
||||
// @TODO Implement template import
|
||||
// const templateId = route.params.id;
|
||||
// await openWorkflowTemplate(templateId.toString());
|
||||
const templateId = route.params.id;
|
||||
await openWorkflowTemplate(templateId.toString());
|
||||
} else {
|
||||
historyStore.reset();
|
||||
|
||||
|
@ -432,6 +438,75 @@ function makeNewWorkflowShareable() {
|
|||
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
|
||||
*/
|
||||
|
@ -1077,6 +1152,7 @@ onBeforeUnmount(() => {
|
|||
:workflow="editableWorkflow"
|
||||
:workflow-object="editableWorkflowObject"
|
||||
:fallback-nodes="fallbackNodes"
|
||||
:event-bus="canvasEventBus"
|
||||
@update:node:position="onUpdateNodePosition"
|
||||
@update:node:active="onSetNodeActive"
|
||||
@update:node:selected="onSetNodeSelected"
|
||||
|
|
Loading…
Reference in a new issue