Files
Automata/packages/editor-ui/src/stores/canvas.store.ts
Michael Auerswald c3ba0123ad feat: Migrate integer primary keys to nanoids (#6345)
* first commit for postgres migration

* (not working)

* sqlite migration

* quicksave

* fix tests

* fix pg test

* fix postgres

* fix variables import

* fix execution saving

* add user settings fix

* change migration to single lines

* patch preferences endpoint

* cleanup

* improve variable import

* cleanup unusued code

* Update packages/cli/src/PublicApi/v1/handlers/workflows/workflows.handler.ts

Co-authored-by: Omar Ajoue <krynble@gmail.com>

* address review notes

* fix var update/import

* refactor: Separate execution data to its own table (#6323)

* wip: Temporary migration process

* refactor: Create boilerplate repository methods for executions

* fix: Lint issues

* refactor: Added search endpoint to repository

* refactor: Make the execution list work again

* wip: Updating how we create and update executions everywhere

* fix: Lint issues and remove most of the direct access to execution model

* refactor: Remove includeWorkflowData flag and fix more tests

* fix: Lint issues

* fix: Fixed ordering of executions for FE, removed transaction when saving execution and removed unnecessary update

* refactor: Add comment about missing feature

* refactor: Refactor counting executions

* refactor: Add migration for other dbms and fix issues found

* refactor: Fix lint issues

* refactor: Remove unnecessary comment and auto inject repo to internal hooks

* refactor: remove type assertion

* fix: Fix broken tests

* fix: Remove unnecessary import

* Remove unnecessary toString() call

Co-authored-by: Iván Ovejero <ivov.src@gmail.com>

* fix: Address comments after review

* refactor: Remove unused import

* fix: Lint issues

* fix: Add correct migration files

---------

Co-authored-by: Iván Ovejero <ivov.src@gmail.com>

* remove null values from credential export

* fix: Fix an issue with queue mode where all running execution would be returned

* fix: Update n8n node to allow for workflow ids with letters

* set upstream on set branch

* remove typo

* add nodeAccess to credentials

* fix unsaved run check for undefined id

* fix(core): Rename version control feature to source control (#6480)

* rename versionControl to sourceControl

* fix source control tooltip wording

---------

Co-authored-by: Romain Minaud <romain.minaud@gmail.com>

* fix(editor): Pay 548 hide the set up version control button (#6485)

* feat(DebugHelper Node): Fix and include in main app (#6406)

* improve node a bit

* fixing continueOnFail() ton contain error in json

* improve pairedItem

* fix random data returning object results

* fix nanoId length typo

* update pnpm-lock file

---------

Co-authored-by: Marcus <marcus@n8n.io>

* fix(editor): Remove setup source control CTA button

* fix(editor): Remove setup source control CTA button

---------

Co-authored-by: Michael Auerswald <michael.auerswald@gmail.com>
Co-authored-by: Marcus <marcus@n8n.io>

* fix(editor): Update source control docs links (#6488)

* feat(DebugHelper Node): Fix and include in main app (#6406)

* improve node a bit

* fixing continueOnFail() ton contain error in json

* improve pairedItem

* fix random data returning object results

* fix nanoId length typo

* update pnpm-lock file

---------

Co-authored-by: Marcus <marcus@n8n.io>

* feat(editor): Replace root events with event bus events (no-changelog) (#6454)

* feat: replace root events with event bus events

* fix: prevent cypress from replacing global with globalThis in import path

* feat: remove emitter mixin

* fix: replace component events with event bus

* fix: fix linting issue

* fix: fix breaking expression switch

* chore: prettify ndv e2e suite code

* fix(editor): Update source control docs links

---------

Co-authored-by: Michael Auerswald <michael.auerswald@gmail.com>
Co-authored-by: Marcus <marcus@n8n.io>
Co-authored-by: Alex Grozav <alex@grozav.com>

* fix tag endpoint regex

---------

Co-authored-by: Omar Ajoue <krynble@gmail.com>
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: Romain Minaud <romain.minaud@gmail.com>
Co-authored-by: Csaba Tuncsik <csaba@n8n.io>
Co-authored-by: Marcus <marcus@n8n.io>
Co-authored-by: Alex Grozav <alex@grozav.com>
2023-06-20 19:13:18 +02:00

288 lines
8.9 KiB
TypeScript

import { computed, ref, watch } from 'vue';
import { defineStore } from 'pinia';
import { v4 as uuid } from 'uuid';
import normalizeWheel from 'normalize-wheel';
import {
useWorkflowsStore,
useNodeTypesStore,
useUIStore,
useHistoryStore,
useSourceControlStore,
} from '@/stores';
import type { INodeUi, XYPosition } from '@/Interface';
import { scaleBigger, scaleReset, scaleSmaller } from '@/utils';
import { START_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 type { Connection } from '@jsplumb/core';
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,
CONNECTOR_PAINT_STYLE_DEFAULT,
CONNECTOR_PAINT_STYLE_PRIMARY,
CONNECTOR_ARROW_OVERLAYS,
} from '@/utils/nodeViewUtils';
import type { PointXY } from '@jsplumb/util';
export const useCanvasStore = defineStore('canvas', () => {
const workflowStore = useWorkflowsStore();
const nodeTypesStore = useNodeTypesStore();
const uiStore = useUIStore();
const historyStore = useHistoryStore();
const sourceControlStore = useSourceControlStore();
const jsPlumbInstanceRef = ref<BrowserJsPlumbInstance>();
const isDragging = ref<boolean>(false);
const lastSelectedConnection = ref<Connection | null>(null);
const newNodeInsertPosition = ref<XYPosition | null>(null);
const nodes = computed<INodeUi[]>(() => workflowStore.allNodes);
const triggerNodes = computed<INodeUi[]>(() =>
nodes.value.filter(
(node) => node.type === START_NODE_TYPE || nodeTypesStore.isTriggerNode(node.type),
),
);
const isDemo = ref<boolean>(false);
const nodeViewScale = ref<number>(1);
const canvasAddButtonPosition = ref<XYPosition>([1, 1]);
const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly);
watch(readOnlyEnv, (readOnly) => {
if (jsPlumbInstanceRef.value) {
jsPlumbInstanceRef.value.elementsDraggable = !readOnly;
}
});
Connectors.register(N8nConnector.type, N8nConnector);
N8nPlusEndpointRenderer.register();
EndpointFactory.registerHandler(N8nPlusEndpointHandler);
const setRecenteredCanvasAddButtonPosition = (offset?: XYPosition) => {
const position = getMidCanvasPosition(nodeViewScale.value, offset || [0, 0]);
position[0] -= PLACEHOLDER_TRIGGER_NODE_SIZE / 2;
position[1] -= PLACEHOLDER_TRIGGER_NODE_SIZE / 2;
canvasAddButtonPosition.value = getNewNodePosition(nodes.value, position);
};
const getPlaceholderTriggerNodeUI = (): INodeUi => {
setRecenteredCanvasAddButtonPosition();
return {
id: uuid(),
...DEFAULT_PLACEHOLDER_TRIGGER_BUTTON,
position: canvasAddButtonPosition.value,
};
};
const getNodesWithPlaceholderNode = (): INodeUi[] =>
triggerNodes.value.length > 0 ? nodes.value : [getPlaceholderTriggerNodeUI(), ...nodes.value];
const setZoomLevel = (zoomLevel: number, offset: XYPosition) => {
nodeViewScale.value = zoomLevel;
jsPlumbInstanceRef.value?.setZoom(zoomLevel);
uiStore.nodeViewOffsetPosition = offset;
};
const resetZoom = () => {
const { scale, offset } = scaleReset({
scale: nodeViewScale.value,
offset: uiStore.nodeViewOffsetPosition,
});
setZoomLevel(scale, offset);
};
const zoomIn = () => {
const { scale, offset } = scaleBigger({
scale: nodeViewScale.value,
offset: uiStore.nodeViewOffsetPosition,
});
setZoomLevel(scale, offset);
};
const zoomOut = () => {
const { scale, offset } = scaleSmaller({
scale: nodeViewScale.value,
offset: uiStore.nodeViewOffsetPosition,
});
setZoomLevel(scale, offset);
};
const zoomToFit = () => {
const nodes = getNodesWithPlaceholderNode();
if (!nodes.length) {
// some unknown workflow executions
return;
}
const { zoomLevel, offset } = getZoomToFit(nodes, !isDemo.value);
setZoomLevel(zoomLevel, offset);
};
const wheelMoveWorkflow = (e: WheelEvent) => {
const normalized = normalizeWheel(e);
const offsetPosition = uiStore.nodeViewOffsetPosition;
const nodeViewOffsetPositionX =
offsetPosition[0] - (e.shiftKey ? normalized.pixelY : normalized.pixelX);
const nodeViewOffsetPositionY =
offsetPosition[1] - (e.shiftKey ? normalized.pixelX : normalized.pixelY);
uiStore.nodeViewOffsetPosition = [nodeViewOffsetPositionX, nodeViewOffsetPositionY];
};
const wheelScroll = (e: WheelEvent) => {
//* Control + scroll zoom
if (e.ctrlKey) {
if (e.deltaY > 0) {
zoomOut();
} else {
zoomIn();
}
e.preventDefault();
return;
}
wheelMoveWorkflow(e);
};
function initInstance(container: Element) {
// Make sure to clean-up previous instance if it exists
if (jsPlumbInstanceRef.value) {
jsPlumbInstanceRef.value.destroy();
jsPlumbInstanceRef.value.reset();
jsPlumbInstanceRef.value = undefined;
}
jsPlumbInstanceRef.value = newInstance({
container,
connector: CONNECTOR_FLOWCHART_TYPE,
resizeObserver: false,
endpoint: {
type: 'Dot',
options: { radius: 5 },
},
paintStyle: CONNECTOR_PAINT_STYLE_DEFAULT,
hoverPaintStyle: CONNECTOR_PAINT_STYLE_PRIMARY,
connectionOverlays: CONNECTOR_ARROW_OVERLAYS,
elementsDraggable: !readOnlyEnv.value,
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.
jsPlumbInstanceRef.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',
},
});
jsPlumbInstanceRef.value?.setDragConstrainFunction((pos: PointXY) => {
const isReadOnly = uiStore.isReadOnlyView;
if (isReadOnly) {
// Do not allow to move nodes in readOnly mode
return null;
}
return pos;
});
}
const jsPlumbInstance = computed(() => jsPlumbInstanceRef.value as BrowserJsPlumbInstance);
return {
isDemo,
nodeViewScale,
canvasAddButtonPosition,
lastSelectedConnection,
newNodeInsertPosition,
jsPlumbInstance,
setRecenteredCanvasAddButtonPosition,
getNodesWithPlaceholderNode,
setZoomLevel,
resetZoom,
zoomIn,
zoomOut,
zoomToFit,
wheelScroll,
initInstance,
};
});