mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(editor): Add plus handle design with ability to add connected nodes in new canvas (no-changelog) (#10097)
This commit is contained in:
@@ -1,7 +1,13 @@
|
|||||||
import { CanvasNodeKey } from '@/constants';
|
import { CanvasNodeHandleKey, CanvasNodeKey } from '@/constants';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import type { CanvasNode, CanvasNodeData } from '@/types';
|
import type {
|
||||||
import { CanvasNodeRenderType } from '@/types';
|
CanvasNode,
|
||||||
|
CanvasNodeData,
|
||||||
|
CanvasNodeHandleInjectionData,
|
||||||
|
CanvasNodeInjectionData,
|
||||||
|
} from '@/types';
|
||||||
|
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
|
||||||
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
|
||||||
export function createCanvasNodeData({
|
export function createCanvasNodeData({
|
||||||
id = 'node',
|
id = 'node',
|
||||||
@@ -11,7 +17,7 @@ export function createCanvasNodeData({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
inputs = [],
|
inputs = [],
|
||||||
outputs = [],
|
outputs = [],
|
||||||
connections = { input: {}, output: {} },
|
connections = { [CanvasConnectionMode.Input]: {}, [CanvasConnectionMode.Output]: {} },
|
||||||
execution = { running: false },
|
execution = { running: false },
|
||||||
issues = { items: [], visible: false },
|
issues = { items: [], visible: false },
|
||||||
pinnedData = { count: 0, visible: false },
|
pinnedData = { count: 0, visible: false },
|
||||||
@@ -73,7 +79,12 @@ export function createCanvasNodeProvide({
|
|||||||
label = 'Test Node',
|
label = 'Test Node',
|
||||||
selected = false,
|
selected = false,
|
||||||
data = {},
|
data = {},
|
||||||
}: { id?: string; label?: string; selected?: boolean; data?: Partial<CanvasNodeData> } = {}) {
|
}: {
|
||||||
|
id?: string;
|
||||||
|
label?: string;
|
||||||
|
selected?: boolean;
|
||||||
|
data?: Partial<CanvasNodeData>;
|
||||||
|
} = {}) {
|
||||||
const props = createCanvasNodeProps({ id, label, selected, data });
|
const props = createCanvasNodeProps({ id, label, selected, data });
|
||||||
return {
|
return {
|
||||||
[`${CanvasNodeKey}`]: {
|
[`${CanvasNodeKey}`]: {
|
||||||
@@ -81,7 +92,28 @@ export function createCanvasNodeProvide({
|
|||||||
label: ref(props.label),
|
label: ref(props.label),
|
||||||
selected: ref(props.selected),
|
selected: ref(props.selected),
|
||||||
data: ref(props.data),
|
data: ref(props.data),
|
||||||
},
|
} satisfies CanvasNodeInjectionData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCanvasHandleProvide({
|
||||||
|
label = 'Handle',
|
||||||
|
mode = CanvasConnectionMode.Input,
|
||||||
|
type = NodeConnectionType.Main,
|
||||||
|
connected = false,
|
||||||
|
}: {
|
||||||
|
label?: string;
|
||||||
|
mode?: CanvasConnectionMode;
|
||||||
|
type?: NodeConnectionType;
|
||||||
|
connected?: boolean;
|
||||||
|
} = {}) {
|
||||||
|
return {
|
||||||
|
[`${CanvasNodeHandleKey}`]: {
|
||||||
|
label: ref(label),
|
||||||
|
mode: ref(mode),
|
||||||
|
type: ref(type),
|
||||||
|
connected: ref(connected),
|
||||||
|
} satisfies CanvasNodeHandleInjectionData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const emit = defineEmits<{
|
|||||||
'update:node:selected': [id: string];
|
'update:node:selected': [id: string];
|
||||||
'update:node:name': [id: string];
|
'update:node:name': [id: string];
|
||||||
'update:node:parameters': [id: string, parameters: Record<string, unknown>];
|
'update:node:parameters': [id: string, parameters: Record<string, unknown>];
|
||||||
|
'click:node:add': [id: string, handle: string];
|
||||||
'run:node': [id: string];
|
'run:node': [id: string];
|
||||||
'delete:node': [id: string];
|
'delete:node': [id: string];
|
||||||
'create:node': [source: NodeCreatorOpenSource];
|
'create:node': [source: NodeCreatorOpenSource];
|
||||||
@@ -108,6 +109,10 @@ const paneReady = ref(false);
|
|||||||
* Nodes
|
* Nodes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
function onClickNodeAdd(id: string, handle: string) {
|
||||||
|
emit('click:node:add', id, handle);
|
||||||
|
}
|
||||||
|
|
||||||
function onNodeDragStop(e: NodeDragEvent) {
|
function onNodeDragStop(e: NodeDragEvent) {
|
||||||
e.nodes.forEach((node) => {
|
e.nodes.forEach((node) => {
|
||||||
onUpdateNodePosition(node.id, node.position);
|
onUpdateNodePosition(node.id, node.position);
|
||||||
@@ -351,6 +356,7 @@ onPaneReady(async () => {
|
|||||||
@open:contextmenu="onOpenNodeContextMenu"
|
@open:contextmenu="onOpenNodeContextMenu"
|
||||||
@update="onUpdateNodeParameters"
|
@update="onUpdateNodeParameters"
|
||||||
@move="onUpdateNodePosition"
|
@move="onUpdateNodePosition"
|
||||||
|
@add="onClickNodeAdd"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createComponentRenderer } from '@/__tests__/render';
|
|||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import { setActivePinia } from 'pinia';
|
import { setActivePinia } from 'pinia';
|
||||||
import { Position } from '@vue-flow/core';
|
import { Position } from '@vue-flow/core';
|
||||||
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
|
||||||
const DEFAULT_PROPS = {
|
const DEFAULT_PROPS = {
|
||||||
sourceX: 0,
|
sourceX: 0,
|
||||||
@@ -14,8 +15,8 @@ const DEFAULT_PROPS = {
|
|||||||
targetPosition: Position.Bottom,
|
targetPosition: Position.Bottom,
|
||||||
data: {
|
data: {
|
||||||
status: undefined,
|
status: undefined,
|
||||||
source: { index: 0, type: 'main' },
|
source: { index: 0, type: NodeConnectionType.Main },
|
||||||
target: { index: 0, type: 'main' },
|
target: { index: 0, type: NodeConnectionType.Main },
|
||||||
},
|
},
|
||||||
} satisfies Partial<CanvasEdgeProps>;
|
} satisfies Partial<CanvasEdgeProps>;
|
||||||
const renderComponent = createComponentRenderer(CanvasEdge, {
|
const renderComponent = createComponentRenderer(CanvasEdge, {
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import HandleRenderer from '@/components/canvas/elements/handles/HandleRenderer.vue';
|
import CanvasHandleRenderer from '@/components/canvas/elements/handles/CanvasHandleRenderer.vue';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { CanvasNodeHandleKey } from '@/constants';
|
import { CanvasNodeHandleKey } from '@/constants';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import { CanvasConnectionMode } from '@/types';
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(HandleRenderer);
|
const renderComponent = createComponentRenderer(CanvasHandleRenderer);
|
||||||
|
|
||||||
const Handle = {
|
const Handle = {
|
||||||
template: '<div><slot /></div>',
|
template: '<div><slot /></div>',
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('HandleRenderer', () => {
|
describe('CanvasHandleRenderer', () => {
|
||||||
it('should render the main input handle correctly', async () => {
|
it('should render the main input handle correctly', async () => {
|
||||||
const { container } = renderComponent({
|
const { container } = renderComponent({
|
||||||
props: {
|
props: {
|
||||||
mode: 'input',
|
mode: CanvasConnectionMode.Input,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
index: 0,
|
index: 0,
|
||||||
position: 'left',
|
position: 'left',
|
||||||
@@ -29,13 +30,13 @@ describe('HandleRenderer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(container.querySelector('.handle')).toBeInTheDocument();
|
expect(container.querySelector('.handle')).toBeInTheDocument();
|
||||||
expect(container.querySelector('.canvas-node-handle-main-input')).toBeInTheDocument();
|
expect(container.querySelector('.inputs.main')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the main output handle correctly', async () => {
|
it('should render the main output handle correctly', async () => {
|
||||||
const { container } = renderComponent({
|
const { container } = renderComponent({
|
||||||
props: {
|
props: {
|
||||||
mode: 'output',
|
mode: CanvasConnectionMode.Output,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
index: 0,
|
index: 0,
|
||||||
position: 'right',
|
position: 'right',
|
||||||
@@ -50,13 +51,13 @@ describe('HandleRenderer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(container.querySelector('.handle')).toBeInTheDocument();
|
expect(container.querySelector('.handle')).toBeInTheDocument();
|
||||||
expect(container.querySelector('.canvas-node-handle-main-output')).toBeInTheDocument();
|
expect(container.querySelector('.outputs.main')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the non-main handle correctly', async () => {
|
it('should render the non-main handle correctly', async () => {
|
||||||
const { container } = renderComponent({
|
const { container } = renderComponent({
|
||||||
props: {
|
props: {
|
||||||
mode: 'input',
|
mode: CanvasConnectionMode.Input,
|
||||||
type: NodeConnectionType.AiTool,
|
type: NodeConnectionType.AiTool,
|
||||||
index: 0,
|
index: 0,
|
||||||
position: 'top',
|
position: 'top',
|
||||||
@@ -71,7 +72,7 @@ describe('HandleRenderer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(container.querySelector('.handle')).toBeInTheDocument();
|
expect(container.querySelector('.handle')).toBeInTheDocument();
|
||||||
expect(container.querySelector('.canvas-node-handle-non-main')).toBeInTheDocument();
|
expect(container.querySelector('.inputs.ai_tool')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should provide the label correctly', async () => {
|
it('should provide the label correctly', async () => {
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
/* eslint-disable vue/no-multiple-template-root */
|
||||||
|
import { computed, h, provide, toRef, useCssModule } from 'vue';
|
||||||
|
import type { CanvasConnectionPort, CanvasElementPortWithRenderData } from '@/types';
|
||||||
|
import { CanvasConnectionMode } from '@/types';
|
||||||
|
|
||||||
|
import type { ValidConnectionFunc } from '@vue-flow/core';
|
||||||
|
import { Handle } from '@vue-flow/core';
|
||||||
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
import CanvasHandleMainOutput from '@/components/canvas/elements/handles/render-types/CanvasHandleMainOutput.vue';
|
||||||
|
import CanvasHandleNonMainInput from '@/components/canvas/elements/handles/render-types/CanvasHandleNonMainInput.vue';
|
||||||
|
import { CanvasNodeHandleKey } from '@/constants';
|
||||||
|
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
mode: CanvasConnectionMode;
|
||||||
|
connected?: boolean;
|
||||||
|
label?: string;
|
||||||
|
type: CanvasConnectionPort['type'];
|
||||||
|
index: CanvasConnectionPort['index'];
|
||||||
|
position: CanvasElementPortWithRenderData['position'];
|
||||||
|
offset: CanvasElementPortWithRenderData['offset'];
|
||||||
|
isValidConnection: ValidConnectionFunc;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
add: [handle: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const style = useCssModule();
|
||||||
|
|
||||||
|
const handleType = computed(() =>
|
||||||
|
props.mode === CanvasConnectionMode.Input ? 'target' : 'source',
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleString = computed(() =>
|
||||||
|
createCanvasConnectionHandleString({
|
||||||
|
mode: props.mode,
|
||||||
|
type: props.type,
|
||||||
|
index: props.index,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const isConnectableStart = computed(() => {
|
||||||
|
return props.mode === CanvasConnectionMode.Output || props.type !== NodeConnectionType.Main;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isConnectableEnd = computed(() => {
|
||||||
|
return props.mode === CanvasConnectionMode.Input || props.type !== NodeConnectionType.Main;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClasses = computed(() => [style.handle, style[props.type], style[props.mode]]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render additional elements
|
||||||
|
*/
|
||||||
|
|
||||||
|
const hasRenderType = computed(() => {
|
||||||
|
return (
|
||||||
|
(props.type === NodeConnectionType.Main && props.mode === CanvasConnectionMode.Output) ||
|
||||||
|
props.type !== NodeConnectionType.Main
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderTypeClasses = computed(() => [style.renderType, style[props.position]]);
|
||||||
|
|
||||||
|
const RenderType = () => {
|
||||||
|
let Component;
|
||||||
|
|
||||||
|
if (props.mode === CanvasConnectionMode.Output) {
|
||||||
|
if (props.type === NodeConnectionType.Main) {
|
||||||
|
Component = CanvasHandleMainOutput;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (props.type !== NodeConnectionType.Main) {
|
||||||
|
Component = CanvasHandleNonMainInput;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Component ? h(Component) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event bindings
|
||||||
|
*/
|
||||||
|
|
||||||
|
function onAdd() {
|
||||||
|
emit('add', handleString.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide
|
||||||
|
*/
|
||||||
|
|
||||||
|
const label = toRef(props, 'label');
|
||||||
|
const connected = toRef(props, 'connected');
|
||||||
|
const mode = toRef(props, 'mode');
|
||||||
|
const type = toRef(props, 'type');
|
||||||
|
|
||||||
|
provide(CanvasNodeHandleKey, {
|
||||||
|
label,
|
||||||
|
mode,
|
||||||
|
type,
|
||||||
|
connected,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Handle
|
||||||
|
v-bind="$attrs"
|
||||||
|
:id="handleString"
|
||||||
|
:class="handleClasses"
|
||||||
|
:type="handleType"
|
||||||
|
:position="position"
|
||||||
|
:style="offset"
|
||||||
|
:connectable-start="isConnectableStart"
|
||||||
|
:connectable-end="isConnectableEnd"
|
||||||
|
:is-valid-connection="isValidConnection"
|
||||||
|
/>
|
||||||
|
<RenderType
|
||||||
|
v-if="hasRenderType"
|
||||||
|
:class="renderTypeClasses"
|
||||||
|
:connected="connected"
|
||||||
|
:style="offset"
|
||||||
|
:label="label"
|
||||||
|
@add="onAdd"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.handle {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border: 0;
|
||||||
|
z-index: 1;
|
||||||
|
background: var(--color-foreground-xdark);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.inputs {
|
||||||
|
&.main {
|
||||||
|
width: 8px;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.main) {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
transform: rotate(45deg) translate(2px, 2px);
|
||||||
|
border-radius: 0;
|
||||||
|
background: hsl(
|
||||||
|
var(--node-type-supplemental-color-h) var(--node-type-supplemental-color-s)
|
||||||
|
var(--node-type-supplemental-color-l)
|
||||||
|
);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.renderType {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 0;
|
||||||
|
|
||||||
|
&.top {
|
||||||
|
top: 0;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.right {
|
||||||
|
right: 0;
|
||||||
|
transform: translate(100%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.left {
|
||||||
|
left: 0;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bottom {
|
||||||
|
bottom: 0;
|
||||||
|
transform: translate(-50%, 50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { computed, h, provide, toRef, useCssModule } from 'vue';
|
|
||||||
import type { CanvasConnectionPort, CanvasElementPortWithPosition } from '@/types';
|
|
||||||
|
|
||||||
import type { ValidConnectionFunc } from '@vue-flow/core';
|
|
||||||
import { Handle } from '@vue-flow/core';
|
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
|
||||||
import CanvasHandleMainInput from '@/components/canvas/elements/handles/render-types/CanvasHandleMainInput.vue';
|
|
||||||
import CanvasHandleMainOutput from '@/components/canvas/elements/handles/render-types/CanvasHandleMainOutput.vue';
|
|
||||||
import CanvasHandleNonMain from '@/components/canvas/elements/handles/render-types/CanvasHandleNonMain.vue';
|
|
||||||
import { CanvasNodeHandleKey } from '@/constants';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
mode: 'output' | 'input';
|
|
||||||
label?: string;
|
|
||||||
type: CanvasConnectionPort['type'];
|
|
||||||
index: CanvasConnectionPort['index'];
|
|
||||||
position: CanvasElementPortWithPosition['position'];
|
|
||||||
offset: CanvasElementPortWithPosition['offset'];
|
|
||||||
isValidConnection: ValidConnectionFunc;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const $style = useCssModule();
|
|
||||||
|
|
||||||
const handleType = computed(() => (props.mode === 'input' ? 'target' : 'source'));
|
|
||||||
|
|
||||||
const isConnectableStart = computed(() => {
|
|
||||||
return props.mode === 'output' || props.type !== NodeConnectionType.Main;
|
|
||||||
});
|
|
||||||
|
|
||||||
const isConnectableEnd = computed(() => {
|
|
||||||
return props.mode === 'input' || props.type !== NodeConnectionType.Main;
|
|
||||||
});
|
|
||||||
|
|
||||||
const Render = (renderProps: { label?: string }) => {
|
|
||||||
let Component;
|
|
||||||
|
|
||||||
if (props.type === NodeConnectionType.Main) {
|
|
||||||
if (props.mode === 'input') {
|
|
||||||
Component = CanvasHandleMainInput;
|
|
||||||
} else {
|
|
||||||
Component = CanvasHandleMainOutput;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Component = CanvasHandleNonMain;
|
|
||||||
}
|
|
||||||
|
|
||||||
return h(Component, renderProps);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provide
|
|
||||||
*/
|
|
||||||
|
|
||||||
const label = toRef(props, 'label');
|
|
||||||
|
|
||||||
provide(CanvasNodeHandleKey, {
|
|
||||||
label,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Handle
|
|
||||||
:id="`${mode}s/${type}/${index}`"
|
|
||||||
:class="[$style.handle]"
|
|
||||||
:type="handleType"
|
|
||||||
:position="position"
|
|
||||||
:style="offset"
|
|
||||||
:connectable-start="isConnectableStart"
|
|
||||||
:connectable-end="isConnectableEnd"
|
|
||||||
:is-valid-connection="isValidConnection"
|
|
||||||
>
|
|
||||||
<Render :label="label" />
|
|
||||||
</Handle>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" module>
|
|
||||||
.handle {
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
display: inline-flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border: 0;
|
|
||||||
background: none;
|
|
||||||
|
|
||||||
> * {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import CanvasHandleMainInput from '@/components/canvas/elements/handles/render-types/CanvasHandleMainInput.vue';
|
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
|
||||||
import { CanvasNodeHandleKey } from '@/constants';
|
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(CanvasHandleMainInput);
|
|
||||||
|
|
||||||
describe('CanvasHandleMainInput', () => {
|
|
||||||
it('should render correctly', async () => {
|
|
||||||
const label = 'Test Label';
|
|
||||||
const { container, getByText } = renderComponent({
|
|
||||||
global: {
|
|
||||||
provide: {
|
|
||||||
[`${CanvasNodeHandleKey}`]: { label: ref(label) },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(container.querySelector('.canvas-node-handle-main-input')).toBeInTheDocument();
|
|
||||||
expect(getByText(label)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { computed, inject, useCssModule } from 'vue';
|
|
||||||
import { CanvasNodeHandleKey } from '@/constants';
|
|
||||||
|
|
||||||
const handle = inject(CanvasNodeHandleKey);
|
|
||||||
|
|
||||||
const $style = useCssModule();
|
|
||||||
|
|
||||||
const label = computed(() => handle?.label.value ?? '');
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<div :class="['canvas-node-handle-main-input', $style.handle]">
|
|
||||||
<div :class="$style.label">{{ label }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" module>
|
|
||||||
.handle {
|
|
||||||
width: 8px;
|
|
||||||
height: 16px;
|
|
||||||
border-radius: 0;
|
|
||||||
background: var(--color-foreground-xdark);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
right: 12px;
|
|
||||||
transform: translate(0, -50%);
|
|
||||||
font-size: var(--font-size-2xs);
|
|
||||||
color: var(--color-foreground-xdark);
|
|
||||||
background: var(--color-background-light);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import CanvasHandleMainOutput from '@/components/canvas/elements/handles/render-types/CanvasHandleMainOutput.vue';
|
import CanvasHandleMainOutput from '@/components/canvas/elements/handles/render-types/CanvasHandleMainOutput.vue';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { CanvasNodeHandleKey } from '@/constants';
|
import { createCanvasHandleProvide } from '@/__tests__/data';
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(CanvasHandleMainOutput);
|
const renderComponent = createComponentRenderer(CanvasHandleMainOutput);
|
||||||
|
|
||||||
@@ -11,7 +10,7 @@ describe('CanvasHandleMainOutput', () => {
|
|||||||
const { container, getByText } = renderComponent({
|
const { container, getByText } = renderComponent({
|
||||||
global: {
|
global: {
|
||||||
provide: {
|
provide: {
|
||||||
[`${CanvasNodeHandleKey}`]: { label: ref(label) },
|
...createCanvasHandleProvide({ label }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,89 +1,27 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, useCssModule } from 'vue';
|
import { useCanvasNodeHandle } from '@/composables/useCanvasNodeHandle';
|
||||||
import { CanvasNodeHandleKey } from '@/constants';
|
import CanvasHandlePlus from '@/components/canvas/elements/handles/render-types/parts/CanvasHandlePlus.vue';
|
||||||
|
|
||||||
const handle = inject(CanvasNodeHandleKey);
|
const emit = defineEmits<{
|
||||||
|
add: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
// const group = svg.node('g');
|
const { label, connected } = useCanvasNodeHandle();
|
||||||
// const containerBorder = svg.node('rect', {
|
|
||||||
// rx: 3,
|
|
||||||
// 'stroke-width': 2,
|
|
||||||
// fillOpacity: 0,
|
|
||||||
// height: ep.params.dimensions - 2,
|
|
||||||
// width: ep.params.dimensions - 2,
|
|
||||||
// y: 1,
|
|
||||||
// x: 1,
|
|
||||||
// });
|
|
||||||
// const plusPath = svg.node('path', {
|
|
||||||
// d: 'm16.40655,10.89837l-3.30491,0l0,-3.30491c0,-0.40555 -0.32889,-0.73443 -0.73443,-0.73443l-0.73443,0c-0.40554,0 -0.73442,0.32888 -0.73442,0.73443l0,3.30491l-3.30491,0c-0.40555,0 -0.73443,0.32888 -0.73443,0.73442l0,0.73443c0,0.40554 0.32888,0.73443 0.73443,0.73443l3.30491,0l0,3.30491c0,0.40554 0.32888,0.73442 0.73442,0.73442l0.73443,0c0.40554,0 0.73443,-0.32888 0.73443,-0.73442l0,-3.30491l3.30491,0c0.40554,0 0.73442,-0.32889 0.73442,-0.73443l0,-0.73443c0,-0.40554 -0.32888,-0.73442 -0.73442,-0.73442z',
|
|
||||||
// });
|
|
||||||
// if (ep.params.size !== 'medium') {
|
|
||||||
// ep.addClass(ep.params.size);
|
|
||||||
// }
|
|
||||||
// group.appendChild(containerBorder);
|
|
||||||
// group.appendChild(plusPath);
|
|
||||||
//
|
|
||||||
// ep.setupOverlays();
|
|
||||||
// ep.setVisible(false);
|
|
||||||
// return group;
|
|
||||||
|
|
||||||
const $style = useCssModule();
|
function onClickAdd() {
|
||||||
|
emit('add');
|
||||||
const label = computed(() => handle?.label.value ?? '');
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div :class="['canvas-node-handle-main-output', $style.handle]">
|
<div :class="['canvas-node-handle-main-output', $style.handle]">
|
||||||
<div :class="$style.label">{{ label }}</div>
|
<div :class="$style.label">{{ label }}</div>
|
||||||
<div :class="$style.circle" />
|
<CanvasHandlePlus v-if="!connected" @click:plus="onClickAdd" />
|
||||||
<!-- @TODO Determine whether handle is connected and find a way to make it work without pointer-events: none -->
|
|
||||||
<!-- <svg :class="$style.plus" viewBox="0 0 70 24">-->
|
|
||||||
<!-- <line x1="0" y1="12" x2="46" y2="12" stroke="var(--color-foreground-xdark)" />-->
|
|
||||||
<!-- <rect-->
|
|
||||||
<!-- x="46"-->
|
|
||||||
<!-- y="2"-->
|
|
||||||
<!-- width="20"-->
|
|
||||||
<!-- height="20"-->
|
|
||||||
<!-- stroke="var(--color-foreground-xdark)"-->
|
|
||||||
<!-- stroke-width="2"-->
|
|
||||||
<!-- rx="4"-->
|
|
||||||
<!-- fill="#ffffff"-->
|
|
||||||
<!-- />-->
|
|
||||||
<!-- <g transform="translate(44, 0)">-->
|
|
||||||
<!-- <path-->
|
|
||||||
<!-- fill="var(--color-foreground-xdark)"-->
|
|
||||||
<!-- d="m16.40655,10.89837l-3.30491,0l0,-3.30491c0,-0.40555 -0.32889,-0.73443 -0.73443,-0.73443l-0.73443,0c-0.40554,0 -0.73442,0.32888 -0.73442,0.73443l0,3.30491l-3.30491,0c-0.40555,0 -0.73443,0.32888 -0.73443,0.73442l0,0.73443c0,0.40554 0.32888,0.73443 0.73443,0.73443l3.30491,0l0,3.30491c0,0.40554 0.32888,0.73442 0.73442,0.73442l0.73443,0c0.40554,0 0.73443,-0.32888 0.73443,-0.73442l0,-3.30491l3.30491,0c0.40554,0 0.73442,-0.32889 0.73442,-0.73443l0,-0.73443c0,-0.40554 -0.32888,-0.73442 -0.73442,-0.73442z"-->
|
|
||||||
<!-- ></path>-->
|
|
||||||
<!-- </g>-->
|
|
||||||
<!-- </svg>-->
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.handle {
|
.handle {
|
||||||
width: 16px;
|
:global(.vue-flow__handle:not(.connectionindicator)) + & {
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.circle {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border-radius: 100%;
|
|
||||||
background: var(--color-foreground-xdark);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.plus {
|
|
||||||
position: absolute;
|
|
||||||
left: 16px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(0, -50%);
|
|
||||||
width: 70px;
|
|
||||||
height: 24px;
|
|
||||||
|
|
||||||
:global(.vue-flow__handle.connecting) & {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,7 +29,7 @@ const label = computed(() => handle?.label.value ?? '');
|
|||||||
.label {
|
.label {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 20px;
|
left: var(--spacing-s);
|
||||||
transform: translate(0, -50%);
|
transform: translate(0, -50%);
|
||||||
font-size: var(--font-size-2xs);
|
font-size: var(--font-size-2xs);
|
||||||
color: var(--color-foreground-xdark);
|
color: var(--color-foreground-xdark);
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { computed, inject, useCssModule } from 'vue';
|
|
||||||
import { CanvasNodeHandleKey } from '@/constants';
|
|
||||||
|
|
||||||
const handle = inject(CanvasNodeHandleKey);
|
|
||||||
|
|
||||||
const $style = useCssModule();
|
|
||||||
|
|
||||||
const label = computed(() => handle?.label.value ?? '');
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<div :class="['canvas-node-handle-non-main', $style.handle]">
|
|
||||||
<div :class="$style.diamond" />
|
|
||||||
<div :class="$style.label">{{ label }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" module>
|
|
||||||
.handle {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diamond {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
transform: rotate(45deg);
|
|
||||||
background: hsl(
|
|
||||||
var(--node-type-supplemental-color-h) var(--node-type-supplemental-color-s)
|
|
||||||
var(--node-type-supplemental-color-l)
|
|
||||||
);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
position: absolute;
|
|
||||||
top: 18px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, 0);
|
|
||||||
font-size: var(--font-size-2xs);
|
|
||||||
color: var(--color-foreground-xdark);
|
|
||||||
background: var(--color-background-light);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
import CanvasHandleNonMain from '@/components/canvas/elements/handles/render-types/CanvasHandleNonMain.vue';
|
import CanvasHandleNonMainInput from '@/components/canvas/elements/handles/render-types/CanvasHandleNonMainInput.vue';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { CanvasNodeHandleKey } from '@/constants';
|
import { createCanvasHandleProvide } from '@/__tests__/data';
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(CanvasHandleNonMain);
|
const renderComponent = createComponentRenderer(CanvasHandleNonMainInput);
|
||||||
|
|
||||||
describe('CanvasHandleNonMain', () => {
|
describe('CanvasHandleNonMainInput', () => {
|
||||||
it('should render correctly', async () => {
|
it('should render correctly', async () => {
|
||||||
const label = 'Test Label';
|
const label = 'Test Label';
|
||||||
const { container, getByText } = renderComponent({
|
const { container, getByText } = renderComponent({
|
||||||
global: {
|
global: {
|
||||||
provide: {
|
provide: {
|
||||||
[`${CanvasNodeHandleKey}`]: { label: ref(label) },
|
...createCanvasHandleProvide({ label }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import CanvasHandlePlus from '@/components/canvas/elements/handles/render-types/parts/CanvasHandlePlus.vue';
|
||||||
|
import { useCanvasNodeHandle } from '@/composables/useCanvasNodeHandle';
|
||||||
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
add: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { label, connected, type } = useCanvasNodeHandle();
|
||||||
|
|
||||||
|
const isAddButtonVisible = computed(
|
||||||
|
() => !connected.value || type.value === NodeConnectionType.AiTool,
|
||||||
|
);
|
||||||
|
|
||||||
|
function onClickAdd() {
|
||||||
|
emit('add');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div :class="['canvas-node-handle-non-main', $style.handle]">
|
||||||
|
<div :class="$style.label">{{ label }}</div>
|
||||||
|
<CanvasHandlePlus v-if="isAddButtonVisible" :class="$style.plus" @click="onClickAdd" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.handle {
|
||||||
|
:global(.vue-flow__handle:not(.connectionindicator)) + & {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
font-size: var(--font-size-2xs);
|
||||||
|
color: var(--color-foreground-xdark);
|
||||||
|
background: var(--color-background-light);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plus {
|
||||||
|
transform: rotate(90deg) translateX(50%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { fireEvent } from '@testing-library/vue';
|
||||||
|
import CanvasHandlePlus from './CanvasHandlePlus.vue';
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import { createCanvasHandleProvide } from '@/__tests__/data';
|
||||||
|
|
||||||
|
const renderComponent = createComponentRenderer(CanvasHandlePlus, {
|
||||||
|
global: {
|
||||||
|
provide: {
|
||||||
|
...createCanvasHandleProvide(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('CanvasHandlePlus', () => {
|
||||||
|
it('should render with default props', () => {
|
||||||
|
const { html } = renderComponent();
|
||||||
|
|
||||||
|
expect(html()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits click:plus event when plus icon is clicked', async () => {
|
||||||
|
const { container, emitted } = renderComponent();
|
||||||
|
const plusIcon = container.querySelector('svg.plus');
|
||||||
|
|
||||||
|
if (!plusIcon) throw new Error('Plus icon not found');
|
||||||
|
|
||||||
|
await fireEvent.click(plusIcon);
|
||||||
|
|
||||||
|
expect(emitted()).toHaveProperty('click:plus');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies correct classes based on position prop', () => {
|
||||||
|
const positions = ['top', 'right', 'bottom', 'left'];
|
||||||
|
|
||||||
|
positions.forEach((position) => {
|
||||||
|
const { container } = renderComponent({
|
||||||
|
props: { position },
|
||||||
|
});
|
||||||
|
expect(container.firstChild).toHaveClass(position);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders SVG elements correctly', () => {
|
||||||
|
const { container } = renderComponent();
|
||||||
|
|
||||||
|
const lineSvg = container.querySelector('svg.line');
|
||||||
|
expect(lineSvg).toBeTruthy();
|
||||||
|
expect(lineSvg?.getAttribute('viewBox')).toBe('0 0 46 24');
|
||||||
|
|
||||||
|
const plusSvg = container.querySelector('svg.plus');
|
||||||
|
expect(plusSvg).toBeTruthy();
|
||||||
|
expect(plusSvg?.getAttribute('viewBox')).toBe('0 0 24 24');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, useCssModule } from 'vue';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
position: 'top' | 'right' | 'bottom' | 'left';
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'click:plus': [event: MouseEvent];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const style = useCssModule();
|
||||||
|
|
||||||
|
const classes = computed(() => [style.wrapper, style[props.position]]);
|
||||||
|
|
||||||
|
function onClick(event: MouseEvent) {
|
||||||
|
emit('click:plus', event);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="classes">
|
||||||
|
<svg :class="$style.line" viewBox="0 0 46 24">
|
||||||
|
<line
|
||||||
|
x1="0"
|
||||||
|
y1="12"
|
||||||
|
x2="46"
|
||||||
|
y2="12"
|
||||||
|
stroke="var(--color-foreground-xdark)"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<svg :class="$style.plus" viewBox="0 0 24 24" @click="onClick">
|
||||||
|
<rect
|
||||||
|
x="2"
|
||||||
|
y="2"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
stroke="var(--color-foreground-xdark)"
|
||||||
|
stroke-width="2"
|
||||||
|
rx="4"
|
||||||
|
fill="#ffffff"
|
||||||
|
/>
|
||||||
|
<g transform="translate(0, 0)">
|
||||||
|
<path
|
||||||
|
fill="var(--color-foreground-xdark)"
|
||||||
|
d="m16.40655,10.89837l-3.30491,0l0,-3.30491c0,-0.40555 -0.32889,-0.73443 -0.73443,-0.73443l-0.73443,0c-0.40554,0 -0.73442,0.32888 -0.73442,0.73443l0,3.30491l-3.30491,0c-0.40555,0 -0.73443,0.32888 -0.73443,0.73442l0,0.73443c0,0.40554 0.32888,0.73443 0.73443,0.73443l3.30491,0l0,3.30491c0,0.40554 0.32888,0.73442 0.73442,0.73442l0.73443,0c0.40554,0 0.73443,-0.32888 0.73443,-0.73442l0,-3.30491l3.30491,0c0.40554,0 0.73442,-0.32889 0.73442,-0.73443l0,-0.73443c0,-0.40554 -0.32888,-0.73442 -0.73442,-0.73442z"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 70px;
|
||||||
|
height: 24px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
width: 46px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plus {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin-left: -1px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
path {
|
||||||
|
fill: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
rect {
|
||||||
|
stroke: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`CanvasHandlePlus > should render with default props 1`] = `
|
||||||
|
"<div class="wrapper right"><svg class="line" viewBox="0 0 46 24">
|
||||||
|
<line x1="0" y1="12" x2="46" y2="12" stroke="var(--color-foreground-xdark)" stroke-width="2"></line>
|
||||||
|
</svg><svg class="plus" viewBox="0 0 24 24">
|
||||||
|
<rect x="2" y="2" width="20" height="20" stroke="var(--color-foreground-xdark)" stroke-width="2" rx="4" fill="#ffffff"></rect>
|
||||||
|
<g transform="translate(0, 0)">
|
||||||
|
<path fill="var(--color-foreground-xdark)" d="m16.40655,10.89837l-3.30491,0l0,-3.30491c0,-0.40555 -0.32889,-0.73443 -0.73443,-0.73443l-0.73443,0c-0.40554,0 -0.73442,0.32888 -0.73442,0.73443l0,3.30491l-3.30491,0c-0.40555,0 -0.73443,0.32888 -0.73443,0.73442l0,0.73443c0,0.40554 0.32888,0.73443 0.73443,0.73443l3.30491,0l0,3.30491c0,0.40554 0.32888,0.73442 0.73442,0.73442l0.73443,0c0.40554,0 0.73443,-0.32888 0.73443,-0.73442l0,-3.30491l3.30491,0c0.40554,0 0.73442,-0.32889 0.73442,-0.73443l0,-0.73443c0,-0.40554 -0.32888,-0.73442 -0.73442,-0.73442z"></path>
|
||||||
|
</g>
|
||||||
|
</svg></div>"
|
||||||
|
`;
|
||||||
@@ -66,7 +66,7 @@ describe('CanvasNode', () => {
|
|||||||
},
|
},
|
||||||
global: {
|
global: {
|
||||||
stubs: {
|
stubs: {
|
||||||
HandleRenderer: true,
|
CanvasHandleRenderer: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, provide, toRef, watch } from 'vue';
|
import { computed, provide, toRef, watch } from 'vue';
|
||||||
import type { CanvasNodeData, CanvasConnectionPort, CanvasElementPortWithPosition } from '@/types';
|
import type {
|
||||||
|
CanvasNodeData,
|
||||||
|
CanvasConnectionPort,
|
||||||
|
CanvasElementPortWithRenderData,
|
||||||
|
} from '@/types';
|
||||||
|
import { CanvasConnectionMode } from '@/types';
|
||||||
import NodeIcon from '@/components/NodeIcon.vue';
|
import NodeIcon from '@/components/NodeIcon.vue';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import CanvasNodeToolbar from '@/components/canvas/elements/nodes/CanvasNodeToolbar.vue';
|
import CanvasNodeToolbar from '@/components/canvas/elements/nodes/CanvasNodeToolbar.vue';
|
||||||
import CanvasNodeRenderer from '@/components/canvas/elements/nodes/CanvasNodeRenderer.vue';
|
import CanvasNodeRenderer from '@/components/canvas/elements/nodes/CanvasNodeRenderer.vue';
|
||||||
import HandleRenderer from '@/components/canvas/elements/handles/HandleRenderer.vue';
|
import CanvasHandleRenderer from '@/components/canvas/elements/handles/CanvasHandleRenderer.vue';
|
||||||
import { useNodeConnections } from '@/composables/useNodeConnections';
|
import { useNodeConnections } from '@/composables/useNodeConnections';
|
||||||
import { CanvasNodeKey } from '@/constants';
|
import { CanvasNodeKey } from '@/constants';
|
||||||
import { useContextMenu } from '@/composables/useContextMenu';
|
import { useContextMenu } from '@/composables/useContextMenu';
|
||||||
@@ -13,6 +18,7 @@ import { Position } from '@vue-flow/core';
|
|||||||
import type { XYPosition, NodeProps } from '@vue-flow/core';
|
import type { XYPosition, NodeProps } from '@vue-flow/core';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
add: [id: string, handle: string];
|
||||||
delete: [id: string];
|
delete: [id: string];
|
||||||
run: [id: string];
|
run: [id: string];
|
||||||
select: [id: string, selected: boolean];
|
select: [id: string, selected: boolean];
|
||||||
@@ -39,7 +45,7 @@ const { mainInputs, nonMainInputs, mainOutputs, nonMainOutputs, isValidConnectio
|
|||||||
|
|
||||||
const isDisabled = computed(() => props.data.disabled);
|
const isDisabled = computed(() => props.data.disabled);
|
||||||
|
|
||||||
const nodeType = computed(() => {
|
const nodeTypeDescription = computed(() => {
|
||||||
return nodeTypesStore.getNodeType(props.data.type, props.data.typeVersion);
|
return nodeTypesStore.getNodeType(props.data.type, props.data.typeVersion);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,10 +53,22 @@ const nodeType = computed(() => {
|
|||||||
* Inputs
|
* Inputs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const inputsWithPosition = computed(() => {
|
const mappedInputs = computed(() => {
|
||||||
return [
|
return [
|
||||||
...mainInputs.value.map(mapEndpointWithPosition(Position.Left, 'top')),
|
...mainInputs.value.map(
|
||||||
...nonMainInputs.value.map(mapEndpointWithPosition(Position.Bottom, 'left')),
|
createEndpointMappingFn({
|
||||||
|
mode: CanvasConnectionMode.Input,
|
||||||
|
position: Position.Left,
|
||||||
|
offsetAxis: 'top',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
...nonMainInputs.value.map(
|
||||||
|
createEndpointMappingFn({
|
||||||
|
mode: CanvasConnectionMode.Input,
|
||||||
|
position: Position.Bottom,
|
||||||
|
offsetAxis: 'left',
|
||||||
|
}),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,10 +76,22 @@ const inputsWithPosition = computed(() => {
|
|||||||
* Outputs
|
* Outputs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const outputsWithPosition = computed(() => {
|
const mappedOutputs = computed(() => {
|
||||||
return [
|
return [
|
||||||
...mainOutputs.value.map(mapEndpointWithPosition(Position.Right, 'top')),
|
...mainOutputs.value.map(
|
||||||
...nonMainOutputs.value.map(mapEndpointWithPosition(Position.Top, 'left')),
|
createEndpointMappingFn({
|
||||||
|
mode: CanvasConnectionMode.Output,
|
||||||
|
position: Position.Right,
|
||||||
|
offsetAxis: 'top',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
...nonMainOutputs.value.map(
|
||||||
|
createEndpointMappingFn({
|
||||||
|
mode: CanvasConnectionMode.Output,
|
||||||
|
position: Position.Top,
|
||||||
|
offsetAxis: 'left',
|
||||||
|
}),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -77,15 +107,24 @@ const nodeIconSize = computed(() =>
|
|||||||
* Endpoints
|
* Endpoints
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const mapEndpointWithPosition =
|
const createEndpointMappingFn =
|
||||||
(position: Position, offsetAxis: 'top' | 'left') =>
|
({
|
||||||
|
mode,
|
||||||
|
position,
|
||||||
|
offsetAxis,
|
||||||
|
}: {
|
||||||
|
mode: CanvasConnectionMode;
|
||||||
|
position: Position;
|
||||||
|
offsetAxis: 'top' | 'left';
|
||||||
|
}) =>
|
||||||
(
|
(
|
||||||
endpoint: CanvasConnectionPort,
|
endpoint: CanvasConnectionPort,
|
||||||
index: number,
|
index: number,
|
||||||
endpoints: CanvasConnectionPort[],
|
endpoints: CanvasConnectionPort[],
|
||||||
): CanvasElementPortWithPosition => {
|
): CanvasElementPortWithRenderData => {
|
||||||
return {
|
return {
|
||||||
...endpoint,
|
...endpoint,
|
||||||
|
connected: !!connections.value[mode][endpoint.type]?.[endpoint.index]?.length,
|
||||||
position,
|
position,
|
||||||
offset: {
|
offset: {
|
||||||
[offsetAxis]: `${(100 / (endpoints.length + 1)) * (index + 1)}%`,
|
[offsetAxis]: `${(100 / (endpoints.length + 1)) * (index + 1)}%`,
|
||||||
@@ -97,6 +136,10 @@ const mapEndpointWithPosition =
|
|||||||
* Events
|
* Events
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
function onAdd(handle: string) {
|
||||||
|
emit('add', props.id, handle);
|
||||||
|
}
|
||||||
|
|
||||||
function onDelete() {
|
function onDelete() {
|
||||||
emit('delete', props.id);
|
emit('delete', props.id);
|
||||||
}
|
}
|
||||||
@@ -142,7 +185,6 @@ provide(CanvasNodeKey, {
|
|||||||
data,
|
data,
|
||||||
label,
|
label,
|
||||||
selected,
|
selected,
|
||||||
nodeType,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const showToolbar = computed(() => {
|
const showToolbar = computed(() => {
|
||||||
@@ -156,8 +198,8 @@ const showToolbar = computed(() => {
|
|||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.selected,
|
() => props.selected,
|
||||||
(selected) => {
|
(value) => {
|
||||||
emit('select', props.id, selected);
|
emit('select', props.id, value);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
@@ -167,34 +209,44 @@ watch(
|
|||||||
:class="[$style.canvasNode, { [$style.showToolbar]: showToolbar }]"
|
:class="[$style.canvasNode, { [$style.showToolbar]: showToolbar }]"
|
||||||
data-test-id="canvas-node"
|
data-test-id="canvas-node"
|
||||||
>
|
>
|
||||||
<template v-for="source in outputsWithPosition" :key="`${source.type}/${source.index}`">
|
<template
|
||||||
<HandleRenderer
|
v-for="source in mappedOutputs"
|
||||||
mode="output"
|
:key="`${CanvasConnectionMode.Output}/${source.type}/${source.index}`"
|
||||||
|
>
|
||||||
|
<CanvasHandleRenderer
|
||||||
data-test-id="canvas-node-output-handle"
|
data-test-id="canvas-node-output-handle"
|
||||||
|
:connected="source.connected"
|
||||||
|
:mode="CanvasConnectionMode.Output"
|
||||||
:type="source.type"
|
:type="source.type"
|
||||||
:label="source.label"
|
:label="source.label"
|
||||||
:index="source.index"
|
:index="source.index"
|
||||||
:position="source.position"
|
:position="source.position"
|
||||||
:offset="source.offset"
|
:offset="source.offset"
|
||||||
:is-valid-connection="isValidConnection"
|
:is-valid-connection="isValidConnection"
|
||||||
|
@add="onAdd"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-for="target in inputsWithPosition" :key="`${target.type}/${target.index}`">
|
<template
|
||||||
<HandleRenderer
|
v-for="target in mappedInputs"
|
||||||
mode="input"
|
:key="`${CanvasConnectionMode.Input}/${target.type}/${target.index}`"
|
||||||
|
>
|
||||||
|
<CanvasHandleRenderer
|
||||||
data-test-id="canvas-node-input-handle"
|
data-test-id="canvas-node-input-handle"
|
||||||
|
:connected="!!connections[CanvasConnectionMode.Input][target.type]?.[target.index]?.length"
|
||||||
|
:mode="CanvasConnectionMode.Input"
|
||||||
:type="target.type"
|
:type="target.type"
|
||||||
:label="target.label"
|
:label="target.label"
|
||||||
:index="target.index"
|
:index="target.index"
|
||||||
:position="target.position"
|
:position="target.position"
|
||||||
:offset="target.offset"
|
:offset="target.offset"
|
||||||
:is-valid-connection="isValidConnection"
|
:is-valid-connection="isValidConnection"
|
||||||
|
@add="onAdd"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<CanvasNodeToolbar
|
<CanvasNodeToolbar
|
||||||
v-if="nodeType"
|
v-if="nodeTypeDescription"
|
||||||
data-test-id="canvas-node-toolbar"
|
data-test-id="canvas-node-toolbar"
|
||||||
:class="$style.canvasNodeToolbar"
|
:class="$style.canvasNodeToolbar"
|
||||||
@delete="onDelete"
|
@delete="onDelete"
|
||||||
@@ -210,8 +262,8 @@ watch(
|
|||||||
@open:contextmenu="onOpenContextMenuFromNode"
|
@open:contextmenu="onOpenContextMenuFromNode"
|
||||||
>
|
>
|
||||||
<NodeIcon
|
<NodeIcon
|
||||||
v-if="nodeType"
|
v-if="nodeTypeDescription"
|
||||||
:node-type="nodeType"
|
:node-type="nodeTypeDescription"
|
||||||
:size="nodeIconSize"
|
:size="nodeIconSize"
|
||||||
:shrink="false"
|
:shrink="false"
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import CanvasNodeDisabledStrikeThrough from '@/components/canvas/elements/nodes/
|
|||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
import { createCanvasNodeProvide } from '@/__tests__/data';
|
import { createCanvasNodeProvide } from '@/__tests__/data';
|
||||||
|
import { CanvasConnectionMode } from '@/types';
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(CanvasNodeDisabledStrikeThrough);
|
const renderComponent = createComponentRenderer(CanvasNodeDisabledStrikeThrough);
|
||||||
|
|
||||||
@@ -13,12 +14,12 @@ describe('CanvasNodeDisabledStrikeThrough', () => {
|
|||||||
...createCanvasNodeProvide({
|
...createCanvasNodeProvide({
|
||||||
data: {
|
data: {
|
||||||
connections: {
|
connections: {
|
||||||
input: {
|
[CanvasConnectionMode.Input]: {
|
||||||
[NodeConnectionType.Main]: [
|
[NodeConnectionType.Main]: [
|
||||||
[{ node: 'node', type: NodeConnectionType.Main, index: 0 }],
|
[{ node: 'node', type: NodeConnectionType.Main, index: 0 }],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
output: {
|
[CanvasConnectionMode.Output]: {
|
||||||
[NodeConnectionType.Main]: [
|
[NodeConnectionType.Main]: [
|
||||||
[{ node: 'node', type: NodeConnectionType.Main, index: 0 }],
|
[{ node: 'node', type: NodeConnectionType.Main, index: 0 }],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import { CanvasConnectionMode } from '@/types';
|
|||||||
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
|
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
|
||||||
|
|
||||||
describe('useNodeConnections', () => {
|
describe('useNodeConnections', () => {
|
||||||
const defaultConnections = { input: {}, output: {} };
|
const defaultConnections = {
|
||||||
|
[CanvasConnectionMode.Input]: {},
|
||||||
|
[CanvasConnectionMode.Output]: {},
|
||||||
|
};
|
||||||
describe('mainInputs', () => {
|
describe('mainInputs', () => {
|
||||||
it('should return main inputs when provided with main inputs', () => {
|
it('should return main inputs when provided with main inputs', () => {
|
||||||
const inputs = ref<CanvasNodeData['inputs']>([
|
const inputs = ref<CanvasNodeData['inputs']>([
|
||||||
@@ -73,13 +76,13 @@ describe('useNodeConnections', () => {
|
|||||||
const inputs = ref<CanvasNodeData['inputs']>([]);
|
const inputs = ref<CanvasNodeData['inputs']>([]);
|
||||||
const outputs = ref<CanvasNodeData['outputs']>([]);
|
const outputs = ref<CanvasNodeData['outputs']>([]);
|
||||||
const connections = ref<CanvasNodeData['connections']>({
|
const connections = ref<CanvasNodeData['connections']>({
|
||||||
input: {
|
[CanvasConnectionMode.Input]: {
|
||||||
[NodeConnectionType.Main]: [
|
[NodeConnectionType.Main]: [
|
||||||
[{ node: 'node1', type: NodeConnectionType.Main, index: 0 }],
|
[{ node: 'node1', type: NodeConnectionType.Main, index: 0 }],
|
||||||
[{ node: 'node2', type: NodeConnectionType.Main, index: 0 }],
|
[{ node: 'node2', type: NodeConnectionType.Main, index: 0 }],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
output: {},
|
[CanvasConnectionMode.Output]: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mainInputConnections } = useNodeConnections({
|
const { mainInputConnections } = useNodeConnections({
|
||||||
@@ -89,7 +92,9 @@ describe('useNodeConnections', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(mainInputConnections.value.length).toBe(2);
|
expect(mainInputConnections.value.length).toBe(2);
|
||||||
expect(mainInputConnections.value).toEqual(connections.value.input[NodeConnectionType.Main]);
|
expect(mainInputConnections.value).toEqual(
|
||||||
|
connections.value[CanvasConnectionMode.Input][NodeConnectionType.Main],
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -139,8 +144,8 @@ describe('useNodeConnections', () => {
|
|||||||
const inputs = ref<CanvasNodeData['inputs']>([]);
|
const inputs = ref<CanvasNodeData['inputs']>([]);
|
||||||
const outputs = ref<CanvasNodeData['outputs']>([]);
|
const outputs = ref<CanvasNodeData['outputs']>([]);
|
||||||
const connections = ref<CanvasNodeData['connections']>({
|
const connections = ref<CanvasNodeData['connections']>({
|
||||||
input: {},
|
[CanvasConnectionMode.Input]: {},
|
||||||
output: {
|
[CanvasConnectionMode.Output]: {
|
||||||
[NodeConnectionType.Main]: [
|
[NodeConnectionType.Main]: [
|
||||||
[{ node: 'node1', type: NodeConnectionType.Main, index: 0 }],
|
[{ node: 'node1', type: NodeConnectionType.Main, index: 0 }],
|
||||||
[{ node: 'node2', type: NodeConnectionType.Main, index: 0 }],
|
[{ node: 'node2', type: NodeConnectionType.Main, index: 0 }],
|
||||||
@@ -156,7 +161,7 @@ describe('useNodeConnections', () => {
|
|||||||
|
|
||||||
expect(mainOutputConnections.value.length).toBe(2);
|
expect(mainOutputConnections.value.length).toBe(2);
|
||||||
expect(mainOutputConnections.value).toEqual(
|
expect(mainOutputConnections.value).toEqual(
|
||||||
connections.value.output[NodeConnectionType.Main],
|
connections.value[CanvasConnectionMode.Output][NodeConnectionType.Main],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -122,11 +122,11 @@ describe('useCanvasMapping', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
connections: {
|
connections: {
|
||||||
input: {},
|
[CanvasConnectionMode.Input]: {},
|
||||||
output: {},
|
[CanvasConnectionMode.Output]: {},
|
||||||
},
|
},
|
||||||
render: {
|
render: {
|
||||||
type: 'default',
|
type: CanvasNodeRenderType.Default,
|
||||||
options: {
|
options: {
|
||||||
configurable: false,
|
configurable: false,
|
||||||
configuration: false,
|
configuration: false,
|
||||||
@@ -205,10 +205,14 @@ describe('useCanvasMapping', () => {
|
|||||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mappedNodes.value[0]?.data?.connections.output).toHaveProperty(
|
expect(mappedNodes.value[0]?.data?.connections[CanvasConnectionMode.Output]).toHaveProperty(
|
||||||
NodeConnectionType.Main,
|
NodeConnectionType.Main,
|
||||||
);
|
);
|
||||||
expect(mappedNodes.value[0]?.data?.connections.output[NodeConnectionType.Main][0][0]).toEqual(
|
expect(
|
||||||
|
mappedNodes.value[0]?.data?.connections[CanvasConnectionMode.Output][
|
||||||
|
NodeConnectionType.Main
|
||||||
|
][0][0],
|
||||||
|
).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
node: setNode.name,
|
node: setNode.name,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
@@ -216,8 +220,14 @@ describe('useCanvasMapping', () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mappedNodes.value[1]?.data?.connections.input).toHaveProperty(NodeConnectionType.Main);
|
expect(mappedNodes.value[1]?.data?.connections[CanvasConnectionMode.Input]).toHaveProperty(
|
||||||
expect(mappedNodes.value[1]?.data?.connections.input[NodeConnectionType.Main][0][0]).toEqual(
|
NodeConnectionType.Main,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
mappedNodes.value[1]?.data?.connections[CanvasConnectionMode.Input][
|
||||||
|
NodeConnectionType.Main
|
||||||
|
][0][0],
|
||||||
|
).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
node: manualTriggerNode.name,
|
node: manualTriggerNode.name,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import type {
|
|||||||
CanvasNodeDefaultRender,
|
CanvasNodeDefaultRender,
|
||||||
CanvasNodeStickyNoteRender,
|
CanvasNodeStickyNoteRender,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
import { CanvasNodeRenderType } from '@/types';
|
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
|
||||||
import {
|
import {
|
||||||
mapLegacyConnectionsToCanvasConnections,
|
mapLegacyConnectionsToCanvasConnections,
|
||||||
mapLegacyEndpointsToCanvasConnectionPort,
|
mapLegacyEndpointsToCanvasConnectionPort,
|
||||||
@@ -263,8 +263,8 @@ export function useCanvasMapping({
|
|||||||
inputs: nodeInputsById.value[node.id] ?? [],
|
inputs: nodeInputsById.value[node.id] ?? [],
|
||||||
outputs: nodeOutputsById.value[node.id] ?? [],
|
outputs: nodeOutputsById.value[node.id] ?? [],
|
||||||
connections: {
|
connections: {
|
||||||
input: inputConnections,
|
[CanvasConnectionMode.Input]: inputConnections,
|
||||||
output: outputConnections,
|
[CanvasConnectionMode.Output]: outputConnections,
|
||||||
},
|
},
|
||||||
issues: {
|
issues: {
|
||||||
items: nodeIssuesById.value[node.id],
|
items: nodeIssuesById.value[node.id],
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||||
import { inject, ref } from 'vue';
|
import { inject, ref } from 'vue';
|
||||||
import type { CanvasNodeInjectionData } from '../types';
|
import type { CanvasNodeData, CanvasNodeInjectionData } from '../types';
|
||||||
import { CanvasNodeRenderType } from '../types';
|
import { CanvasConnectionMode, CanvasNodeRenderType } from '../types';
|
||||||
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
|
||||||
vi.mock('vue', async () => {
|
vi.mock('vue', async () => {
|
||||||
const actual = await vi.importActual('vue');
|
const actual = await vi.importActual('vue');
|
||||||
@@ -18,7 +19,10 @@ describe('useCanvasNode', () => {
|
|||||||
expect(result.label.value).toBe('');
|
expect(result.label.value).toBe('');
|
||||||
expect(result.inputs.value).toEqual([]);
|
expect(result.inputs.value).toEqual([]);
|
||||||
expect(result.outputs.value).toEqual([]);
|
expect(result.outputs.value).toEqual([]);
|
||||||
expect(result.connections.value).toEqual({ input: {}, output: {} });
|
expect(result.connections.value).toEqual({
|
||||||
|
[CanvasConnectionMode.Input]: {},
|
||||||
|
[CanvasConnectionMode.Output]: {},
|
||||||
|
});
|
||||||
expect(result.isDisabled.value).toBe(false);
|
expect(result.isDisabled.value).toBe(false);
|
||||||
expect(result.isSelected.value).toBeUndefined();
|
expect(result.isSelected.value).toBeUndefined();
|
||||||
expect(result.pinnedDataCount.value).toBe(0);
|
expect(result.pinnedDataCount.value).toBe(0);
|
||||||
@@ -41,9 +45,12 @@ describe('useCanvasNode', () => {
|
|||||||
type: 'nodeType1',
|
type: 'nodeType1',
|
||||||
typeVersion: 1,
|
typeVersion: 1,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
inputs: [{ type: 'main', index: 0 }],
|
inputs: [{ type: NodeConnectionType.Main, index: 0 }],
|
||||||
outputs: [{ type: 'main', index: 0 }],
|
outputs: [{ type: NodeConnectionType.Main, index: 0 }],
|
||||||
connections: { input: { '0': [] }, output: {} },
|
connections: {
|
||||||
|
[CanvasConnectionMode.Input]: { '0': [] },
|
||||||
|
[CanvasConnectionMode.Output]: {},
|
||||||
|
},
|
||||||
issues: { items: ['issue1'], visible: true },
|
issues: { items: ['issue1'], visible: true },
|
||||||
execution: { status: 'running', waiting: 'waiting', running: true },
|
execution: { status: 'running', waiting: 'waiting', running: true },
|
||||||
runData: { count: 1, visible: true },
|
runData: { count: 1, visible: true },
|
||||||
@@ -56,7 +63,7 @@ describe('useCanvasNode', () => {
|
|||||||
trigger: false,
|
trigger: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
} satisfies CanvasNodeData),
|
||||||
id: ref('1'),
|
id: ref('1'),
|
||||||
label: ref('Node 1'),
|
label: ref('Node 1'),
|
||||||
selected: ref(true),
|
selected: ref(true),
|
||||||
@@ -68,9 +75,12 @@ describe('useCanvasNode', () => {
|
|||||||
|
|
||||||
expect(result.label.value).toBe('Node 1');
|
expect(result.label.value).toBe('Node 1');
|
||||||
expect(result.name.value).toBe('Node 1');
|
expect(result.name.value).toBe('Node 1');
|
||||||
expect(result.inputs.value).toEqual([{ type: 'main', index: 0 }]);
|
expect(result.inputs.value).toEqual([{ type: NodeConnectionType.Main, index: 0 }]);
|
||||||
expect(result.outputs.value).toEqual([{ type: 'main', index: 0 }]);
|
expect(result.outputs.value).toEqual([{ type: NodeConnectionType.Main, index: 0 }]);
|
||||||
expect(result.connections.value).toEqual({ input: { '0': [] }, output: {} });
|
expect(result.connections.value).toEqual({
|
||||||
|
[CanvasConnectionMode.Input]: { '0': [] },
|
||||||
|
[CanvasConnectionMode.Output]: {},
|
||||||
|
});
|
||||||
expect(result.isDisabled.value).toBe(true);
|
expect(result.isDisabled.value).toBe(true);
|
||||||
expect(result.isSelected.value).toBe(true);
|
expect(result.isSelected.value).toBe(true);
|
||||||
expect(result.pinnedDataCount.value).toBe(1);
|
expect(result.pinnedDataCount.value).toBe(1);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { CanvasNodeKey } from '@/constants';
|
import { CanvasNodeKey } from '@/constants';
|
||||||
import { computed, inject } from 'vue';
|
import { computed, inject } from 'vue';
|
||||||
import type { CanvasNodeData } from '@/types';
|
import type { CanvasNodeData } from '@/types';
|
||||||
import { CanvasNodeRenderType } from '@/types';
|
import { CanvasNodeRenderType, CanvasConnectionMode } from '@/types';
|
||||||
|
|
||||||
export function useCanvasNode() {
|
export function useCanvasNode() {
|
||||||
const node = inject(CanvasNodeKey);
|
const node = inject(CanvasNodeKey);
|
||||||
@@ -20,7 +20,7 @@ export function useCanvasNode() {
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
inputs: [],
|
inputs: [],
|
||||||
outputs: [],
|
outputs: [],
|
||||||
connections: { input: {}, output: {} },
|
connections: { [CanvasConnectionMode.Input]: {}, [CanvasConnectionMode.Output]: {} },
|
||||||
issues: { items: [], visible: false },
|
issues: { items: [], visible: false },
|
||||||
pinnedData: { count: 0, visible: false },
|
pinnedData: { count: 0, visible: false },
|
||||||
execution: {
|
execution: {
|
||||||
|
|||||||
25
packages/editor-ui/src/composables/useCanvasNodeHandle.ts
Normal file
25
packages/editor-ui/src/composables/useCanvasNodeHandle.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Canvas V2 Only
|
||||||
|
* @TODO Remove this notice when Canvas V2 is the only one in use
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CanvasNodeHandleKey } from '@/constants';
|
||||||
|
import { computed, inject } from 'vue';
|
||||||
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
import { CanvasConnectionMode } from '@/types';
|
||||||
|
|
||||||
|
export function useCanvasNodeHandle() {
|
||||||
|
const handle = inject(CanvasNodeHandleKey);
|
||||||
|
|
||||||
|
const label = computed(() => handle?.label.value ?? '');
|
||||||
|
const connected = computed(() => handle?.connected.value ?? false);
|
||||||
|
const type = computed(() => handle?.type.value ?? NodeConnectionType.Main);
|
||||||
|
const mode = computed(() => handle?.mode.value ?? CanvasConnectionMode.Input);
|
||||||
|
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
connected,
|
||||||
|
type,
|
||||||
|
mode,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { CanvasNodeData } from '@/types';
|
import type { CanvasNodeData } from '@/types';
|
||||||
|
import { CanvasConnectionMode } from '@/types';
|
||||||
import type { MaybeRef } from 'vue';
|
import type { MaybeRef } from 'vue';
|
||||||
import { computed, unref } from 'vue';
|
import { computed, unref } from 'vue';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
@@ -31,7 +32,7 @@ export function useNodeConnections({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const mainInputConnections = computed(
|
const mainInputConnections = computed(
|
||||||
() => unref(connections).input[NodeConnectionType.Main] ?? [],
|
() => unref(connections)[CanvasConnectionMode.Input][NodeConnectionType.Main] ?? [],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,7 +48,7 @@ export function useNodeConnections({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const mainOutputConnections = computed(
|
const mainOutputConnections = computed(
|
||||||
() => unref(connections).output[NodeConnectionType.Main] ?? [],
|
() => unref(connections)[CanvasConnectionMode.Output][NodeConnectionType.Main] ?? [],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
|
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
|
||||||
import type {
|
import type {
|
||||||
ConnectionTypes,
|
|
||||||
ExecutionStatus,
|
ExecutionStatus,
|
||||||
IConnection,
|
|
||||||
INodeConnections,
|
INodeConnections,
|
||||||
INodeTypeDescription,
|
IConnection,
|
||||||
|
NodeConnectionType,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import type { DefaultEdge, Node, NodeProps, Position } from '@vue-flow/core';
|
import type { DefaultEdge, Node, NodeProps, Position } from '@vue-flow/core';
|
||||||
import type { INodeUi } from '@/Interface';
|
import type { INodeUi } from '@/Interface';
|
||||||
import type { ComputedRef, Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import type { PartialBy } from '@/utils/typeHelpers';
|
import type { PartialBy } from '@/utils/typeHelpers';
|
||||||
|
|
||||||
export type CanvasConnectionPortType = ConnectionTypes;
|
export type CanvasConnectionPortType = NodeConnectionType;
|
||||||
|
|
||||||
export const enum CanvasConnectionMode {
|
export const enum CanvasConnectionMode {
|
||||||
Input = 'inputs',
|
Input = 'inputs',
|
||||||
@@ -30,7 +29,8 @@ export type CanvasConnectionPort = {
|
|||||||
label?: string;
|
label?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface CanvasElementPortWithPosition extends CanvasConnectionPort {
|
export interface CanvasElementPortWithRenderData extends CanvasConnectionPort {
|
||||||
|
connected: boolean;
|
||||||
position: Position;
|
position: Position;
|
||||||
offset?: { top?: string; left?: string };
|
offset?: { top?: string; left?: string };
|
||||||
}
|
}
|
||||||
@@ -74,8 +74,8 @@ export interface CanvasNodeData {
|
|||||||
inputs: CanvasConnectionPort[];
|
inputs: CanvasConnectionPort[];
|
||||||
outputs: CanvasConnectionPort[];
|
outputs: CanvasConnectionPort[];
|
||||||
connections: {
|
connections: {
|
||||||
input: INodeConnections;
|
[CanvasConnectionMode.Input]: INodeConnections;
|
||||||
output: INodeConnections;
|
[CanvasConnectionMode.Output]: INodeConnections;
|
||||||
};
|
};
|
||||||
issues: {
|
issues: {
|
||||||
items: string[];
|
items: string[];
|
||||||
@@ -122,11 +122,13 @@ export interface CanvasNodeInjectionData {
|
|||||||
data: Ref<CanvasNodeData>;
|
data: Ref<CanvasNodeData>;
|
||||||
label: Ref<NodeProps['label']>;
|
label: Ref<NodeProps['label']>;
|
||||||
selected: Ref<NodeProps['selected']>;
|
selected: Ref<NodeProps['selected']>;
|
||||||
nodeType: ComputedRef<INodeTypeDescription | null>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CanvasNodeHandleInjectionData {
|
export interface CanvasNodeHandleInjectionData {
|
||||||
label: Ref<string | undefined>;
|
label: Ref<string | undefined>;
|
||||||
|
mode: Ref<CanvasConnectionMode>;
|
||||||
|
type: Ref<NodeConnectionType>;
|
||||||
|
connected: Ref<boolean | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConnectStartEvent = { handleId: string; handleType: string; nodeId: string };
|
export type ConnectStartEvent = { handleId: string; handleType: string; nodeId: string };
|
||||||
|
|||||||
@@ -148,7 +148,8 @@ export function mapLegacyEndpointsToCanvasConnectionPort(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return endpoints.map((endpoint, endpointIndex) => {
|
return endpoints.map((endpoint, endpointIndex) => {
|
||||||
const type = typeof endpoint === 'string' ? endpoint : endpoint.type;
|
const typeValue = typeof endpoint === 'string' ? endpoint : endpoint.type;
|
||||||
|
const type = isValidNodeConnectionType(typeValue) ? typeValue : NodeConnectionType.Main;
|
||||||
const label = typeof endpoint === 'string' ? undefined : endpoint.displayName;
|
const label = typeof endpoint === 'string' ? undefined : endpoint.displayName;
|
||||||
const index =
|
const index =
|
||||||
endpoints
|
endpoints
|
||||||
|
|||||||
@@ -560,6 +560,16 @@ function onUpdateNodeParameters(id: string, parameters: Record<string, unknown>)
|
|||||||
setNodeParameters(id, parameters);
|
setNodeParameters(id, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onClickNodeAdd(source: string, sourceHandle: string) {
|
||||||
|
nodeCreatorStore.openNodeCreatorForConnectingNode({
|
||||||
|
connection: {
|
||||||
|
source,
|
||||||
|
sourceHandle,
|
||||||
|
},
|
||||||
|
eventSource: NODE_CREATOR_OPEN_SOURCES.PLUS_ENDPOINT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Credentials
|
* Credentials
|
||||||
*/
|
*/
|
||||||
@@ -1248,6 +1258,7 @@ onBeforeUnmount(() => {
|
|||||||
@update:node:enabled="onToggleNodeDisabled"
|
@update:node:enabled="onToggleNodeDisabled"
|
||||||
@update:node:name="onOpenRenameNodeModal"
|
@update:node:name="onOpenRenameNodeModal"
|
||||||
@update:node:parameters="onUpdateNodeParameters"
|
@update:node:parameters="onUpdateNodeParameters"
|
||||||
|
@click:node:add="onClickNodeAdd"
|
||||||
@run:node="onRunWorkflowToNode"
|
@run:node="onRunWorkflowToNode"
|
||||||
@delete:node="onDeleteNode"
|
@delete:node="onDeleteNode"
|
||||||
@create:connection="onCreateConnection"
|
@create:connection="onCreateConnection"
|
||||||
|
|||||||
Reference in New Issue
Block a user