mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
✨ Add /workflows/demo route (#2745)
* ⚡ added /workflows/demo route * ⚡ suggested improvements * ⚡ added n8n version in postmessage * ⚡ updated zoom menu styling * ⚡ updated component padding for zoomToFit * ⚡ suggested improvements * ⚡ moved getFixedNodeList to canvasHelpers * 📝 removed unused import and rebased from master
This commit is contained in:
parent
d265d8b74f
commit
1b69148d9a
|
@ -96,6 +96,9 @@ export default mixins(externalHooks, nodeHelpers, workflowHelpers).extend({
|
||||||
this.$externalHooks().run('dataDisplay.nodeTypeChanged', { nodeSubtitle: this.getNodeSubtitle(node, this.nodeType, this.getWorkflow()) });
|
this.$externalHooks().run('dataDisplay.nodeTypeChanged', { nodeSubtitle: this.getNodeSubtitle(node, this.nodeType, this.getWorkflow()) });
|
||||||
this.$telemetry.track('User opened node modal', { node_type: this.nodeType ? this.nodeType.name : '', workflow_id: this.$store.getters.workflowId });
|
this.$telemetry.track('User opened node modal', { node_type: this.nodeType ? this.nodeType.name : '', workflow_id: this.$store.getters.workflowId });
|
||||||
}
|
}
|
||||||
|
if (window.top) {
|
||||||
|
window.top.postMessage(JSON.stringify({command: (node? 'openNDV': 'closeNDV')}), '*');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -51,5 +51,12 @@ export default new Router({
|
||||||
sidebar: MainSidebar,
|
sidebar: MainSidebar,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/workflows/demo',
|
||||||
|
name: 'WorkflowDemo',
|
||||||
|
components: {
|
||||||
|
default: NodeView,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
@nodeTypeSelected="nodeTypeSelected"
|
@nodeTypeSelected="nodeTypeSelected"
|
||||||
@closeNodeCreator="closeNodeCreator"
|
@closeNodeCreator="closeNodeCreator"
|
||||||
></node-creator>
|
></node-creator>
|
||||||
<div :class="{ 'zoom-menu': true, expanded: !sidebarMenuCollapsed }">
|
<div :class="{ 'zoom-menu': true, 'regular-zoom-menu': !isDemo, 'demo-zoom-menu': isDemo, expanded: !sidebarMenuCollapsed }">
|
||||||
<button @click="zoomToFit" class="button-white" :title="$locale.baseText('nodeView.zoomToFit')">
|
<button @click="zoomToFit" class="button-white" :title="$locale.baseText('nodeView.zoomToFit')">
|
||||||
<font-awesome-icon icon="expand"/>
|
<font-awesome-icon icon="expand"/>
|
||||||
</button>
|
</button>
|
||||||
|
@ -53,7 +53,7 @@
|
||||||
<font-awesome-icon icon="search-minus"/>
|
<font-awesome-icon icon="search-minus"/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="nodeViewScale !== 1"
|
v-if="nodeViewScale !== 1 && !isDemo"
|
||||||
@click="resetZoom()"
|
@click="resetZoom()"
|
||||||
class="button-white"
|
class="button-white"
|
||||||
:title="$locale.baseText('nodeView.resetZoom')"
|
:title="$locale.baseText('nodeView.resetZoom')"
|
||||||
|
@ -277,6 +277,9 @@ export default mixins(
|
||||||
executionWaitingForWebhook (): boolean {
|
executionWaitingForWebhook (): boolean {
|
||||||
return this.$store.getters.executionWaitingForWebhook;
|
return this.$store.getters.executionWaitingForWebhook;
|
||||||
},
|
},
|
||||||
|
isDemo (): boolean {
|
||||||
|
return this.$route.name === 'WorkflowDemo';
|
||||||
|
},
|
||||||
lastSelectedNode (): INodeUi | null {
|
lastSelectedNode (): INodeUi | null {
|
||||||
return this.$store.getters.lastSelectedNode;
|
return this.$store.getters.lastSelectedNode;
|
||||||
},
|
},
|
||||||
|
@ -510,6 +513,17 @@ export default mixins(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async importWorkflowExact(data: {workflow: IWorkflowDataUpdate}) {
|
||||||
|
if (!data.workflow.nodes || !data.workflow.connections) {
|
||||||
|
throw new Error('Invalid workflow object');
|
||||||
|
}
|
||||||
|
this.resetWorkspace();
|
||||||
|
data.workflow.nodes = CanvasHelpers.getFixedNodesList(data.workflow.nodes);
|
||||||
|
await this.addNodes(data.workflow.nodes, data.workflow.connections);
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.zoomToFit();
|
||||||
|
});
|
||||||
|
},
|
||||||
async openWorkflowTemplate (templateId: string) {
|
async openWorkflowTemplate (templateId: string) {
|
||||||
this.setLoadingText(this.$locale.baseText('nodeView.loadingTemplate'));
|
this.setLoadingText(this.$locale.baseText('nodeView.loadingTemplate'));
|
||||||
this.resetWorkspace();
|
this.resetWorkspace();
|
||||||
|
@ -539,22 +553,7 @@ export default mixins(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodes = data.workflow.nodes;
|
data.workflow.nodes = CanvasHelpers.getFixedNodesList(data.workflow.nodes);
|
||||||
const hasStartNode = !!nodes.find(node => node.type === START_NODE_TYPE);
|
|
||||||
|
|
||||||
const leftmostTop = CanvasHelpers.getLeftmostTopNode(nodes);
|
|
||||||
|
|
||||||
const diffX = CanvasHelpers.DEFAULT_START_POSITION_X - leftmostTop.position[0];
|
|
||||||
const diffY = CanvasHelpers.DEFAULT_START_POSITION_Y - leftmostTop.position[1];
|
|
||||||
|
|
||||||
data.workflow.nodes.map((node) => {
|
|
||||||
node.position[0] += diffX + (hasStartNode? 0 : CanvasHelpers.NODE_SIZE * 2);
|
|
||||||
node.position[1] += diffY;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!hasStartNode) {
|
|
||||||
data.workflow.nodes.push({...CanvasHelpers.DEFAULT_START_NODE});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.blankRedirect = true;
|
this.blankRedirect = true;
|
||||||
this.$router.push({ name: 'NodeViewNew', query: { templateId } });
|
this.$router.push({ name: 'NodeViewNew', query: { templateId } });
|
||||||
|
@ -1029,7 +1028,7 @@ export default mixins(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {zoomLevel, offset} = CanvasHelpers.getZoomToFit(nodes);
|
const {zoomLevel, offset} = CanvasHelpers.getZoomToFit(nodes, !this.isDemo);
|
||||||
|
|
||||||
this.setZoomLevel(zoomLevel);
|
this.setZoomLevel(zoomLevel);
|
||||||
this.$store.commit('setNodeViewOffsetPosition', {newOffset: offset});
|
this.$store.commit('setNodeViewOffsetPosition', {newOffset: offset});
|
||||||
|
@ -1845,7 +1844,10 @@ export default mixins(
|
||||||
document.addEventListener('keyup', this.keyUp);
|
document.addEventListener('keyup', this.keyUp);
|
||||||
|
|
||||||
window.addEventListener("beforeunload", (e) => {
|
window.addEventListener("beforeunload", (e) => {
|
||||||
if(this.$store.getters.getStateIsDirty === true) {
|
if(this.isDemo){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if(this.$store.getters.getStateIsDirty === true) {
|
||||||
const confirmationMessage = this.$locale.baseText('nodeView.itLooksLikeYouHaveBeenEditingSomething');
|
const confirmationMessage = this.$locale.baseText('nodeView.itLooksLikeYouHaveBeenEditingSomething');
|
||||||
(e || window.event).returnValue = confirmationMessage; //Gecko + IE
|
(e || window.event).returnValue = confirmationMessage; //Gecko + IE
|
||||||
return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
|
return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
|
||||||
|
@ -2690,6 +2692,27 @@ export default mixins(
|
||||||
|
|
||||||
|
|
||||||
async mounted () {
|
async mounted () {
|
||||||
|
window.addEventListener('message', async (message) => {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(message.data);
|
||||||
|
if (json && json.command === 'openWorkflow') {
|
||||||
|
try {
|
||||||
|
await this.importWorkflowExact(json);
|
||||||
|
} catch (e) {
|
||||||
|
if (window.top) {
|
||||||
|
window.top.postMessage(JSON.stringify({command: 'error', message: 'Could not import workflow'}), '*');
|
||||||
|
}
|
||||||
|
this.$showMessage({
|
||||||
|
title: 'Could not import workflow',
|
||||||
|
message: (e as Error).message,
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.$root.$on('importWorkflowData', async (data: IDataObject) => {
|
this.$root.$on('importWorkflowData', async (data: IDataObject) => {
|
||||||
await this.importWorkflowData(data.data as IWorkflowDataUpdate);
|
await this.importWorkflowData(data.data as IWorkflowDataUpdate);
|
||||||
});
|
});
|
||||||
|
@ -2737,6 +2760,9 @@ export default mixins(
|
||||||
try {
|
try {
|
||||||
this.initNodeView();
|
this.initNodeView();
|
||||||
await this.initView();
|
await this.initView();
|
||||||
|
if (window.top) {
|
||||||
|
window.top.postMessage(JSON.stringify({command: 'n8nReady',version:this.$store.getters.versionCli}), '*');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.$showError(
|
this.$showError(
|
||||||
error,
|
error,
|
||||||
|
@ -2773,10 +2799,6 @@ export default mixins(
|
||||||
color: #444;
|
color: #444;
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
|
|
||||||
@media (max-width: $--breakpoint-2xs) {
|
|
||||||
bottom: 90px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.expanded {
|
&.expanded {
|
||||||
left: $--sidebar-expanded-width + $--zoom-menu-margin;
|
left: $--sidebar-expanded-width + $--zoom-menu-margin;
|
||||||
}
|
}
|
||||||
|
@ -2786,6 +2808,17 @@ export default mixins(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.regular-zoom-menu {
|
||||||
|
@media (max-width: $--breakpoint-2xs) {
|
||||||
|
bottom: 90px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-zoom-menu {
|
||||||
|
left: 10px;
|
||||||
|
bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.node-creator-button {
|
.node-creator-button {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { IBounds, INodeUi, IZoomConfig, XYPosition } from "@/Interface";
|
||||||
import { Connection, Endpoint, Overlay, OverlaySpec, PaintStyle } from "jsplumb";
|
import { Connection, Endpoint, Overlay, OverlaySpec, PaintStyle } from "jsplumb";
|
||||||
import {
|
import {
|
||||||
IConnection,
|
IConnection,
|
||||||
|
INode,
|
||||||
ITaskData,
|
ITaskData,
|
||||||
INodeExecutionData,
|
INodeExecutionData,
|
||||||
NodeInputConnections,
|
NodeInputConnections,
|
||||||
|
@ -587,25 +588,27 @@ export const addConnectionOutputSuccess = (connection: Connection, output: {tota
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const getZoomToFit = (nodes: INodeUi[]): {offset: XYPosition, zoomLevel: number} => {
|
export const getZoomToFit = (nodes: INodeUi[], addComponentPadding = true): {offset: XYPosition, zoomLevel: number} => {
|
||||||
const {minX, minY, maxX, maxY} = getWorkflowCorners(nodes);
|
const {minX, minY, maxX, maxY} = getWorkflowCorners(nodes);
|
||||||
|
const sidebarWidth = addComponentPadding? SIDEBAR_WIDTH: 0;
|
||||||
|
const headerHeight = addComponentPadding? HEADER_HEIGHT: 0;
|
||||||
|
|
||||||
const PADDING = NODE_SIZE * 4;
|
const PADDING = NODE_SIZE * 4;
|
||||||
|
|
||||||
const editorWidth = window.innerWidth;
|
const editorWidth = window.innerWidth;
|
||||||
const diffX = maxX - minX + SIDEBAR_WIDTH + PADDING;
|
const diffX = maxX - minX + sidebarWidth + PADDING;
|
||||||
const scaleX = editorWidth / diffX;
|
const scaleX = editorWidth / diffX;
|
||||||
|
|
||||||
const editorHeight = window.innerHeight;
|
const editorHeight = window.innerHeight;
|
||||||
const diffY = maxY - minY + HEADER_HEIGHT + PADDING;
|
const diffY = maxY - minY + headerHeight + PADDING;
|
||||||
const scaleY = editorHeight / diffY;
|
const scaleY = editorHeight / diffY;
|
||||||
|
|
||||||
const zoomLevel = Math.min(scaleX, scaleY, 1);
|
const zoomLevel = Math.min(scaleX, scaleY, 1);
|
||||||
let xOffset = (minX * -1) * zoomLevel + SIDEBAR_WIDTH; // find top right corner
|
let xOffset = (minX * -1) * zoomLevel + sidebarWidth; // find top right corner
|
||||||
xOffset += (editorWidth - SIDEBAR_WIDTH - (maxX - minX + NODE_SIZE) * zoomLevel) / 2; // add padding to center workflow
|
xOffset += (editorWidth - sidebarWidth - (maxX - minX + NODE_SIZE) * zoomLevel) / 2; // add padding to center workflow
|
||||||
|
|
||||||
let yOffset = (minY * -1) * zoomLevel + HEADER_HEIGHT; // find top right corner
|
let yOffset = (minY * -1) * zoomLevel + headerHeight; // find top right corner
|
||||||
yOffset += (editorHeight - HEADER_HEIGHT - (maxY - minY + NODE_SIZE * 2) * zoomLevel) / 2; // add padding to center workflow
|
yOffset += (editorHeight - headerHeight - (maxY - minY + NODE_SIZE * 2) * zoomLevel) / 2; // add padding to center workflow
|
||||||
|
|
||||||
return {
|
return {
|
||||||
zoomLevel,
|
zoomLevel,
|
||||||
|
@ -685,3 +688,23 @@ export const getOutputEndpointUUID = (nodeIndex: string, outputIndex: number) =>
|
||||||
export const getInputEndpointUUID = (nodeIndex: string, inputIndex: number) => {
|
export const getInputEndpointUUID = (nodeIndex: string, inputIndex: number) => {
|
||||||
return `${nodeIndex}${INPUT_UUID_KEY}${inputIndex}`;
|
return `${nodeIndex}${INPUT_UUID_KEY}${inputIndex}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getFixedNodesList = (workflowNodes: INode[]) => {
|
||||||
|
const nodes = [...workflowNodes];
|
||||||
|
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];
|
||||||
|
|
||||||
|
nodes.map((node) => {
|
||||||
|
node.position[0] += diffX + (hasStartNode? 0 : NODE_SIZE * 2);
|
||||||
|
node.position[1] += diffY;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasStartNode) {
|
||||||
|
nodes.push({...DEFAULT_START_NODE});
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in a new issue