mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-31 15:37:26 -08:00
feat(editor): Support adding nodes via drag and drop from node creator on new canvas (#12197)
This commit is contained in:
parent
65b8e20049
commit
1bfd9c0e91
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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 };
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
|
|
@ -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';
|
||||
|
@ -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';
|
||||
|
||||
|
@ -65,6 +64,7 @@ const emit = defineEmits<{
|
|||
'run:workflow': [];
|
||||
'save:workflow': [];
|
||||
'create:workflow': [];
|
||||
'drag-and-drop': [position: XYPosition, event: DragEvent];
|
||||
}>();
|
||||
|
||||
const props = withDefaults(
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -736,6 +743,8 @@ provide(CanvasKey, {
|
|||
|
||||
<style lang="scss" module>
|
||||
.canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
|
||||
&.ready {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
|
@ -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>"
|
||||
`;
|
|
@ -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,
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue