feat(editor): Add capability to open workflow from template in new canvas (no-changelog) (#10011)

This commit is contained in:
Alex Grozav 2024-07-11 18:29:06 +03:00 committed by GitHub
parent 2d19aef540
commit 8171d75f5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 135 additions and 22 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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"