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:
Mutasem Aldmour
2021-06-22 20:33:07 +03:00
committed by GitHub
parent 3016978b68
commit 1d5ba3d437
11 changed files with 237 additions and 44 deletions

View File

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