Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(editor): Support adding nodes via drag and drop from node creator on new canvas #12197

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
} from '@/constants';

import { isCommunityPackageName } from '@/utils/nodeTypesUtils';
import { getNewNodePosition, NODE_SIZE } from '@/utils/nodeViewUtils';
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
import NodeIcon from '@/components/NodeIcon.vue';

Expand Down Expand Up @@ -93,15 +92,6 @@ const isTrigger = computed<boolean>(() => {
});

function onDragStart(event: DragEvent): void {
/**
* Workaround for firefox, that doesn't attach the pageX and pageY coordinates to "ondrag" event.
* All browsers attach the correct page coordinates to the "dragover" event.
* @bug https://bugzilla.mozilla.org/show_bug.cgi?id=505521
*/
document.body.addEventListener('dragover', onDragOver);

const { pageX: x, pageY: y } = event;

if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'copy';
event.dataTransfer.dropEffect = 'copy';
Expand All @@ -113,22 +103,9 @@ function onDragStart(event: DragEvent): void {
}

dragging.value = true;
draggablePosition.value = { x, y };
}

function onDragOver(event: DragEvent): void {
if (!dragging.value || (event.pageX === 0 && event.pageY === 0)) {
return;
}

const [x, y] = getNewNodePosition([], [event.pageX - NODE_SIZE / 2, event.pageY - NODE_SIZE / 2]);

draggablePosition.value = { x, y };
}

