mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
✨ Allow to load workflow templates (#1887)
* implement import * set name, remove console log * add validation and such * remove monday.com package for testing * clean up code * await new name * refactor api requests * remove unnessary import * build * add zoom button * update positions on loading template * update error handling * build * update zoom to center * set state to dirty upon leaving * clean up pr * refactor func * refactor redir * fix lint issue * refactor func out * use new endpoint * revert error changes * revert error changes * update logic to find top left node * zoom to fit when opening workflow * revert testing change * update case * address comments * reset zoom when opening new workflow * update endpoint to plural form * update endpoint * ⚡ Minor improvements Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
@@ -39,6 +39,9 @@
|
||||
@closeNodeCreator="closeNodeCreator"
|
||||
></node-creator>
|
||||
<div :class="{ 'zoom-menu': true, expanded: !sidebarMenuCollapsed }">
|
||||
<button @click="zoomToFit" class="button-white" title="Zoom to Fit">
|
||||
<font-awesome-icon icon="expand"/>
|
||||
</button>
|
||||
<button @click="setZoom('in')" class="button-white" title="Zoom In">
|
||||
<font-awesome-icon icon="search-plus"/>
|
||||
</button>
|
||||
@@ -113,7 +116,7 @@ import {
|
||||
} from 'jsplumb';
|
||||
import { MessageBoxInputData } from 'element-ui/types/message-box';
|
||||
import { jsPlumb, Endpoint, OnConnectionBindInfo } from 'jsplumb';
|
||||
import { NODE_NAME_PREFIX, PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
|
||||
import { NODE_NAME_PREFIX, PLACEHOLDER_EMPTY_WORKFLOW_ID, START_NODE_TYPE } from '@/constants';
|
||||
import { copyPaste } from '@/components/mixins/copyPaste';
|
||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
@@ -133,9 +136,10 @@ import NodeCreator from '@/components/NodeCreator/NodeCreator.vue';
|
||||
import NodeSettings from '@/components/NodeSettings.vue';
|
||||
import RunData from '@/components/RunData.vue';
|
||||
|
||||
import { getLeftmostTopNode, getWorkflowCorners } from './helpers';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { v4 as uuidv4} from 'uuid';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
IConnection,
|
||||
IConnections,
|
||||
@@ -144,7 +148,6 @@ import {
|
||||
INodeConnections,
|
||||
INodeIssues,
|
||||
INodeTypeDescription,
|
||||
IRunData,
|
||||
NodeInputConnections,
|
||||
NodeHelpers,
|
||||
Workflow,
|
||||
@@ -155,19 +158,35 @@ import {
|
||||
IExecutionResponse,
|
||||
IExecutionsStopData,
|
||||
IN8nUISettings,
|
||||
IStartRunData,
|
||||
IWorkflowDb,
|
||||
IWorkflowData,
|
||||
INodeUi,
|
||||
IRunDataUi,
|
||||
IUpdateInformation,
|
||||
IWorkflowDataUpdate,
|
||||
XYPositon,
|
||||
IPushDataExecutionFinished,
|
||||
ITag,
|
||||
IWorkflowTemplate,
|
||||
} from '../Interface';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
const NODE_SIZE = 100;
|
||||
const DEFAULT_START_POSITION_X = 250;
|
||||
const DEFAULT_START_POSITION_Y = 300;
|
||||
const HEADER_HEIGHT = 65;
|
||||
const SIDEBAR_WIDTH = 65;
|
||||
|
||||
const DEFAULT_START_NODE = {
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.start',
|
||||
typeVersion: 1,
|
||||
position: [
|
||||
DEFAULT_START_POSITION_X,
|
||||
DEFAULT_START_POSITION_Y,
|
||||
] as XYPositon,
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
export default mixins(
|
||||
copyPaste,
|
||||
externalHooks,
|
||||
@@ -311,6 +330,7 @@ export default mixins(
|
||||
nodeViewScale: 1,
|
||||
ctrlKeyPressed: false,
|
||||
stopExecutionInProgress: false,
|
||||
blankRedirect: false,
|
||||
};
|
||||
},
|
||||
beforeDestroy () {
|
||||
@@ -329,7 +349,6 @@ export default mixins(
|
||||
this.$externalHooks().run('nodeView.createNodeActiveChanged', { source: 'add_node_button' });
|
||||
},
|
||||
async openExecution (executionId: string) {
|
||||
this.resetWorkspace();
|
||||
|
||||
let data: IExecutionResponse | undefined;
|
||||
try {
|
||||
@@ -352,6 +371,60 @@ export default mixins(
|
||||
|
||||
this.$externalHooks().run('execution.open', { workflowId: data.workflowData.id, workflowName: data.workflowData.name, executionId });
|
||||
},
|
||||
async openWorkflowTemplate (templateId: string) {
|
||||
this.setLoadingText('Loading template');
|
||||
this.resetWorkspace();
|
||||
|
||||
let data: IWorkflowTemplate | undefined;
|
||||
try {
|
||||
this.$externalHooks().run('template.requested', { templateId });
|
||||
data = await this.$store.dispatch('workflows/getWorkflowTemplate', templateId);
|
||||
|
||||
if (!data) {
|
||||
throw new Error(`Workflow template with id "${templateId}" could not be found!`);
|
||||
}
|
||||
|
||||
data.workflow.nodes.forEach((node) => {
|
||||
if (!this.$store.getters.nodeType(node.type)) {
|
||||
const name = node.type.replace('n8n-nodes-base.', '');
|
||||
throw new Error(`The ${name} node is not supported`);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
this.$showError(error, `Couldn't import workflow`);
|
||||
this.$router.push({ name: 'NodeViewNew' });
|
||||
return;
|
||||
}
|
||||
|
||||
const nodes = data.workflow.nodes;
|
||||
const hasStartNode = !!nodes.find(node => node.type === START_NODE_TYPE);
|
||||
|
||||
const leftmostTop = getLeftmostTopNode(nodes);
|
||||
|
||||
const diffX = DEFAULT_START_POSITION_X - leftmostTop.position[0];
|
||||
const diffY = DEFAULT_START_POSITION_Y - leftmostTop.position[1];
|
||||
|
||||
data.workflow.nodes.map((node) => {
|
||||
node.position[0] += diffX + (hasStartNode? 0 : NODE_SIZE * 2);
|
||||
node.position[1] += diffY;
|
||||
});
|
||||
|
||||
if (!hasStartNode) {
|
||||
data.workflow.nodes.push(DEFAULT_START_NODE);
|
||||
}
|
||||
|
||||
this.blankRedirect = true;
|
||||
this.$router.push({ name: 'NodeViewNew' });
|
||||
|
||||
await this.addNodes(data.workflow.nodes, data.workflow.connections);
|
||||
await this.$store.dispatch('workflows/setNewWorkflowName', data.name);
|
||||
this.$nextTick(() => {
|
||||
this.zoomToFit();
|
||||
this.$store.commit('setStateDirty', true);
|
||||
});
|
||||
|
||||
this.$externalHooks().run('template.open', { templateId, templateName: data.name, workflow: data.workflow });
|
||||
},
|
||||
async openWorkflow (workflowId: string) {
|
||||
this.resetWorkspace();
|
||||
|
||||
@@ -381,6 +454,7 @@ export default mixins(
|
||||
await this.addNodes(data.nodes, data.connections);
|
||||
|
||||
this.$store.commit('setStateDirty', false);
|
||||
this.zoomToFit();
|
||||
|
||||
this.$externalHooks().run('workflow.open', { workflowId, workflowName: data.name });
|
||||
|
||||
@@ -705,17 +779,22 @@ export default mixins(
|
||||
},
|
||||
|
||||
setZoom (zoom: string) {
|
||||
let scale = this.nodeViewScale;
|
||||
if (zoom === 'in') {
|
||||
this.nodeViewScale *= 1.25;
|
||||
scale *= 1.25;
|
||||
} else if (zoom === 'out') {
|
||||
this.nodeViewScale /= 1.25;
|
||||
scale /= 1.25;
|
||||
} else {
|
||||
this.nodeViewScale = 1;
|
||||
scale = 1;
|
||||
}
|
||||
this.setZoomLevel(scale);
|
||||
},
|
||||
|
||||
const zoomLevel = this.nodeViewScale;
|
||||
|
||||
setZoomLevel (zoomLevel: number) {
|
||||
this.nodeViewScale = zoomLevel; // important for background
|
||||
const element = this.instance.getContainer() as HTMLElement;
|
||||
|
||||
// https://docs.jsplumbtoolkit.com/community/current/articles/zooming.html
|
||||
const prependProperties = ['webkit', 'moz', 'ms', 'o'];
|
||||
const scaleString = 'scale(' + zoomLevel + ')';
|
||||
|
||||
@@ -729,6 +808,32 @@ export default mixins(
|
||||
this.instance.setZoom(zoomLevel);
|
||||
},
|
||||
|
||||
zoomToFit () {
|
||||
const nodes = this.$store.getters.allNodes as INodeUi[];
|
||||
|
||||
const {minX, minY, maxX, maxY} = getWorkflowCorners(nodes);
|
||||
|
||||
const PADDING = NODE_SIZE * 4;
|
||||
|
||||
const editorWidth = window.innerWidth;
|
||||
const diffX = maxX - minX + SIDEBAR_WIDTH + PADDING;
|
||||
const scaleX = editorWidth / diffX;
|
||||
|
||||
const editorHeight = window.innerHeight;
|
||||
const diffY = maxY - minY + HEADER_HEIGHT + PADDING;
|
||||
const scaleY = editorHeight / diffY;
|
||||
|
||||
const zoomLevel = Math.min(scaleX, scaleY, 1);
|
||||
let xOffset = (minX * -1) * zoomLevel + SIDEBAR_WIDTH; // find top right corner
|
||||
xOffset += (editorWidth - SIDEBAR_WIDTH - (maxX - minX + NODE_SIZE) * zoomLevel) / 2; // add padding to center workflow
|
||||
|
||||
let yOffset = (minY * -1) * zoomLevel + HEADER_HEIGHT; // find top right corner
|
||||
yOffset += (editorHeight - HEADER_HEIGHT - (maxY - minY + NODE_SIZE * 2) * zoomLevel) / 2; // add padding to center workflow
|
||||
|
||||
this.setZoomLevel(zoomLevel);
|
||||
this.$store.commit('setNodeViewOffsetPosition', {newOffset: [xOffset, yOffset]});
|
||||
},
|
||||
|
||||
async stopExecution () {
|
||||
const executionId = this.$store.getters.activeExecutionId;
|
||||
if (executionId === null) {
|
||||
@@ -1406,23 +1511,10 @@ export default mixins(
|
||||
await this.$store.dispatch('workflows/setNewWorkflowName');
|
||||
this.$store.commit('setStateDirty', false);
|
||||
|
||||
// Create start node
|
||||
const defaultNodes = [
|
||||
{
|
||||
name: 'Start',
|
||||
type: 'n8n-nodes-base.start',
|
||||
typeVersion: 1,
|
||||
position: [
|
||||
250,
|
||||
300,
|
||||
] as XYPositon,
|
||||
parameters: {},
|
||||
},
|
||||
];
|
||||
|
||||
await this.addNodes(defaultNodes);
|
||||
await this.addNodes([DEFAULT_START_NODE]);
|
||||
this.$store.commit('setStateDirty', false);
|
||||
|
||||
this.setZoomLevel(1);
|
||||
},
|
||||
async initView (): Promise<void> {
|
||||
if (this.$route.params.action === 'workflowSave') {
|
||||
@@ -1432,7 +1524,14 @@ export default mixins(
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (this.$route.name === 'ExecutionById') {
|
||||
if (this.blankRedirect) {
|
||||
this.blankRedirect = false;
|
||||
}
|
||||
else if (this.$route.name === 'WorkflowTemplate') {
|
||||
const templateId = this.$route.params.id;
|
||||
await this.openWorkflowTemplate(templateId);
|
||||
}
|
||||
else if (this.$route.name === 'ExecutionById') {
|
||||
// Load an execution
|
||||
const executionId = this.$route.params.id;
|
||||
await this.openExecution(executionId);
|
||||
|
||||
Reference in New Issue
Block a user