mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-31 15:37:26 -08:00
feat(editor): Improve node and edge bring-to-front mechanism on new canvas (#11793)
This commit is contained in:
parent
4dde287cde
commit
b89ca9d482
|
@ -154,6 +154,9 @@ export function createCanvasHandleProvide({
|
||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
isRequired?: boolean;
|
isRequired?: boolean;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
|
const maxConnections = [NodeConnectionType.Main, NodeConnectionType.AiTool].includes(type)
|
||||||
|
? Infinity
|
||||||
|
: 1;
|
||||||
return {
|
return {
|
||||||
[String(CanvasNodeHandleKey)]: {
|
[String(CanvasNodeHandleKey)]: {
|
||||||
label: ref(label),
|
label: ref(label),
|
||||||
|
@ -164,6 +167,7 @@ export function createCanvasHandleProvide({
|
||||||
isConnecting: ref(isConnecting),
|
isConnecting: ref(isConnecting),
|
||||||
isReadOnly: ref(isReadOnly),
|
isReadOnly: ref(isReadOnly),
|
||||||
isRequired: ref(isRequired),
|
isRequired: ref(isRequired),
|
||||||
|
maxConnections: ref(maxConnections),
|
||||||
runData: ref(runData),
|
runData: ref(runData),
|
||||||
} satisfies CanvasNodeHandleInjectionData,
|
} satisfies CanvasNodeHandleInjectionData,
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,10 +29,11 @@ import type { PinDataSource } from '@/composables/usePinnedData';
|
||||||
import { isPresent } from '@/utils/typesUtils';
|
import { isPresent } from '@/utils/typesUtils';
|
||||||
import { GRID_SIZE } from '@/utils/nodeViewUtils';
|
import { GRID_SIZE } from '@/utils/nodeViewUtils';
|
||||||
import { CanvasKey } from '@/constants';
|
import { CanvasKey } from '@/constants';
|
||||||
import { onKeyDown, onKeyUp } from '@vueuse/core';
|
import { onKeyDown, onKeyUp, useThrottleFn } from '@vueuse/core';
|
||||||
import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue';
|
import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue';
|
||||||
import CanvasBackgroundStripedPattern from './elements/CanvasBackgroundStripedPattern.vue';
|
import CanvasBackgroundStripedPattern from './elements/CanvasBackgroundStripedPattern.vue';
|
||||||
import { useCanvasTraversal } from '@/composables/useCanvasTraversal';
|
import { useCanvasTraversal } from '@/composables/useCanvasTraversal';
|
||||||
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
|
||||||
|
@ -115,6 +116,11 @@ const {
|
||||||
onPaneReady,
|
onPaneReady,
|
||||||
findNode,
|
findNode,
|
||||||
viewport,
|
viewport,
|
||||||
|
onEdgeMouseLeave,
|
||||||
|
onEdgeMouseEnter,
|
||||||
|
onEdgeMouseMove,
|
||||||
|
onNodeMouseEnter,
|
||||||
|
onNodeMouseLeave,
|
||||||
} = vueFlow;
|
} = vueFlow;
|
||||||
const {
|
const {
|
||||||
getIncomingNodes,
|
getIncomingNodes,
|
||||||
|
@ -297,7 +303,7 @@ function onUpdateNodeParameters(id: string, parameters: Record<string, unknown>)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connections
|
* Connections / Edges
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const connectionCreated = ref(false);
|
const connectionCreated = ref(false);
|
||||||
|
@ -339,6 +345,55 @@ function onClickConnectionAdd(connection: Connection) {
|
||||||
|
|
||||||
const arrowHeadMarkerId = ref('custom-arrow-head');
|
const arrowHeadMarkerId = ref('custom-arrow-head');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edge and Nodes Hovering
|
||||||
|
*/
|
||||||
|
|
||||||
|
const edgesHoveredById = ref<Record<string, boolean>>({});
|
||||||
|
const edgesBringToFrontById = ref<Record<string, boolean>>({});
|
||||||
|
const nodesHoveredById = ref<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
onEdgeMouseEnter(({ edge }) => {
|
||||||
|
edgesBringToFrontById.value = { [edge.id]: true };
|
||||||
|
edgesHoveredById.value = { [edge.id]: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
onEdgeMouseMove(
|
||||||
|
useThrottleFn(({ edge, event }) => {
|
||||||
|
const type = edge.data.source.type;
|
||||||
|
if (type !== NodeConnectionType.AiTool) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!edge.data.maxConnections || edge.data.maxConnections > 1) {
|
||||||
|
const projectedPosition = getProjectedPosition(event);
|
||||||
|
const yDiff = projectedPosition.y - edge.targetY;
|
||||||
|
if (yDiff < 4 * GRID_SIZE) {
|
||||||
|
edgesBringToFrontById.value = { [edge.id]: false };
|
||||||
|
} else {
|
||||||
|
edgesBringToFrontById.value = { [edge.id]: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100),
|
||||||
|
);
|
||||||
|
|
||||||
|
onEdgeMouseLeave(({ edge }) => {
|
||||||
|
edgesBringToFrontById.value = { [edge.id]: false };
|
||||||
|
edgesHoveredById.value = { [edge.id]: false };
|
||||||
|
});
|
||||||
|
|
||||||
|
onNodeMouseEnter(({ node }) => {
|
||||||
|
nodesHoveredById.value = { [node.id]: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
onNodeMouseLeave(({ node }) => {
|
||||||
|
nodesHoveredById.value = { [node.id]: false };
|
||||||
|
});
|
||||||
|
|
||||||
|
function onUpdateEdgeHovered(id: string, hovered: boolean) {
|
||||||
|
edgesHoveredById.value[id] = hovered;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executions
|
* Executions
|
||||||
*/
|
*/
|
||||||
|
@ -375,7 +430,7 @@ const defaultZoom = 1;
|
||||||
const zoom = ref(defaultZoom);
|
const zoom = ref(defaultZoom);
|
||||||
const isPaneMoving = ref(false);
|
const isPaneMoving = ref(false);
|
||||||
|
|
||||||
function getProjectedPosition(event?: MouseEvent) {
|
function getProjectedPosition(event?: Pick<MouseEvent, 'clientX' | 'clientY'>) {
|
||||||
const bounds = viewportRef.value?.getBoundingClientRect() ?? { left: 0, top: 0 };
|
const bounds = viewportRef.value?.getBoundingClientRect() ?? { left: 0, top: 0 };
|
||||||
const offsetX = event?.clientX ?? 0;
|
const offsetX = event?.clientX ?? 0;
|
||||||
const offsetY = event?.clientY ?? 0;
|
const offsetY = event?.clientY ?? 0;
|
||||||
|
@ -590,11 +645,13 @@ provide(CanvasKey, {
|
||||||
@move-end="onPaneMoveEnd"
|
@move-end="onPaneMoveEnd"
|
||||||
@node-drag-stop="onNodeDragStop"
|
@node-drag-stop="onNodeDragStop"
|
||||||
>
|
>
|
||||||
<template #node-canvas-node="canvasNodeProps">
|
<template #node-canvas-node="nodeProps">
|
||||||
<Node
|
<Node
|
||||||
v-bind="canvasNodeProps"
|
v-bind="nodeProps"
|
||||||
:read-only="readOnly"
|
:read-only="readOnly"
|
||||||
:event-bus="eventBus"
|
:event-bus="eventBus"
|
||||||
|
:hovered="nodesHoveredById[nodeProps.id]"
|
||||||
|
:bring-to-front="nodesHoveredById[nodeProps.id]"
|
||||||
@delete="onDeleteNode"
|
@delete="onDeleteNode"
|
||||||
@run="onRunNode"
|
@run="onRunNode"
|
||||||
@select="onSelectNode"
|
@select="onSelectNode"
|
||||||
|
@ -607,13 +664,16 @@ provide(CanvasKey, {
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #edge-canvas-edge="canvasEdgeProps">
|
<template #edge-canvas-edge="edgeProps">
|
||||||
<Edge
|
<Edge
|
||||||
v-bind="canvasEdgeProps"
|
v-bind="edgeProps"
|
||||||
:marker-end="`url(#${arrowHeadMarkerId})`"
|
:marker-end="`url(#${arrowHeadMarkerId})`"
|
||||||
:read-only="readOnly"
|
:read-only="readOnly"
|
||||||
|
:hovered="edgesHoveredById[edgeProps.id]"
|
||||||
|
:bring-to-front="edgesBringToFrontById[edgeProps.id]"
|
||||||
@add="onClickConnectionAdd"
|
@add="onClickConnectionAdd"
|
||||||
@delete="onDeleteConnection"
|
@delete="onDeleteConnection"
|
||||||
|
@update:hovered="onUpdateEdgeHovered(edgeProps.id, $event)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,11 @@ beforeEach(() => {
|
||||||
|
|
||||||
describe('CanvasEdge', () => {
|
describe('CanvasEdge', () => {
|
||||||
it('should emit delete event when toolbar delete is clicked', async () => {
|
it('should emit delete event when toolbar delete is clicked', async () => {
|
||||||
const { emitted, getByTestId } = renderComponent();
|
const { emitted, getByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
hovered: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
await userEvent.hover(getByTestId('edge-label-wrapper'));
|
await userEvent.hover(getByTestId('edge-label-wrapper'));
|
||||||
const deleteButton = getByTestId('delete-connection-button');
|
const deleteButton = getByTestId('delete-connection-button');
|
||||||
|
|
||||||
|
@ -40,7 +44,11 @@ describe('CanvasEdge', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit add event when toolbar add is clicked', async () => {
|
it('should emit add event when toolbar add is clicked', async () => {
|
||||||
const { emitted, getByTestId } = renderComponent();
|
const { emitted, getByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
hovered: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
await userEvent.hover(getByTestId('edge-label-wrapper'));
|
await userEvent.hover(getByTestId('edge-label-wrapper'));
|
||||||
|
|
||||||
const addButton = getByTestId('add-connection-button');
|
const addButton = getByTestId('add-connection-button');
|
||||||
|
|
|
@ -3,40 +3,28 @@
|
||||||
import type { CanvasConnectionData } from '@/types';
|
import type { CanvasConnectionData } from '@/types';
|
||||||
import { isValidNodeConnectionType } from '@/utils/typeGuards';
|
import { isValidNodeConnectionType } from '@/utils/typeGuards';
|
||||||
import type { Connection, EdgeProps } from '@vue-flow/core';
|
import type { Connection, EdgeProps } from '@vue-flow/core';
|
||||||
import { useVueFlow, BaseEdge, EdgeLabelRenderer } from '@vue-flow/core';
|
import { BaseEdge, EdgeLabelRenderer } from '@vue-flow/core';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
import { computed, useCssModule, ref, toRef } from 'vue';
|
import { computed, useCssModule, toRef } from 'vue';
|
||||||
import CanvasEdgeToolbar from './CanvasEdgeToolbar.vue';
|
import CanvasEdgeToolbar from './CanvasEdgeToolbar.vue';
|
||||||
import { getCustomPath } from './utils/edgePath';
|
import { getCustomPath } from './utils/edgePath';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
add: [connection: Connection];
|
add: [connection: Connection];
|
||||||
delete: [connection: Connection];
|
delete: [connection: Connection];
|
||||||
|
'update:hovered': [hovered: boolean];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
export type CanvasEdgeProps = EdgeProps<CanvasConnectionData> & {
|
export type CanvasEdgeProps = EdgeProps<CanvasConnectionData> & {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
hovered?: boolean;
|
hovered?: boolean;
|
||||||
|
bringToFront?: boolean; // Determines if entire edges layer should be brought to front
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = defineProps<CanvasEdgeProps>();
|
const props = defineProps<CanvasEdgeProps>();
|
||||||
|
|
||||||
const data = toRef(props, 'data');
|
const data = toRef(props, 'data');
|
||||||
|
|
||||||
const { onEdgeMouseEnter, onEdgeMouseLeave } = useVueFlow();
|
|
||||||
|
|
||||||
const isHovered = ref(false);
|
|
||||||
|
|
||||||
onEdgeMouseEnter(({ edge }) => {
|
|
||||||
if (edge.id !== props.id) return;
|
|
||||||
isHovered.value = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
onEdgeMouseLeave(({ edge }) => {
|
|
||||||
if (edge.id !== props.id) return;
|
|
||||||
isHovered.value = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
|
||||||
const connectionType = computed(() =>
|
const connectionType = computed(() =>
|
||||||
|
@ -45,7 +33,7 @@ const connectionType = computed(() =>
|
||||||
: NodeConnectionType.Main,
|
: NodeConnectionType.Main,
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderToolbar = computed(() => isHovered.value && !props.readOnly);
|
const renderToolbar = computed(() => props.hovered && !props.readOnly);
|
||||||
|
|
||||||
const isMainConnection = computed(() => data.value.source.type === NodeConnectionType.Main);
|
const isMainConnection = computed(() => data.value.source.type === NodeConnectionType.Main);
|
||||||
|
|
||||||
|
@ -71,12 +59,13 @@ const edgeStyle = computed(() => ({
|
||||||
...props.style,
|
...props.style,
|
||||||
...(isMainConnection.value ? {} : { strokeDasharray: '8,8' }),
|
...(isMainConnection.value ? {} : { strokeDasharray: '8,8' }),
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
stroke: isHovered.value ? 'var(--color-primary)' : edgeColor.value,
|
stroke: props.hovered ? 'var(--color-primary)' : edgeColor.value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const edgeClasses = computed(() => ({
|
const edgeClasses = computed(() => ({
|
||||||
[$style.edge]: true,
|
[$style.edge]: true,
|
||||||
hovered: isHovered.value,
|
hovered: props.hovered,
|
||||||
|
'bring-to-front': props.bringToFront,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const edgeLabelStyle = computed(() => ({
|
const edgeLabelStyle = computed(() => ({
|
||||||
|
@ -87,7 +76,7 @@ const edgeToolbarStyle = computed(() => {
|
||||||
const [, labelX, labelY] = path.value;
|
const [, labelX, labelY] = path.value;
|
||||||
return {
|
return {
|
||||||
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||||
...(isHovered.value ? { zIndex: 1 } : {}),
|
...(props.hovered ? { zIndex: 1 } : {}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -117,6 +106,14 @@ function onAdd() {
|
||||||
function onDelete() {
|
function onDelete() {
|
||||||
emit('delete', connection.value);
|
emit('delete', connection.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onEdgeLabelMouseEnter() {
|
||||||
|
emit('update:hovered', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEdgeLabelMouseLeave() {
|
||||||
|
emit('update:hovered', false);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -137,8 +134,8 @@ function onDelete() {
|
||||||
:data-edge-status="status"
|
:data-edge-status="status"
|
||||||
:style="edgeToolbarStyle"
|
:style="edgeToolbarStyle"
|
||||||
:class="edgeToolbarClasses"
|
:class="edgeToolbarClasses"
|
||||||
@mouseenter="isHovered = true"
|
@mouseenter="onEdgeLabelMouseEnter"
|
||||||
@mouseleave="isHovered = false"
|
@mouseleave="onEdgeLabelMouseLeave"
|
||||||
>
|
>
|
||||||
<CanvasEdgeToolbar
|
<CanvasEdgeToolbar
|
||||||
v-if="renderToolbar"
|
v-if="renderToolbar"
|
||||||
|
|
|
@ -126,6 +126,7 @@ const mode = toRef(props, 'mode');
|
||||||
const type = toRef(props, 'type');
|
const type = toRef(props, 'type');
|
||||||
const index = toRef(props, 'index');
|
const index = toRef(props, 'index');
|
||||||
const isRequired = toRef(props, 'required');
|
const isRequired = toRef(props, 'required');
|
||||||
|
const maxConnections = toRef(props, 'maxConnections');
|
||||||
|
|
||||||
provide(CanvasNodeHandleKey, {
|
provide(CanvasNodeHandleKey, {
|
||||||
label,
|
label,
|
||||||
|
@ -137,6 +138,7 @@ provide(CanvasNodeHandleKey, {
|
||||||
isConnected,
|
isConnected,
|
||||||
isConnecting,
|
isConnecting,
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
|
maxConnections,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -155,6 +157,7 @@ provide(CanvasNodeHandleKey, {
|
||||||
<RenderType
|
<RenderType
|
||||||
:class="renderTypeClasses"
|
:class="renderTypeClasses"
|
||||||
:is-connected="isConnected"
|
:is-connected="isConnected"
|
||||||
|
:max-connections="maxConnections"
|
||||||
:style="offset"
|
:style="offset"
|
||||||
:label="label"
|
:label="label"
|
||||||
@add="onAdd"
|
@add="onAdd"
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import CanvasHandlePlus from '@/components/canvas/elements/handles/render-types/parts/CanvasHandlePlus.vue';
|
import CanvasHandlePlus from '@/components/canvas/elements/handles/render-types/parts/CanvasHandlePlus.vue';
|
||||||
import { useCanvasNodeHandle } from '@/composables/useCanvasNodeHandle';
|
import { useCanvasNodeHandle } from '@/composables/useCanvasNodeHandle';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
|
||||||
import { computed, ref, useCssModule } from 'vue';
|
import { computed, ref, useCssModule } from 'vue';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -10,7 +9,7 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
|
||||||
const { label, isConnected, isConnecting, isRequired, type } = useCanvasNodeHandle();
|
const { label, isConnected, isConnecting, isRequired, maxConnections } = useCanvasNodeHandle();
|
||||||
|
|
||||||
const handleClasses = 'target';
|
const handleClasses = 'target';
|
||||||
|
|
||||||
|
@ -20,14 +19,12 @@ const classes = computed(() => ({
|
||||||
[$style.required]: isRequired.value,
|
[$style.required]: isRequired.value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const supportsMultipleConnections = computed(() => type.value === NodeConnectionType.AiTool);
|
|
||||||
|
|
||||||
const isHandlePlusAvailable = computed(
|
const isHandlePlusAvailable = computed(
|
||||||
() => !isConnected.value || supportsMultipleConnections.value,
|
() => !isConnected.value || !maxConnections.value || maxConnections.value > 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
const isHandlePlusVisible = computed(
|
const isHandlePlusVisible = computed(
|
||||||
() => !isConnecting.value || isHovered.value || supportsMultipleConnections.value,
|
() => !isConnecting.value || isHovered.value || !maxConnections.value || maxConnections.value > 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
const isHovered = ref(false);
|
const isHovered = ref(false);
|
||||||
|
|
|
@ -156,18 +156,18 @@ function onClick(event: MouseEvent) {
|
||||||
stroke: var(--color-success);
|
stroke: var(--color-success);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.plus {
|
.plus {
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
path {
|
path {
|
||||||
fill: var(--color-primary);
|
fill: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
rect {
|
rect {
|
||||||
stroke: var(--color-primary);
|
stroke: var(--color-primary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,14 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onBeforeUnmount, onMounted, provide, ref, toRef, watch } from 'vue';
|
import {
|
||||||
|
computed,
|
||||||
|
onBeforeUnmount,
|
||||||
|
onMounted,
|
||||||
|
provide,
|
||||||
|
ref,
|
||||||
|
toRef,
|
||||||
|
useCssModule,
|
||||||
|
watch,
|
||||||
|
} from 'vue';
|
||||||
import type {
|
import type {
|
||||||
CanvasConnectionPort,
|
CanvasConnectionPort,
|
||||||
CanvasElementPortWithRenderData,
|
CanvasElementPortWithRenderData,
|
||||||
|
@ -26,6 +35,8 @@ import { createEventBus } from 'n8n-design-system';
|
||||||
type Props = NodeProps<CanvasNodeData> & {
|
type Props = NodeProps<CanvasNodeData> & {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
eventBus?: EventBus<CanvasEventBusEvents>;
|
eventBus?: EventBus<CanvasEventBusEvents>;
|
||||||
|
hovered?: boolean;
|
||||||
|
bringToFront?: boolean; // Determines if entire nodes layer should be brought to front
|
||||||
};
|
};
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -40,6 +51,8 @@ const emit = defineEmits<{
|
||||||
move: [id: string, position: XYPosition];
|
move: [id: string, position: XYPosition];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const style = useCssModule();
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
@ -63,6 +76,14 @@ const nodeTypeDescription = computed(() => {
|
||||||
return nodeTypesStore.getNodeType(props.data.type, props.data.typeVersion);
|
return nodeTypesStore.getNodeType(props.data.type, props.data.typeVersion);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const classes = computed(() => ({
|
||||||
|
[style.canvasNode]: true,
|
||||||
|
[style.showToolbar]: showToolbar.value,
|
||||||
|
hovered: props.hovered,
|
||||||
|
selected: props.selected,
|
||||||
|
'bring-to-front': props.bringToFront,
|
||||||
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event bus
|
* Event bus
|
||||||
*/
|
*/
|
||||||
|
@ -256,11 +277,7 @@ onBeforeUnmount(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div :class="classes" data-test-id="canvas-node" :data-node-type="data.type">
|
||||||
:class="[$style.canvasNode, { [$style.showToolbar]: showToolbar }]"
|
|
||||||
data-test-id="canvas-node"
|
|
||||||
:data-node-type="data.type"
|
|
||||||
>
|
|
||||||
<template
|
<template
|
||||||
v-for="source in mappedOutputs"
|
v-for="source in mappedOutputs"
|
||||||
:key="`${source.handleId}(${source.index + 1}/${mappedOutputs.length})`"
|
:key="`${source.handleId}(${source.index + 1}/${mappedOutputs.length})`"
|
||||||
|
|
|
@ -514,30 +514,40 @@ export function useCanvasMapping({
|
||||||
});
|
});
|
||||||
|
|
||||||
function getConnectionData(connection: CanvasConnection): CanvasConnectionData {
|
function getConnectionData(connection: CanvasConnection): CanvasConnectionData {
|
||||||
const fromNode = nodes.value.find((node) => node.name === connection.data?.fromNodeName);
|
const { type, index } = parseCanvasConnectionHandleString(connection.sourceHandle);
|
||||||
|
const runDataTotal =
|
||||||
|
nodeExecutionRunDataOutputMapById.value[connection.source]?.[type]?.[index]?.total ?? 0;
|
||||||
|
|
||||||
let status: CanvasConnectionData['status'];
|
let status: CanvasConnectionData['status'];
|
||||||
if (fromNode) {
|
if (nodeExecutionRunningById.value[connection.source]) {
|
||||||
const { type, index } = parseCanvasConnectionHandleString(connection.sourceHandle);
|
status = 'running';
|
||||||
const runDataTotal =
|
} else if (
|
||||||
nodeExecutionRunDataOutputMapById.value[fromNode.id]?.[type]?.[index]?.total ?? 0;
|
nodePinnedDataById.value[connection.source] &&
|
||||||
|
nodeExecutionRunDataById.value[connection.source]
|
||||||
if (nodeExecutionRunningById.value[fromNode.id]) {
|
) {
|
||||||
status = 'running';
|
status = 'pinned';
|
||||||
} else if (
|
} else if (nodeHasIssuesById.value[connection.source]) {
|
||||||
nodePinnedDataById.value[fromNode.id] &&
|
status = 'error';
|
||||||
nodeExecutionRunDataById.value[fromNode.id]
|
} else if (runDataTotal > 0) {
|
||||||
) {
|
status = 'success';
|
||||||
status = 'pinned';
|
|
||||||
} else if (nodeHasIssuesById.value[fromNode.id]) {
|
|
||||||
status = 'error';
|
|
||||||
} else if (runDataTotal > 0) {
|
|
||||||
status = 'success';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxConnections = [
|
||||||
|
...nodeInputsById.value[connection.source],
|
||||||
|
...nodeInputsById.value[connection.target],
|
||||||
|
]
|
||||||
|
.filter((port) => port.type === type)
|
||||||
|
.reduce<number | undefined>((acc, port) => {
|
||||||
|
if (port.maxConnections === undefined) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(acc ?? Infinity, port.maxConnections);
|
||||||
|
}, undefined);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...(connection.data as CanvasConnectionData),
|
...(connection.data as CanvasConnectionData),
|
||||||
|
...(maxConnections ? { maxConnections } : {}),
|
||||||
status,
|
status,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ export function useCanvasNodeHandle() {
|
||||||
const isConnecting = computed(() => handle?.isConnecting.value ?? false);
|
const isConnecting = computed(() => handle?.isConnecting.value ?? false);
|
||||||
const isReadOnly = computed(() => handle?.isReadOnly.value);
|
const isReadOnly = computed(() => handle?.isReadOnly.value);
|
||||||
const isRequired = computed(() => handle?.isRequired.value);
|
const isRequired = computed(() => handle?.isRequired.value);
|
||||||
|
const maxConnections = computed(() => handle?.maxConnections.value);
|
||||||
const type = computed(() => handle?.type.value ?? NodeConnectionType.Main);
|
const type = computed(() => handle?.type.value ?? NodeConnectionType.Main);
|
||||||
const mode = computed(() => handle?.mode.value ?? CanvasConnectionMode.Input);
|
const mode = computed(() => handle?.mode.value ?? CanvasConnectionMode.Input);
|
||||||
const index = computed(() => handle?.index.value ?? 0);
|
const index = computed(() => handle?.index.value ?? 0);
|
||||||
|
@ -27,6 +28,7 @@ export function useCanvasNodeHandle() {
|
||||||
isConnecting,
|
isConnecting,
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
isRequired,
|
isRequired,
|
||||||
|
maxConnections,
|
||||||
type,
|
type,
|
||||||
mode,
|
mode,
|
||||||
index,
|
index,
|
||||||
|
|
|
@ -77,6 +77,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vue-flow__nodes:has(.bring-to-front) {
|
||||||
|
z-index: 2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selection
|
* Selection
|
||||||
*/
|
*/
|
||||||
|
@ -92,8 +96,7 @@
|
||||||
* Edges
|
* Edges
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.vue-flow__edges:has(.selected),
|
.vue-flow__edges:has(.bring-to-front),
|
||||||
.vue-flow__edges:has(.hovered),
|
|
||||||
.vue-flow__edge-label.selected {
|
.vue-flow__edge-label.selected {
|
||||||
z-index: 1 !important;
|
z-index: 1 !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,6 +118,7 @@ export interface CanvasConnectionData {
|
||||||
target: CanvasConnectionPort;
|
target: CanvasConnectionPort;
|
||||||
fromNodeName?: string;
|
fromNodeName?: string;
|
||||||
status?: 'success' | 'error' | 'pinned' | 'running';
|
status?: 'success' | 'error' | 'pinned' | 'running';
|
||||||
|
maxConnections?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CanvasConnection = DefaultEdge<CanvasConnectionData>;
|
export type CanvasConnection = DefaultEdge<CanvasConnectionData>;
|
||||||
|
@ -173,6 +174,7 @@ export interface CanvasNodeHandleInjectionData {
|
||||||
isConnected: ComputedRef<boolean | undefined>;
|
isConnected: ComputedRef<boolean | undefined>;
|
||||||
isConnecting: Ref<boolean | undefined>;
|
isConnecting: Ref<boolean | undefined>;
|
||||||
isReadOnly: Ref<boolean | undefined>;
|
isReadOnly: Ref<boolean | undefined>;
|
||||||
|
maxConnections: Ref<number | undefined>;
|
||||||
runData: Ref<ExecutionOutputMapData | undefined>;
|
runData: Ref<ExecutionOutputMapData | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -807,7 +807,12 @@ describe('mapLegacyEndpointsToCanvasConnectionPort', () => {
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{ type: NodeConnectionType.Main, index: 0, label: undefined },
|
{ type: NodeConnectionType.Main, index: 0, label: undefined },
|
||||||
{ type: NodeConnectionType.AiTool, index: 0, label: undefined },
|
{
|
||||||
|
type: NodeConnectionType.AiTool,
|
||||||
|
index: 0,
|
||||||
|
label: undefined,
|
||||||
|
maxConnections: undefined,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -820,7 +825,13 @@ describe('mapLegacyEndpointsToCanvasConnectionPort', () => {
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{ type: NodeConnectionType.Main, index: 0, label: 'Main Input' },
|
{ type: NodeConnectionType.Main, index: 0, label: 'Main Input' },
|
||||||
{ type: NodeConnectionType.AiTool, index: 0, label: 'AI Tool', required: true },
|
{
|
||||||
|
type: NodeConnectionType.AiTool,
|
||||||
|
index: 0,
|
||||||
|
label: 'AI Tool',
|
||||||
|
required: true,
|
||||||
|
maxConnections: undefined,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -834,7 +845,12 @@ describe('mapLegacyEndpointsToCanvasConnectionPort', () => {
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{ type: NodeConnectionType.Main, index: 0, label: undefined },
|
{ type: NodeConnectionType.Main, index: 0, label: undefined },
|
||||||
{ type: NodeConnectionType.AiTool, index: 0, label: 'AI Tool' },
|
{
|
||||||
|
type: NodeConnectionType.AiTool,
|
||||||
|
index: 0,
|
||||||
|
label: 'AI Tool',
|
||||||
|
maxConnections: undefined,
|
||||||
|
},
|
||||||
{ type: NodeConnectionType.Main, index: 1, label: undefined },
|
{ type: NodeConnectionType.Main, index: 1, label: undefined },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
@ -874,7 +890,12 @@ describe('mapLegacyEndpointsToCanvasConnectionPort', () => {
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{ type: NodeConnectionType.Main, index: 0, label: 'Main Input', required: true },
|
{ type: NodeConnectionType.Main, index: 0, label: 'Main Input', required: true },
|
||||||
{ type: NodeConnectionType.AiTool, index: 0, label: 'Optional Tool' },
|
{
|
||||||
|
type: NodeConnectionType.AiTool,
|
||||||
|
index: 0,
|
||||||
|
label: 'Optional Tool',
|
||||||
|
maxConnections: undefined,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue