refactor(editor): Upgrade to jsPlumb 5 (#4989)

* WIP: Nodeview

* Replace types

* Finish N8nPlus endpoint type

* Working on connector

* Apply prettier

* Fixed prettier issues

* Debugging rendering

* Fixed connectorrs position recalc

* Fix snapping and output labels, WIP dragging

* Fix N8nPlus endpoint rendering issues

* Cleanup

* Fix undo/redo and canvas add button position, cleanup

* Cleanup

* Revert accidental CLI changes

* Fix pnpm-lock

* Address bugs that came up during review

* Reset CLI package from master

* Various fixes

* Fix run items label toggling

* Linter fixes

* Fix stalk size for larger run items label

* Remove comment

* Correctly reset workspace after renaming the node

* Fix canvas e2e tests

* Fix undo/redo tests

* Fix stalk positioning and triggering of endpoint overlays

* Repaint connections on pin removal

* Limit repaintings

* Unbind jsPlumb events on deactivation

* Fix jsPlumb managment of Sticky and minor memort managment improvments

* Address rest of PR points

* Lint fix

* Copy patches folder to docker

* Fix e2e tests

* set allowNonAppliedPatches to allow build

* fix(editor): Handling router errors when navigation is canceled by user (#5271)

* 🔨 Handling router errors in main sidebar, removing unused code
* 🔨 Handling router errors in modals

* ci(core): Fix docker nightly/custom image build (no-changelog) (#5284)

* ci(core): Copy patches dir to Docker (no-changelog)

* Update patch

* Update package-lock

* reapply the patch

* skip patchedDependencies after the frontend is built

---------

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>

* Fix connector hover state on success

* Remove allowNonAppliedPatches from package.json

---------

Co-authored-by: Milorad FIlipović <milorad@n8n.io>
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
OlegIvaniv
2023-01-30 18:20:50 +01:00
committed by GitHub
parent 5cb7e5007d
commit 766501723b
30 changed files with 1756 additions and 2268 deletions

View File

@@ -1,29 +1,44 @@
import { computed, ref } from 'vue';
import { defineStore } from 'pinia';
import { jsPlumb } from 'jsplumb';
import { v4 as uuid } from 'uuid';
import normalizeWheel from 'normalize-wheel';
import { useWorkflowsStore } from '@/stores/workflows';
import { useNodeTypesStore } from '@/stores/nodeTypes';
import { useUIStore } from '@/stores/ui';
import { useHistoryStore } from '@/stores/history';
import { INodeUi, XYPosition } from '@/Interface';
import { scaleBigger, scaleReset, scaleSmaller } from '@/utils';
import { START_NODE_TYPE } from '@/constants';
import '@/plugins/N8nCustomConnectorType';
import '@/plugins/PlusEndpointType';
import { START_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants';
import type {
BeforeStartEventParams,
BrowserJsPlumbInstance,
DragStopEventParams,
} from '@jsplumb/browser-ui';
import { newInstance } from '@jsplumb/browser-ui';
import { N8nPlusEndpointHandler } from '@/plugins/endpoints/N8nPlusEndpointType';
import * as N8nPlusEndpointRenderer from '@/plugins/endpoints/N8nPlusEndpointRenderer';
import { N8nConnector } from '@/plugins/connectors/N8nCustomConnector';
import { EndpointFactory, Connectors } from '@jsplumb/core';
import { MoveNodeCommand } from '@/models/history';
import {
DEFAULT_PLACEHOLDER_TRIGGER_BUTTON,
getMidCanvasPosition,
getNewNodePosition,
getZoomToFit,
PLACEHOLDER_TRIGGER_NODE_SIZE,
CONNECTOR_FLOWCHART_TYPE,
GRID_SIZE,
} from '@/utils/nodeViewUtils';
import { PointXY } from '@jsplumb/util';
export const useCanvasStore = defineStore('canvas', () => {
const workflowStore = useWorkflowsStore();
const nodeTypesStore = useNodeTypesStore();
const uiStore = useUIStore();
const jsPlumbInstance = jsPlumb.getInstance();
const historyStore = useHistoryStore();
const jsPlumbInstance = ref<BrowserJsPlumbInstance>();
const isDragging = ref<boolean>(false);
const nodes = computed<INodeUi[]>(() => workflowStore.allNodes);
const triggerNodes = computed<INodeUi[]>(() =>
@@ -35,6 +50,10 @@ export const useCanvasStore = defineStore('canvas', () => {
const nodeViewScale = ref<number>(1);
const canvasAddButtonPosition = ref<XYPosition>([1, 1]);
Connectors.register(N8nConnector.type, N8nConnector);
N8nPlusEndpointRenderer.register();
EndpointFactory.registerHandler(N8nPlusEndpointHandler);
const setRecenteredCanvasAddButtonPosition = (offset?: XYPosition) => {
const position = getMidCanvasPosition(nodeViewScale.value, offset || [0, 0]);
@@ -59,7 +78,7 @@ export const useCanvasStore = defineStore('canvas', () => {
const setZoomLevel = (zoomLevel: number, offset: XYPosition) => {
nodeViewScale.value = zoomLevel;
jsPlumbInstance.setZoom(zoomLevel);
jsPlumbInstance.value?.setZoom(zoomLevel);
uiStore.nodeViewOffsetPosition = offset;
};
@@ -122,8 +141,106 @@ export const useCanvasStore = defineStore('canvas', () => {
wheelMoveWorkflow(e);
};
function initInstance(container: Element) {
// Make sure to clean-up previous instance if it exists
if (jsPlumbInstance.value) {
jsPlumbInstance.value.destroy();
jsPlumbInstance.value.reset();
jsPlumbInstance.value = undefined;
}
jsPlumbInstance.value = newInstance({
container,
connector: CONNECTOR_FLOWCHART_TYPE,
resizeObserver: false,
dragOptions: {
cursor: 'pointer',
grid: { w: GRID_SIZE, h: GRID_SIZE },
start: (params: BeforeStartEventParams) => {
const draggedNode = params.drag.getDragElement();
const nodeName = draggedNode.getAttribute('data-name');
if (!nodeName) return;
isDragging.value = true;
const isSelected = uiStore.isNodeSelected(nodeName);
if (params.e && !isSelected) {
// Only the node which gets dragged directly gets an event, for all others it is
// undefined. So check if the currently dragged node is selected and if not clear
// the drag-selection.
jsPlumbInstance.value?.clearDragSelection();
uiStore.resetSelectedNodes();
}
uiStore.addActiveAction('dragActive');
return true;
},
stop: (params: DragStopEventParams) => {
const draggedNode = params.drag.getDragElement();
const nodeName = draggedNode.getAttribute('data-name');
if (!nodeName) return;
const nodeData = workflowStore.getNodeByName(nodeName);
isDragging.value = false;
if (uiStore.isActionActive('dragActive') && nodeData) {
const moveNodes = uiStore.getSelectedNodes.slice();
const selectedNodeNames = moveNodes.map((node: INodeUi) => node.name);
if (!selectedNodeNames.includes(nodeData.name)) {
// If the current node is not in selected add it to the nodes which
// got moved manually
moveNodes.push(nodeData);
}
if (moveNodes.length > 1) {
historyStore.startRecordingUndo();
}
// This does for some reason just get called once for the node that got clicked
// even though "start" and "drag" gets called for all. So lets do for now
// some dirty DOM query to get the new positions till I have more time to
// create a proper solution
let newNodePosition: XYPosition;
moveNodes.forEach((node: INodeUi) => {
const element = document.getElementById(node.id);
if (element === null) {
return;
}
newNodePosition = [
parseInt(element.style.left!.slice(0, -2), 10),
parseInt(element.style.top!.slice(0, -2), 10),
];
const updateInformation = {
name: node.name,
properties: {
position: newNodePosition,
},
};
const oldPosition = node.position;
if (oldPosition[0] !== newNodePosition[0] || oldPosition[1] !== newNodePosition[1]) {
historyStore.pushCommandToUndo(
new MoveNodeCommand(node.name, oldPosition, newNodePosition),
);
workflowStore.updateNodeProperties(updateInformation);
}
});
if (moveNodes.length > 1) {
historyStore.stopRecordingUndo();
}
}
},
filter: '.node-description, .node-description .node-name, .node-description .node-subtitle',
},
});
jsPlumbInstance.value?.setDragConstrainFunction((pos: PointXY) => {
const isReadOnly = uiStore.isReadOnlyView;
if (isReadOnly) {
// Do not allow to move nodes in readOnly mode
return null;
}
return pos;
});
}
return {
jsPlumbInstance,
isDemo,
nodeViewScale,
canvasAddButtonPosition,
@@ -135,5 +252,7 @@ export const useCanvasStore = defineStore('canvas', () => {
zoomOut,
zoomToFit,
wheelScroll,
initInstance,
jsPlumbInstance,
};
});

View File

@@ -251,6 +251,9 @@ export const useUIStore = defineStore(STORES.UI, {
return (id: string) =>
this.fakeDoorFeatures.find((fakeDoor) => fakeDoor.id.toString() === id);
},
isReadOnlyView(): boolean {
return ![VIEWS.WORKFLOW, VIEWS.NEW_WORKFLOW].includes(this.currentView as VIEWS);
},
isNodeView(): boolean {
return [
VIEWS.NEW_WORKFLOW.toString(),

View File

@@ -195,7 +195,9 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
},
getNodeById() {
return (nodeId: string): INodeUi | undefined =>
this.workflow.nodes.find((node: INodeUi) => node.id === nodeId);
this.workflow.nodes.find((node: INodeUi) => {
return node.id === nodeId;
});
},
nodesIssuesExist(): boolean {
for (const node of this.workflow.nodes) {