mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-20 11:22:15 +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:
@@ -221,6 +221,15 @@ export interface IWorkflowDataUpdate {
|
|||||||
tags?: ITag[] | string[]; // string[] when store or requested, ITag[] from API response
|
tags?: ITag[] | string[]; // string[] when store or requested, ITag[] from API response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IWorkflowTemplate {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
workflow: {
|
||||||
|
nodes: INodeUi[];
|
||||||
|
connections: IConnections;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Almost identical to cli.Interfaces.ts
|
// Almost identical to cli.Interfaces.ts
|
||||||
export interface IWorkflowDb {
|
export interface IWorkflowDb {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -42,15 +42,13 @@ class ResponseError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function makeRestApiRequest(context: IRestApiContext, method: Method, endpoint: string, data?: IDataObject) {
|
async function request(config: {method: Method, baseURL: string, endpoint: string, headers?: IDataObject, data?: IDataObject}) {
|
||||||
const { baseUrl, sessionId } = context;
|
const { method, baseURL, endpoint, headers, data } = config;
|
||||||
const options: AxiosRequestConfig = {
|
const options: AxiosRequestConfig = {
|
||||||
method,
|
method,
|
||||||
url: endpoint,
|
url: endpoint,
|
||||||
baseURL: baseUrl,
|
baseURL,
|
||||||
headers: {
|
headers,
|
||||||
sessionid: sessionId,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
if (['PATCH', 'POST', 'PUT'].includes(method)) {
|
if (['PATCH', 'POST', 'PUT'].includes(method)) {
|
||||||
options.data = data;
|
options.data = data;
|
||||||
@@ -60,7 +58,7 @@ export async function makeRestApiRequest(context: IRestApiContext, method: Metho
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.request(options);
|
const response = await axios.request(options);
|
||||||
return response.data.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message === 'Network Error') {
|
if (error.message === 'Network Error') {
|
||||||
throw new ResponseError('API-Server can not be reached. It is probably down.');
|
throw new ResponseError('API-Server can not be reached. It is probably down.');
|
||||||
@@ -79,3 +77,20 @@ export async function makeRestApiRequest(context: IRestApiContext, method: Metho
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function makeRestApiRequest(context: IRestApiContext, method: Method, endpoint: string, data?: IDataObject) {
|
||||||
|
const response = await request({
|
||||||
|
method,
|
||||||
|
baseURL: context.baseUrl,
|
||||||
|
endpoint,
|
||||||
|
headers: {sessionid: context.sessionId},
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-ignore all cli rest api endpoints return data wrapped in `data` key
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get(baseURL: string, endpoint: string, params?: IDataObject) {
|
||||||
|
return await request({method: 'GET', baseURL, endpoint, data: params});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { IRestApiContext } from '@/Interface';
|
import { IRestApiContext, IWorkflowTemplate } from '@/Interface';
|
||||||
import { makeRestApiRequest } from './helpers';
|
import { makeRestApiRequest, get } from './helpers';
|
||||||
|
import { TEMPLATES_BASE_URL } from '@/constants';
|
||||||
|
|
||||||
export async function getNewWorkflow(context: IRestApiContext, name?: string) {
|
export async function getNewWorkflow(context: IRestApiContext, name?: string) {
|
||||||
return await makeRestApiRequest(context, 'GET', `/workflows/new`, name ? { name } : {});
|
return await makeRestApiRequest(context, 'GET', `/workflows/new`, name ? { name } : {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getWorkflowTemplate(templateId: string): Promise<IWorkflowTemplate> {
|
||||||
|
return await get(TEMPLATES_BASE_URL, `/workflows/templates/${templateId}`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ export const genericHelpers = mixins(showMessage).extend({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
setLoadingText (text: string) {
|
||||||
|
this.loadingService.text = text;
|
||||||
|
},
|
||||||
stopLoading () {
|
stopLoading () {
|
||||||
if (this.loadingService !== null) {
|
if (this.loadingService !== null) {
|
||||||
this.loadingService.close();
|
this.loadingService.close();
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ export const showMessage = mixins(externalHooks).extend({
|
|||||||
return Notification(messageData);
|
return Notification(messageData);
|
||||||
},
|
},
|
||||||
|
|
||||||
$showError(error: Error, title: string, message: string) {
|
$showError(error: Error, title: string, message?: string) {
|
||||||
|
const messageLine = message ? `${message}<br/>` : '';
|
||||||
this.$showMessage({
|
this.$showMessage({
|
||||||
title,
|
title,
|
||||||
message: `
|
message: `
|
||||||
${message}
|
${messageLine}
|
||||||
<br>
|
|
||||||
<i>${error.message}</i>
|
<i>${error.message}</i>
|
||||||
${this.collapsableDetails(error)}`,
|
${this.collapsableDetails(error)}`,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ export const BREAKPOINT_MD = 992;
|
|||||||
export const BREAKPOINT_LG = 1200;
|
export const BREAKPOINT_LG = 1200;
|
||||||
export const BREAKPOINT_XL = 1920;
|
export const BREAKPOINT_XL = 1920;
|
||||||
|
|
||||||
|
|
||||||
|
// templates
|
||||||
|
export const TEMPLATES_BASE_URL = `https://api.n8n.io/`;
|
||||||
|
export const START_NODE_TYPE = 'n8n-nodes-base.start';
|
||||||
|
|
||||||
// Node creator
|
// Node creator
|
||||||
export const CORE_NODES_CATEGORY = 'Core Nodes';
|
export const CORE_NODES_CATEGORY = 'Core Nodes';
|
||||||
export const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
|
export const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import {
|
|||||||
faEnvelope,
|
faEnvelope,
|
||||||
faEye,
|
faEye,
|
||||||
faExclamationTriangle,
|
faExclamationTriangle,
|
||||||
|
faExpand,
|
||||||
faExternalLinkAlt,
|
faExternalLinkAlt,
|
||||||
faExchangeAlt,
|
faExchangeAlt,
|
||||||
faFile,
|
faFile,
|
||||||
@@ -139,6 +140,7 @@ library.add(faEdit);
|
|||||||
library.add(faEnvelope);
|
library.add(faEnvelope);
|
||||||
library.add(faEye);
|
library.add(faEye);
|
||||||
library.add(faExclamationTriangle);
|
library.add(faExclamationTriangle);
|
||||||
|
library.add(faExpand);
|
||||||
library.add(faExternalLinkAlt);
|
library.add(faExternalLinkAlt);
|
||||||
library.add(faExchangeAlt);
|
library.add(faExchangeAlt);
|
||||||
library.add(faFile);
|
library.add(faFile);
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
import { getNewWorkflow } from '@/api/workflows';
|
import { getNewWorkflow, getWorkflowTemplate } from '@/api/workflows';
|
||||||
import { DUPLICATE_POSTFFIX, MAX_WORKFLOW_NAME_LENGTH, DEFAULT_NEW_WORKFLOW_NAME } from '@/constants';
|
import { DUPLICATE_POSTFFIX, MAX_WORKFLOW_NAME_LENGTH, DEFAULT_NEW_WORKFLOW_NAME } from '@/constants';
|
||||||
import { ActionContext, Module } from 'vuex';
|
import { ActionContext, Module } from 'vuex';
|
||||||
import {
|
import {
|
||||||
IRootState,
|
IRootState,
|
||||||
IWorkflowsState,
|
IWorkflowsState,
|
||||||
|
IWorkflowTemplate,
|
||||||
} from '../Interface';
|
} from '../Interface';
|
||||||
|
|
||||||
const module: Module<IWorkflowsState, IRootState> = {
|
const module: Module<IWorkflowsState, IRootState> = {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {},
|
state: {},
|
||||||
actions: {
|
actions: {
|
||||||
setNewWorkflowName: async (context: ActionContext<IWorkflowsState, IRootState>): Promise<void> => {
|
setNewWorkflowName: async (context: ActionContext<IWorkflowsState, IRootState>, name?: string): Promise<void> => {
|
||||||
let newName = '';
|
let newName = '';
|
||||||
try {
|
try {
|
||||||
const newWorkflow = await getNewWorkflow(context.rootGetters.getRestApiContext);
|
const newWorkflow = await getNewWorkflow(context.rootGetters.getRestApiContext, name);
|
||||||
newName = newWorkflow.name;
|
newName = newWorkflow.name;
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
// in case of error, default to original name
|
// in case of error, default to original name
|
||||||
newName = DEFAULT_NEW_WORKFLOW_NAME;
|
newName = name || DEFAULT_NEW_WORKFLOW_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.commit('setWorkflowName', { newName }, { root: true });
|
context.commit('setWorkflowName', { newName }, { root: true });
|
||||||
@@ -42,6 +43,9 @@ const module: Module<IWorkflowsState, IRootState> = {
|
|||||||
|
|
||||||
return newName;
|
return newName;
|
||||||
},
|
},
|
||||||
|
getWorkflowTemplate: async (context: ActionContext<IWorkflowsState, IRootState>, templateId: string): Promise<IWorkflowTemplate> => {
|
||||||
|
return await getWorkflowTemplate(templateId);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -48,5 +48,14 @@ export default new Router({
|
|||||||
path: '/',
|
path: '/',
|
||||||
redirect: '/workflow',
|
redirect: '/workflow',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/workflows/templates/:id',
|
||||||
|
name: 'WorkflowTemplate',
|
||||||
|
components: {
|
||||||
|
default: NodeView,
|
||||||
|
header: MainHeader,
|
||||||
|
sidebar: MainSidebar,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,6 +39,9 @@
|
|||||||
@closeNodeCreator="closeNodeCreator"
|
@closeNodeCreator="closeNodeCreator"
|
||||||
></node-creator>
|
></node-creator>
|
||||||
<div :class="{ 'zoom-menu': true, expanded: !sidebarMenuCollapsed }">
|
<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">
|
<button @click="setZoom('in')" class="button-white" title="Zoom In">
|
||||||
<font-awesome-icon icon="search-plus"/>
|
<font-awesome-icon icon="search-plus"/>
|
||||||
</button>
|
</button>
|
||||||
@@ -113,7 +116,7 @@ import {
|
|||||||
} from 'jsplumb';
|
} from 'jsplumb';
|
||||||
import { MessageBoxInputData } from 'element-ui/types/message-box';
|
import { MessageBoxInputData } from 'element-ui/types/message-box';
|
||||||
import { jsPlumb, Endpoint, OnConnectionBindInfo } from 'jsplumb';
|
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 { copyPaste } from '@/components/mixins/copyPaste';
|
||||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||||
@@ -133,9 +136,10 @@ import NodeCreator from '@/components/NodeCreator/NodeCreator.vue';
|
|||||||
import NodeSettings from '@/components/NodeSettings.vue';
|
import NodeSettings from '@/components/NodeSettings.vue';
|
||||||
import RunData from '@/components/RunData.vue';
|
import RunData from '@/components/RunData.vue';
|
||||||
|
|
||||||
|
import { getLeftmostTopNode, getWorkflowCorners } from './helpers';
|
||||||
|
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
import { v4 as uuidv4} from 'uuid';
|
import { v4 as uuidv4} from 'uuid';
|
||||||
import axios from 'axios';
|
|
||||||
import {
|
import {
|
||||||
IConnection,
|
IConnection,
|
||||||
IConnections,
|
IConnections,
|
||||||
@@ -144,7 +148,6 @@ import {
|
|||||||
INodeConnections,
|
INodeConnections,
|
||||||
INodeIssues,
|
INodeIssues,
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
IRunData,
|
|
||||||
NodeInputConnections,
|
NodeInputConnections,
|
||||||
NodeHelpers,
|
NodeHelpers,
|
||||||
Workflow,
|
Workflow,
|
||||||
@@ -155,19 +158,35 @@ import {
|
|||||||
IExecutionResponse,
|
IExecutionResponse,
|
||||||
IExecutionsStopData,
|
IExecutionsStopData,
|
||||||
IN8nUISettings,
|
IN8nUISettings,
|
||||||
IStartRunData,
|
|
||||||
IWorkflowDb,
|
IWorkflowDb,
|
||||||
IWorkflowData,
|
IWorkflowData,
|
||||||
INodeUi,
|
INodeUi,
|
||||||
IRunDataUi,
|
|
||||||
IUpdateInformation,
|
IUpdateInformation,
|
||||||
IWorkflowDataUpdate,
|
IWorkflowDataUpdate,
|
||||||
XYPositon,
|
XYPositon,
|
||||||
IPushDataExecutionFinished,
|
IPushDataExecutionFinished,
|
||||||
ITag,
|
ITag,
|
||||||
|
IWorkflowTemplate,
|
||||||
} from '../Interface';
|
} from '../Interface';
|
||||||
import { mapGetters } from 'vuex';
|
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(
|
export default mixins(
|
||||||
copyPaste,
|
copyPaste,
|
||||||
externalHooks,
|
externalHooks,
|
||||||
@@ -311,6 +330,7 @@ export default mixins(
|
|||||||
nodeViewScale: 1,
|
nodeViewScale: 1,
|
||||||
ctrlKeyPressed: false,
|
ctrlKeyPressed: false,
|
||||||
stopExecutionInProgress: false,
|
stopExecutionInProgress: false,
|
||||||
|
blankRedirect: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
@@ -329,7 +349,6 @@ export default mixins(
|
|||||||
this.$externalHooks().run('nodeView.createNodeActiveChanged', { source: 'add_node_button' });
|
this.$externalHooks().run('nodeView.createNodeActiveChanged', { source: 'add_node_button' });
|
||||||
},
|
},
|
||||||
async openExecution (executionId: string) {
|
async openExecution (executionId: string) {
|
||||||
this.resetWorkspace();
|
|
||||||
|
|
||||||
let data: IExecutionResponse | undefined;
|
let data: IExecutionResponse | undefined;
|
||||||
try {
|
try {
|
||||||
@@ -352,6 +371,60 @@ export default mixins(
|
|||||||
|
|
||||||
this.$externalHooks().run('execution.open', { workflowId: data.workflowData.id, workflowName: data.workflowData.name, executionId });
|
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) {
|
async openWorkflow (workflowId: string) {
|
||||||
this.resetWorkspace();
|
this.resetWorkspace();
|
||||||
|
|
||||||
@@ -381,6 +454,7 @@ export default mixins(
|
|||||||
await this.addNodes(data.nodes, data.connections);
|
await this.addNodes(data.nodes, data.connections);
|
||||||
|
|
||||||
this.$store.commit('setStateDirty', false);
|
this.$store.commit('setStateDirty', false);
|
||||||
|
this.zoomToFit();
|
||||||
|
|
||||||
this.$externalHooks().run('workflow.open', { workflowId, workflowName: data.name });
|
this.$externalHooks().run('workflow.open', { workflowId, workflowName: data.name });
|
||||||
|
|
||||||
@@ -705,17 +779,22 @@ export default mixins(
|
|||||||
},
|
},
|
||||||
|
|
||||||
setZoom (zoom: string) {
|
setZoom (zoom: string) {
|
||||||
|
let scale = this.nodeViewScale;
|
||||||
if (zoom === 'in') {
|
if (zoom === 'in') {
|
||||||
this.nodeViewScale *= 1.25;
|
scale *= 1.25;
|
||||||
} else if (zoom === 'out') {
|
} else if (zoom === 'out') {
|
||||||
this.nodeViewScale /= 1.25;
|
scale /= 1.25;
|
||||||
} else {
|
} 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;
|
const element = this.instance.getContainer() as HTMLElement;
|
||||||
|
|
||||||
|
// https://docs.jsplumbtoolkit.com/community/current/articles/zooming.html
|
||||||
const prependProperties = ['webkit', 'moz', 'ms', 'o'];
|
const prependProperties = ['webkit', 'moz', 'ms', 'o'];
|
||||||
const scaleString = 'scale(' + zoomLevel + ')';
|
const scaleString = 'scale(' + zoomLevel + ')';
|
||||||
|
|
||||||
@@ -729,6 +808,32 @@ export default mixins(
|
|||||||
this.instance.setZoom(zoomLevel);
|
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 () {
|
async stopExecution () {
|
||||||
const executionId = this.$store.getters.activeExecutionId;
|
const executionId = this.$store.getters.activeExecutionId;
|
||||||
if (executionId === null) {
|
if (executionId === null) {
|
||||||
@@ -1406,23 +1511,10 @@ export default mixins(
|
|||||||
await this.$store.dispatch('workflows/setNewWorkflowName');
|
await this.$store.dispatch('workflows/setNewWorkflowName');
|
||||||
this.$store.commit('setStateDirty', false);
|
this.$store.commit('setStateDirty', false);
|
||||||
|
|
||||||
// Create start node
|
await this.addNodes([DEFAULT_START_NODE]);
|
||||||
const defaultNodes = [
|
|
||||||
{
|
|
||||||
name: 'Start',
|
|
||||||
type: 'n8n-nodes-base.start',
|
|
||||||
typeVersion: 1,
|
|
||||||
position: [
|
|
||||||
250,
|
|
||||||
300,
|
|
||||||
] as XYPositon,
|
|
||||||
parameters: {},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
await this.addNodes(defaultNodes);
|
|
||||||
this.$store.commit('setStateDirty', false);
|
this.$store.commit('setStateDirty', false);
|
||||||
|
|
||||||
|
this.setZoomLevel(1);
|
||||||
},
|
},
|
||||||
async initView (): Promise<void> {
|
async initView (): Promise<void> {
|
||||||
if (this.$route.params.action === 'workflowSave') {
|
if (this.$route.params.action === 'workflowSave') {
|
||||||
@@ -1432,7 +1524,14 @@ export default mixins(
|
|||||||
return Promise.resolve();
|
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
|
// Load an execution
|
||||||
const executionId = this.$route.params.id;
|
const executionId = this.$route.params.id;
|
||||||
await this.openExecution(executionId);
|
await this.openExecution(executionId);
|
||||||
|
|||||||
42
packages/editor-ui/src/views/helpers.ts
Normal file
42
packages/editor-ui/src/views/helpers.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { INodeUi } from "@/Interface";
|
||||||
|
|
||||||
|
interface ICorners {
|
||||||
|
minX: number;
|
||||||
|
minY: number;
|
||||||
|
maxX: number;
|
||||||
|
maxY: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getLeftmostTopNode = (nodes: INodeUi[]): INodeUi => {
|
||||||
|
return nodes.reduce((leftmostTop, node) => {
|
||||||
|
if (node.position[0] > leftmostTop.position[0] || node.position[1] > leftmostTop.position[1]) {
|
||||||
|
return leftmostTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getWorkflowCorners = (nodes: INodeUi[]): ICorners => {
|
||||||
|
return nodes.reduce((accu: ICorners, node: INodeUi) => {
|
||||||
|
if (node.position[0] < accu.minX) {
|
||||||
|
accu.minX = node.position[0];
|
||||||
|
}
|
||||||
|
if (node.position[1] < accu.minY) {
|
||||||
|
accu.minY = node.position[1];
|
||||||
|
}
|
||||||
|
if (node.position[0] > accu.maxX) {
|
||||||
|
accu.maxX = node.position[0];
|
||||||
|
}
|
||||||
|
if (node.position[1] > accu.maxY) {
|
||||||
|
accu.maxY = node.position[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return accu;
|
||||||
|
}, {
|
||||||
|
minX: nodes[0].position[0],
|
||||||
|
minY: nodes[0].position[1],
|
||||||
|
maxX: nodes[0].position[0],
|
||||||
|
maxY: nodes[0].position[1],
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user