function onDragEnd(): void {
document.body.removeEventListener('dragover', onDragOver);

dragging.value = false;
setTimeout(() => {
draggablePosition.value = { x: -100, y: -100 };
Expand All @@ -144,7 +121,7 @@ function onCommunityNodeTooltipClick(event: MouseEvent) {

<template>
<!-- Node Item is draggable only if it doesn't contain actions -->
<n8n-node-creator-node
<N8nNodeCreatorNode
:draggable="!showActionArrow"
:class="$style.nodeItem"
:description="description"
Expand Down Expand Up @@ -176,12 +153,16 @@ function onCommunityNodeTooltipClick(event: MouseEvent) {
/>
</template>
<template #dragContent>
<div ref="draggableDataTransfer" :class="$style.draggableDataTransfer" />
<div v-show="dragging" :class="$style.draggable" :style="draggableStyle">
<div
ref="draggableDataTransfer"
v-show="dragging"
:class="$style.draggable"
:style="draggableStyle"
>
<NodeIcon :node-type="nodeType" :size="40" :shrink="false" @click.capture.stop />
</div>
</template>
</n8n-node-creator-node>
</N8nNodeCreatorNode>
</template>

<style lang="scss" module>
Expand Down
35 changes: 22 additions & 13 deletions packages/editor-ui/src/components/canvas/Canvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type {
} from '@/types';
import type { Connection, XYPosition, NodeDragEvent, GraphNode } from '@vue-flow/core';
import { useVueFlow, VueFlow, PanelPosition, MarkerType } from '@vue-flow/core';
import { Background } from '@vue-flow/background';
import { MiniMap } from '@vue-flow/minimap';
import Node from './elements/nodes/CanvasNode.vue';
import Edge from './elements/edges/CanvasEdge.vue';
Expand All @@ -25,7 +24,7 @@ import { GRID_SIZE } from '@/utils/nodeViewUtils';
import { CanvasKey } from '@/constants';
import { onKeyDown, onKeyUp, useThrottleFn } from '@vueuse/core';
import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue';
import CanvasBackgroundStripedPattern from './elements/CanvasBackgroundStripedPattern.vue';
import CanvasBackground from './elements/background/CanvasBackground.vue';
import { useCanvasTraversal } from '@/composables/useCanvasTraversal';
import { NodeConnectionType } from 'n8n-workflow';

Expand Down Expand Up @@ -65,6 +64,7 @@ const emit = defineEmits<{
'run:workflow': [];
'save:workflow': [];
'create:workflow': [];
'drag-and-drop': [position: XYPosition, event: DragEvent];
}>();

const props = withDefaults(
Expand Down Expand Up @@ -535,6 +535,20 @@ function onContextMenuAction(action: ContextMenuAction, nodeIds: string[]) {
}
}

/**
* Drag and drop
*/

function onDragOver(event: DragEvent) {
event.preventDefault();
}

function onDrop(event: DragEvent) {
const position = getProjectedPosition(event);

emit('drag-and-drop', position, event);
}

/**
* Minimap
*/
Expand Down Expand Up @@ -626,6 +640,7 @@ provide(CanvasKey, {
:id="id"
:nodes="nodes"
:edges="connections"
:class="classes"
:apply-changes="false"
:connection-line-options="{ markerEnd: MarkerType.ArrowClosed }"
:connection-radius="60"
Expand All @@ -635,7 +650,6 @@ provide(CanvasKey, {
:snap-grid="[GRID_SIZE, GRID_SIZE]"
:min-zoom="0"
:max-zoom="4"
:class="classes"
:selection-key-code="selectionKeyCode"
:pan-activation-key-code="panningKeyCode"
:disable-keyboard-a11y="true"
Expand All @@ -649,6 +663,8 @@ provide(CanvasKey, {
@move-end="onPaneMoveEnd"
@node-drag-stop="onNodeDragStop"
@selection-drag-stop="onSelectionDragStop"
@dragover="onDragOver"
@drop="onDrop"
>
<template #node-canvas-node="nodeProps">
<Node
Expand Down Expand Up @@ -687,16 +703,7 @@ provide(CanvasKey, {

<CanvasArrowHeadMarker :id="arrowHeadMarkerId" />

<Background data-test-id="canvas-background" pattern-color="#aaa" :gap="GRID_SIZE">
<template v-if="readOnly" #pattern-container="patternProps">
<CanvasBackgroundStripedPattern
:id="patternProps.id"
:x="viewport.x"
:y="viewport.y"
:zoom="viewport.zoom"
/>
</template>
</Background>
<CanvasBackground :viewport="viewport" :striped="readOnly" />

<Transition name="minimap">
<MiniMap
Expand Down Expand Up @@ -736,6 +743,8 @@ provide(CanvasKey, {

<style lang="scss" module>
.canvas {
width: 100%;
height: 100%;
opacity: 0;

&.ready {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { createComponentRenderer } from '@/__tests__/render';
import CanvasBackground from '@/components/canvas/elements/background/CanvasBackground.vue';

const renderComponent = createComponentRenderer(CanvasBackground);

describe('CanvasBackground', () => {
it('should render the background with the correct gap', () => {
const { getByTestId, html } = renderComponent({
props: { striped: false, viewport: { x: 0, y: 0, zoom: 1 } },
});
const background = getByTestId('canvas-background');

expect(background).toBeInTheDocument();
expect(html()).toMatchSnapshot();
});

it('should render the striped pattern when striped is true', () => {
const { getByTestId } = renderComponent({
props: { striped: true, viewport: { x: 0, y: 0, zoom: 1 } },
});
const pattern = getByTestId('canvas-background-striped-pattern');

expect(pattern).toBeInTheDocument();
});

it('should not render the striped pattern when striped is false', () => {
const { getByTestId } = renderComponent({
props: { striped: false, viewport: { x: 0, y: 0, zoom: 1 } },
});

expect(() => getByTestId('canvas-background-striped-pattern')).toThrow();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script setup lang="ts">
import { GRID_SIZE } from '@/utils/nodeViewUtils';
import CanvasBackgroundStripedPattern from './CanvasBackgroundStripedPattern.vue';
import { Background } from '@vue-flow/background';
import type { ViewportTransform } from '@vue-flow/core';

defineProps<{
striped: boolean;
viewport: ViewportTransform;
}>();
</script>
<template>
<Background data-test-id="canvas-background" pattern-color="#aaa" :gap="GRID_SIZE">
<template v-if="striped" #pattern-container="patternProps">
<CanvasBackgroundStripedPattern
:id="patternProps.id"
data-test-id="canvas-background-striped-pattern"
:x="viewport.x"
:y="viewport.y"
:zoom="viewport.zoom"
/>
</template>
</Background>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`CanvasBackground > should render the background with the correct gap 1`] = `
"<svg class="vue-flow__background vue-flow__container" style="height: 100%; width: 100%;" data-test-id="canvas-background">
<pattern id="pattern-vue-flow-0" x="0" y="0" width="20" height="20" patternTransform="translate(-11,-11)" patternUnits="userSpaceOnUse">
<circle cx="0.5" cy="0.5" r="0.5" fill="#aaa"></circle>
<!---->
</pattern>
<rect x="0" y="0" width="100%" height="100%" fill="url(#pattern-vue-flow-0)"></rect>
</svg>"
`;
24 changes: 24 additions & 0 deletions packages/editor-ui/src/views/NodeView.v2.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import type {
import { CanvasNodeRenderType, CanvasConnectionMode } from '@/types';
import {
CHAT_TRIGGER_NODE_TYPE,
DRAG_EVENT_DATA_KEY,
EnterpriseEditionFeature,
MAIN_HEADER_TABS,
MANUAL_CHAT_TRIGGER_NODE_TYPE,
Expand Down Expand Up @@ -1442,6 +1443,28 @@ function onClickPane(position: CanvasNode['position']) {
setNodeSelected();
}

/**
* Drag and Drop events
*/

async function onDragAndDrop(position: VueFlowXYPosition, event: DragEvent) {
if (!event.dataTransfer) {
return;
}

const dropData = jsonParse<AddedNodesAndConnections>(
event.dataTransfer.getData(DRAG_EVENT_DATA_KEY),
);

if (dropData) {
const insertNodePosition: XYPosition = [position.x, position.y];

await onAddNodesAndConnections(dropData, true, insertNodePosition);

onToggleNodeCreator({ createNodeActive: false, hasAddedNodes: true });
}
}

/**
* Custom Actions
*/
Expand Down Expand Up @@ -1615,6 +1638,7 @@ onBeforeUnmount(() => {
@save:workflow="onSaveWorkflow"
@create:workflow="onCreateWorkflow"
@viewport-change="onViewportChange"
@drag-and-drop="onDragAndDrop"
>
<div v-if="!isCanvasReadOnly" :class="$style.executionButtons">
<CanvasRunWorkflowButton
Expand Down
5 changes: 1 addition & 4 deletions packages/editor-ui/src/views/NodeView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2056,10 +2056,7 @@ export default defineComponent({
);
if (dropData) {
const mousePosition = this.getMousePositionWithinNodeView(event);
const insertNodePosition = [
mousePosition[0] - NodeViewUtils.NODE_SIZE / 2 + NodeViewUtils.GRID_SIZE,
mousePosition[1] - NodeViewUtils.NODE_SIZE / 2,
] as XYPosition;
const insertNodePosition: XYPosition = [mousePosition[0], mousePosition[1]];

await this.onAddNodes(dropData, true, insertNodePosition);
this.createNodeActive = false;
Expand Down
Loading