refactor(editor): Encapsulate canvas actions (#4416)

* feat(editor): encapsulating canvas actions

* fiz(editor): zoomToFit

* fiz(editor): zoomToFit

* fiz(editor): fix imoprts in canvas controls

* fiz(editor): fix imports in node view

* fiz(editor): remove unused props from canvas controls

* fiz(editor): fix zoomToFit functionality

* fiz(editor): move more functions from NodeView to canvas store

* chore(editor): code formatting fixes

* fix(editor): adding back some lost refactoring after merge

* fix(editor): remove console.log

* fix(editor): add missing canvasAddButtonPosition

* fix(editor): modify root store env query

* fix(editor): modify canvas control position styling

* fix(editor): modify canvas control position styling

* fix(editor): roll back process.env

* fix(editor): fix canvas controls positioning

* fix(editor): fix canvas controls positioning

* fix(editor): adopting new styles after merge

* fix(editor): not storing html element in the store

* fix(editor): remove unused variables

* fix(editor): update canvas controls after conflict resolution

* fix(editor): revert main.ts to reduce change noise

* fix(editor): remove old store commit

* fix(editor): simplify canvas store

* fix(editor): reposition execute workflow button in mobile view

* fix(editor): fox mouse scroll zoom in canvas

* fix(editor): move canvas scroll handling into canvas controls
This commit is contained in:
Csaba Tuncsik
2022-11-07 09:53:27 +01:00
committed by GitHub
parent 4517c4a90a
commit 350b356271
5 changed files with 266 additions and 191 deletions

View File

@@ -15,7 +15,7 @@
@mousedown="mouseDown"
v-touch:tap="touchTap"
@mouseup="mouseUp"
@wheel="wheelScroll"
@wheel="canvasStore.wheelScroll"
>
<div id="node-view-background" class="node-view-background" :style="backgroundStyle" />
<div
@@ -29,8 +29,8 @@
@click="showTriggerCreator('trigger_placeholder_button')"
v-show="showCanvasAddButton"
:showTooltip="!containsTrigger && showTriggerMissingTooltip"
:position="canvasAddButtonPosition"
@hook:mounted="setRecenteredCanvasAddButtonPosition"
:position="canvasStore.canvasAddButtonPosition"
@hook:mounted="canvasStore.setRecenteredCanvasAddButtonPosition"
/>
<div v-for="nodeData in nodes" :key="nodeData.id">
<node
@@ -86,17 +86,7 @@
@toggleNodeCreator="onToggleNodeCreator"
@addNode="onAddNode"
/>
<div
:class="{ 'zoom-menu': true, 'regular-zoom-menu': !isDemo, 'demo-zoom-menu': isDemo, expanded: !uiStore.sidebarMenuCollapsed }">
<n8n-icon-button @click="zoomToFit" type="tertiary" size="large" :title="$locale.baseText('nodeView.zoomToFit')"
icon="expand" />
<n8n-icon-button @click="zoomIn" type="tertiary" size="large" :title="$locale.baseText('nodeView.zoomIn')"
icon="search-plus" />
<n8n-icon-button @click="zoomOut" type="tertiary" size="large" :title="$locale.baseText('nodeView.zoomOut')"
icon="search-minus" />
<n8n-icon-button v-if="nodeViewScale !== 1 && !isDemo" @click="resetZoom" type="tertiary" size="large"
:title="$locale.baseText('nodeView.resetZoom')" icon="undo" />
</div>
<canvas-controls />
<div
class="workflow-execute-wrapper" v-if="!isReadOnly"
>
@@ -137,11 +127,11 @@
<script lang="ts">
import Vue from 'vue';
import { mapStores } from 'pinia';
import {
Connection, Endpoint, N8nPlusEndpoint,
OnConnectionBindInfo, Connection, Endpoint, N8nPlusEndpoint, jsPlumbInstance,
} from 'jsplumb';
import type { MessageBoxInputData } from 'element-ui/types/message-box';
import { jsPlumb, OnConnectionBindInfo } from 'jsplumb';
import once from 'lodash/once';
import {
@@ -223,14 +213,9 @@ import {
IUser,
INodeUpdatePropertiesInformation,
} from '@/Interface';
import { mapGetters } from 'vuex';
import '../plugins/N8nCustomConnectorType';
import '../plugins/PlusEndpointType';
import { getAccountAge } from '@/modules/userHelpers';
import { dataPinningEventBus } from "@/event-bus/data-pinning-event-bus";
import { debounceHelper } from '@/components/mixins/debounce';
import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui';
import { useSettingsStore } from '@/stores/settings';
import { useUsersStore } from '@/stores/users';
@@ -241,12 +226,16 @@ import { useRootStore } from '@/stores/n8nRootStore';
import { useNDVStore } from '@/stores/ndv';
import { useTemplatesStore } from '@/stores/templates';
import { useNodeTypesStore } from '@/stores/nodeTypes';
import { useCanvasStore } from '@/stores/canvas';
interface AddNodeOptions {
position?: XYPosition;
dragAndDrop?: boolean;
}
const NodeCreator = () => import('@/components/Node/NodeCreator/NodeCreator.vue');
const NodeCreation = () => import('@/components/Node/NodeCreation.vue');
const CanvasControls = () => import('@/components/CanvasControls.vue');
export default mixins(
copyPaste,
@@ -268,11 +257,12 @@ export default mixins(
components: {
NodeDetailsView,
Node,
NodeCreator: () => import('@/components/Node/NodeCreator/NodeCreator.vue'),
NodeCreator,
NodeSettings,
Sticky,
CanvasAddButton,
NodeCreation: () => import('@/components/Node/NodeCreation.vue'),
NodeCreation,
CanvasControls,
},
errorCaptured: (err, vm, info) => {
console.error('errorCaptured'); // eslint-disable-line no-console
@@ -323,9 +313,16 @@ export default mixins(
},
containsTrigger(containsTrigger) {
// Re-center CanvasAddButton if there's no triggers
if (containsTrigger === false) this.setRecenteredCanvasAddButtonPosition(this.getNodeViewOffsetPosition);
if (containsTrigger === false) this.canvasStore.setRecenteredCanvasAddButtonPosition(this.getNodeViewOffsetPosition);
else this.tryToAddWelcomeSticky();
},
nodeViewScale(newScale) {
const element = this.$refs.nodeView as HTMLDivElement;
if(element) {
element.style.transform = `scale(${newScale})`;
}
},
},
async beforeRouteLeave(to, from, next) {
const nextTab = getNodeViewTab(to);
@@ -486,16 +483,21 @@ export default mixins(
getNodeViewOffsetPosition(): XYPosition {
return this.uiStore.nodeViewOffsetPosition;
},
...mapStores(useCanvasStore),
nodeViewScale(): number {
return this.canvasStore.nodeViewScale;
},
instance(): jsPlumbInstance {
return this.canvasStore.jsPlumbInstance;
},
},
data() {
return {
GRID_SIZE: CanvasHelpers.GRID_SIZE,
STICKY_NODE_TYPE,
createNodeActive: false,
instance: jsPlumb.getInstance(),
lastSelectedConnection: null as null | Connection,
lastClickPosition: [450, 450] as XYPosition,
nodeViewScale: 1,
ctrlKeyPressed: false,
stopExecutionInProgress: false,
blankRedirect: false,
@@ -508,7 +510,6 @@ export default mixins(
showStickyButton: false,
isExecutionPreview: false,
showTriggerMissingTooltip: false,
canvasAddButtonPosition: [1, 1] as XYPosition,
workflowData: null as INewWorkflowData | null,
};
},
@@ -677,7 +678,7 @@ export default mixins(
await this.addNodes(deepCopy(data.workflowData.nodes), deepCopy(data.workflowData.connections));
this.$nextTick(() => {
this.zoomToFit();
this.canvasStore.zoomToFit();
this.uiStore.stateIsDirty = false;
});
this.$externalHooks().run('execution.open', { workflowId: data.workflowData.id, workflowName: data.workflowData.name, executionId });
@@ -740,7 +741,7 @@ export default mixins(
}
this.$nextTick(() => {
this.zoomToFit();
this.canvasStore.zoomToFit();
});
},
async openWorkflowTemplate(templateId: string) {
@@ -778,7 +779,7 @@ export default mixins(
await this.addNodes(data.workflow.nodes, data.workflow.connections);
this.workflowData = await this.workflowsStore.getNewWorkflowData(data.name) || {};
this.$nextTick(() => {
this.zoomToFit();
this.canvasStore.zoomToFit();
this.uiStore.stateIsDirty = true;
});
@@ -824,7 +825,7 @@ export default mixins(
if (!this.credentialsUpdated) {
this.uiStore.stateIsDirty = false;
}
this.zoomToFit();
this.canvasStore.zoomToFit();
this.$externalHooks().run('workflow.open', { workflowId, workflowName: data.name });
this.workflowsStore.activeWorkflowExecution = null;
this.stopLoading();
@@ -849,20 +850,6 @@ export default mixins(
this.mouseUpMouseSelect(e);
this.mouseUpMoveWorkflow(e);
},
wheelScroll(e: WheelEvent) {
//* Control + scroll zoom
if (e.ctrlKey) {
if (e.deltaY > 0) {
this.zoomOut();
} else {
this.zoomIn();
}
e.preventDefault();
return;
}
this.wheelMoveWorkflow(e);
},
keyUp(e: KeyboardEvent) {
if (e.key === this.controlKeyCode) {
this.ctrlKeyPressed = false;
@@ -920,15 +907,7 @@ export default mixins(
if (lastSelectedNode !== null && lastSelectedNode.type !== STICKY_NODE_TYPE) {
this.callDebounced('renameNodePrompt', { debounceTime: 1500 }, lastSelectedNode.name);
}
} else if ((e.key === '=' || e.key === '+') && !this.isCtrlKeyPressed(e)) {
this.zoomIn();
} else if ((e.key === '_' || e.key === '-') && !this.isCtrlKeyPressed(e)) {
this.zoomOut();
} else if ((e.key === '0') && !this.isCtrlKeyPressed(e)) {
this.resetZoom();
} else if ((e.key === '1') && !this.isCtrlKeyPressed(e)) {
this.zoomToFit();
} else if ((e.key === 'a') && this.isCtrlKeyPressed(e)) {
} else if ((e.key === 'a') && (this.isCtrlKeyPressed(e) === true)) {
// Select all nodes
e.stopPropagation();
e.preventDefault();
@@ -1206,90 +1185,6 @@ export default mixins(
}
});
},
resetZoom() {
const { scale, offset } = CanvasHelpers.scaleReset({ scale: this.nodeViewScale, offset: this.getNodeViewOffsetPosition });
this.setZoomLevel(scale);
this.uiStore.nodeViewOffsetPosition = offset;
},
zoomIn() {
const { scale, offset: [xOffset, yOffset] } = CanvasHelpers.scaleBigger({ scale: this.nodeViewScale, offset: this.getNodeViewOffsetPosition });
this.setZoomLevel(scale);
this.uiStore.nodeViewOffsetPosition = [xOffset, yOffset];
},
zoomOut() {
const { scale, offset: [xOffset, yOffset] } = CanvasHelpers.scaleSmaller({ scale: this.nodeViewScale, offset: this.getNodeViewOffsetPosition });
this.setZoomLevel(scale);
this.uiStore.nodeViewOffsetPosition = [xOffset, yOffset];
},
setZoomLevel(zoomLevel: number) {
this.nodeViewScale = zoomLevel; // important for background
const element = this.$refs.nodeView as HTMLElement;
if (!element) {
return;
}
// https://docs.jsplumbtoolkit.com/community/current/articles/zooming.html
const scaleString = 'scale(' + zoomLevel + ')';
['webkit', 'moz', 'ms', 'o'].forEach((prefix) => {
// @ts-ignore
element.style[prefix + 'Transform'] = scaleString;
});
element.style['transform'] = scaleString;
// @ts-ignore
this.instance.setZoom(zoomLevel);
},
setRecenteredCanvasAddButtonPosition (offset?: XYPosition) {
const position = CanvasHelpers.getMidCanvasPosition(this.nodeViewScale, offset || [0, 0]);
position[0] -= CanvasHelpers.PLACEHOLDER_TRIGGER_NODE_SIZE / 2;
position[1] -= CanvasHelpers.PLACEHOLDER_TRIGGER_NODE_SIZE / 2;
this.canvasAddButtonPosition = CanvasHelpers.getNewNodePosition(this.nodes, position);
},
getPlaceholderTriggerNodeUI (): INodeUi {
this.setRecenteredCanvasAddButtonPosition();
return {
id: uuid(),
...CanvasHelpers.DEFAULT_PLACEHOLDER_TRIGGER_BUTTON,
position: this.canvasAddButtonPosition,
};
},
// Extend nodes with placeholder trigger button as NodeUI object
// with the centered position if canvas doesn't contains trigger node
getNodesWithPlaceholderNode(): INodeUi[] {
const nodes = this.workflowsStore.allNodes;
const extendedNodes = this.containsTrigger
? nodes
: [this.getPlaceholderTriggerNodeUI(), ...nodes];
return extendedNodes;
},
zoomToFit() {
const nodes = this.getNodesWithPlaceholderNode() as INodeUi[];
if (nodes.length === 0) { // some unknown workflow executions
return;
}
const {zoomLevel, offset} = CanvasHelpers.getZoomToFit(nodes, !this.isDemo);
this.setZoomLevel(zoomLevel);
this.uiStore.nodeViewOffsetPosition = offset;
},
async stopExecution() {
const executionId = this.workflowsStore.activeExecutionId;
if (executionId === null) {
@@ -1699,7 +1594,7 @@ export default mixins(
const lastSelectedNode = this.lastSelectedNode;
if (options.position) {
newNodeData.position = CanvasHelpers.getNewNodePosition(this.getNodesWithPlaceholderNode(), options.position);
newNodeData.position = CanvasHelpers.getNewNodePosition(this.canvasStore.getNodesWithPlaceholderNode(), options.position);
} else if (lastSelectedNode) {
const lastSelectedConnection = this.lastSelectedConnection;
if (lastSelectedConnection) { // set when injecting into a connection
@@ -1741,7 +1636,7 @@ export default mixins(
// If added node is a trigger and it's the first one added to the canvas
// we place it at canvasAddButtonPosition to replace the canvas add button
const position = this.nodeTypesStore.isTriggerNode(nodeTypeName) && !this.containsTrigger
? this.canvasAddButtonPosition
? this.canvasStore.canvasAddButtonPosition
// If no node is active find a free spot
: this.lastClickPosition as XYPosition;
@@ -2199,8 +2094,8 @@ export default mixins(
this.workflowsStore.activeWorkflowExecution = null;
this.uiStore.stateIsDirty = false;
this.setZoomLevel(1);
this.zoomToFit();
this.canvasStore.setZoomLevel(1);
this.canvasStore.zoomToFit();
},
tryToAddWelcomeSticky: once(async function(this: any) {
const newWorkflow = this.workflowData;
@@ -3398,6 +3293,7 @@ export default mixins(
this.$root.$on('importWorkflowUrl', this.onImportWorkflowUrlEvent);
dataPinningEventBus.$on('pin-data', this.addPinDataConnections);
dataPinningEventBus.$on('unpin-data', this.removePinDataConnections);
this.canvasStore.isDemo = this.isDemo;
},
deactivated () {
document.removeEventListener('keydown', this.keyDown);
@@ -3425,44 +3321,6 @@ export default mixins(
</script>
<style scoped lang="scss">
.zoom-menu {
$--zoom-menu-margin: 15;
position: absolute;
left: $sidebar-width + $--zoom-menu-margin;
width: 210px;
bottom: 108px;
left: 35px;
line-height: 25px;
color: #444;
padding-right: 5px;
button {
border: var(--border-base);
}
>* {
+* {
margin-left: var(--spacing-3xs);
}
&:hover {
transform: scale(1.1);
}
}
}
.regular-zoom-menu {
@media (max-width: $breakpoint-2xs) {
bottom: 90px;
}
}
.demo-zoom-menu {
left: 10px;
bottom: 10px;
}
.node-view-root {
position: relative;
flex: 1;
@@ -3511,13 +3369,20 @@ export default mixins(
position: absolute;
display: flex;
justify-content: center;
align-items: center;
left: 50%;
transform: translateX(-50%);
bottom: 110px;
width: auto;
> * {
margin-inline-end: 0.625rem;
@media (max-width: $breakpoint-2xs) {
bottom: 150px;
}
button {
display: flex;
justify-content: center;
align-items: center;
}
}