mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
fix(editor): Provide correct node output runData
information in new canvas (no-changelog) (#10691)
This commit is contained in:
parent
156eb72ebe
commit
468f01aaa8
|
@ -6,6 +6,7 @@ import type {
|
||||||
CanvasNodeEventBusEvents,
|
CanvasNodeEventBusEvents,
|
||||||
CanvasNodeHandleInjectionData,
|
CanvasNodeHandleInjectionData,
|
||||||
CanvasNodeInjectionData,
|
CanvasNodeInjectionData,
|
||||||
|
ExecutionOutputMapData,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
|
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
@ -25,7 +26,7 @@ export function createCanvasNodeData({
|
||||||
execution = { running: false },
|
execution = { running: false },
|
||||||
issues = { items: [], visible: false },
|
issues = { items: [], visible: false },
|
||||||
pinnedData = { count: 0, visible: false },
|
pinnedData = { count: 0, visible: false },
|
||||||
runData = { count: 0, visible: false },
|
runData = { outputMap: {}, iterations: 0, visible: false },
|
||||||
render = {
|
render = {
|
||||||
type: CanvasNodeRenderType.Default,
|
type: CanvasNodeRenderType.Default,
|
||||||
options: { configurable: false, configuration: false, trigger: false },
|
options: { configurable: false, configuration: false, trigger: false },
|
||||||
|
@ -119,6 +120,8 @@ export function createCanvasHandleProvide({
|
||||||
label = 'Handle',
|
label = 'Handle',
|
||||||
mode = CanvasConnectionMode.Input,
|
mode = CanvasConnectionMode.Input,
|
||||||
type = NodeConnectionType.Main,
|
type = NodeConnectionType.Main,
|
||||||
|
index = 0,
|
||||||
|
runData,
|
||||||
isConnected = false,
|
isConnected = false,
|
||||||
isConnecting = false,
|
isConnecting = false,
|
||||||
isReadOnly = false,
|
isReadOnly = false,
|
||||||
|
@ -126,6 +129,8 @@ export function createCanvasHandleProvide({
|
||||||
label?: string;
|
label?: string;
|
||||||
mode?: CanvasConnectionMode;
|
mode?: CanvasConnectionMode;
|
||||||
type?: NodeConnectionType;
|
type?: NodeConnectionType;
|
||||||
|
index?: number;
|
||||||
|
runData?: ExecutionOutputMapData;
|
||||||
isConnected?: boolean;
|
isConnected?: boolean;
|
||||||
isConnecting?: boolean;
|
isConnecting?: boolean;
|
||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
|
@ -135,8 +140,10 @@ export function createCanvasHandleProvide({
|
||||||
label: ref(label),
|
label: ref(label),
|
||||||
mode: ref(mode),
|
mode: ref(mode),
|
||||||
type: ref(type),
|
type: ref(type),
|
||||||
|
index: ref(index),
|
||||||
isConnected: ref(isConnected),
|
isConnected: ref(isConnected),
|
||||||
isConnecting: ref(isConnecting),
|
isConnecting: ref(isConnecting),
|
||||||
|
runData: ref(runData),
|
||||||
isReadOnly: ref(isReadOnly),
|
isReadOnly: ref(isReadOnly),
|
||||||
} satisfies CanvasNodeHandleInjectionData,
|
} satisfies CanvasNodeHandleInjectionData,
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
import { computed, h, provide, toRef, useCssModule } from 'vue';
|
import { computed, h, provide, toRef, useCssModule } from 'vue';
|
||||||
import type { CanvasConnectionPort, CanvasElementPortWithRenderData } from '@/types';
|
import type { CanvasConnectionPort, CanvasElementPortWithRenderData } from '@/types';
|
||||||
import { CanvasConnectionMode } from '@/types';
|
import { CanvasConnectionMode } from '@/types';
|
||||||
|
|
||||||
import type { ValidConnectionFunc } from '@vue-flow/core';
|
import type { ValidConnectionFunc } from '@vue-flow/core';
|
||||||
import { Handle } from '@vue-flow/core';
|
import { Handle } from '@vue-flow/core';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
@ -13,6 +12,7 @@ import CanvasHandleNonMainInput from '@/components/canvas/elements/handles/rende
|
||||||
import CanvasHandleNonMainOutput from '@/components/canvas/elements/handles/render-types/CanvasHandleNonMainOutput.vue';
|
import CanvasHandleNonMainOutput from '@/components/canvas/elements/handles/render-types/CanvasHandleNonMainOutput.vue';
|
||||||
import { CanvasNodeHandleKey } from '@/constants';
|
import { CanvasNodeHandleKey } from '@/constants';
|
||||||
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
|
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
|
||||||
|
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
mode: CanvasConnectionMode;
|
mode: CanvasConnectionMode;
|
||||||
|
@ -59,6 +59,18 @@ const isConnectableEnd = computed(() => {
|
||||||
|
|
||||||
const handleClasses = computed(() => [style.handle, style[props.type], style[props.mode]]);
|
const handleClasses = computed(() => [style.handle, style[props.type], style[props.mode]]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run data
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { runDataOutputMap } = useCanvasNode();
|
||||||
|
|
||||||
|
const runData = computed(() =>
|
||||||
|
props.mode === CanvasConnectionMode.Output
|
||||||
|
? runDataOutputMap.value[props.type]?.[props.index]
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render additional elements
|
* Render additional elements
|
||||||
*/
|
*/
|
||||||
|
@ -103,11 +115,14 @@ const isConnecting = toRef(props, 'isConnecting');
|
||||||
const isReadOnly = toRef(props, 'isReadOnly');
|
const isReadOnly = toRef(props, 'isReadOnly');
|
||||||
const mode = toRef(props, 'mode');
|
const mode = toRef(props, 'mode');
|
||||||
const type = toRef(props, 'type');
|
const type = toRef(props, 'type');
|
||||||
|
const index = toRef(props, 'index');
|
||||||
|
|
||||||
provide(CanvasNodeHandleKey, {
|
provide(CanvasNodeHandleKey, {
|
||||||
label,
|
label,
|
||||||
mode,
|
mode,
|
||||||
type,
|
type,
|
||||||
|
index,
|
||||||
|
runData,
|
||||||
isConnected,
|
isConnected,
|
||||||
isConnecting,
|
isConnecting,
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
|
|
|
@ -31,4 +31,36 @@ describe('CanvasHandleMainOutput', () => {
|
||||||
|
|
||||||
expect(queryByTestId('canvas-handle-plus')).not.toBeInTheDocument();
|
expect(queryByTestId('canvas-handle-plus')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should render run data label', async () => {
|
||||||
|
const runData = {
|
||||||
|
total: 1,
|
||||||
|
iterations: 1,
|
||||||
|
};
|
||||||
|
const { getByText } = renderComponent({
|
||||||
|
global: {
|
||||||
|
provide: {
|
||||||
|
...createCanvasHandleProvide({ label: '', runData }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(getByText('1 item')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render run data label if output label is available', async () => {
|
||||||
|
const runData = {
|
||||||
|
total: 1,
|
||||||
|
iterations: 1,
|
||||||
|
};
|
||||||
|
const { getByText } = renderComponent({
|
||||||
|
global: {
|
||||||
|
provide: {
|
||||||
|
...createCanvasHandleProvide({ label: 'Output', runData }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => getByText('1 item')).toThrow();
|
||||||
|
expect(getByText('Output')).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,28 +3,41 @@ import { useCanvasNodeHandle } from '@/composables/useCanvasNodeHandle';
|
||||||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import type { CanvasNodeDefaultRender } from '@/types';
|
import type { CanvasNodeDefaultRender } from '@/types';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
add: [];
|
add: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
const { render } = useCanvasNode();
|
const { render } = useCanvasNode();
|
||||||
const { label, isConnected, isConnecting, isReadOnly } = useCanvasNodeHandle();
|
const { label, isConnected, isConnecting, isReadOnly, runData } = useCanvasNodeHandle();
|
||||||
|
|
||||||
const handleClasses = 'source';
|
const handleClasses = 'source';
|
||||||
const isHovered = ref(false);
|
const isHovered = ref(false);
|
||||||
|
|
||||||
const renderOptions = computed(() => render.value.options as CanvasNodeDefaultRender['options']);
|
const renderOptions = computed(() => render.value.options as CanvasNodeDefaultRender['options']);
|
||||||
|
|
||||||
|
const runDataLabel = computed(() =>
|
||||||
|
runData.value
|
||||||
|
? i18n.baseText('ndv.output.items', {
|
||||||
|
adjustToNumber: runData.value.total,
|
||||||
|
interpolate: { count: String(runData.value.total) },
|
||||||
|
})
|
||||||
|
: '',
|
||||||
|
);
|
||||||
|
|
||||||
const isHandlePlusVisible = computed(() => !isConnecting.value || isHovered.value);
|
const isHandlePlusVisible = computed(() => !isConnecting.value || isHovered.value);
|
||||||
|
|
||||||
|
const plusState = computed(() => (runData.value ? 'success' : 'default'));
|
||||||
|
|
||||||
const plusLineSize = computed(
|
const plusLineSize = computed(
|
||||||
() =>
|
() =>
|
||||||
({
|
({
|
||||||
small: 46,
|
small: 46,
|
||||||
medium: 66,
|
medium: 66,
|
||||||
large: 80,
|
large: 80,
|
||||||
})[renderOptions.value.outputs?.labelSize ?? 'small'],
|
})[renderOptions.value.outputs?.labelSize ?? runData.value ? 'large' : 'small'],
|
||||||
);
|
);
|
||||||
|
|
||||||
function onMouseEnter() {
|
function onMouseEnter() {
|
||||||
|
@ -41,7 +54,8 @@ function onClickAdd() {
|
||||||
</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 v-if="label" :class="[$style.label, $style.outputLabel]">{{ label }}</div>
|
||||||
|
<div v-else-if="runData" :class="[$style.label, $style.runDataLabel]">{{ runDataLabel }}</div>
|
||||||
<CanvasHandleDot :handle-classes="handleClasses" />
|
<CanvasHandleDot :handle-classes="handleClasses" />
|
||||||
<Transition name="canvas-node-handle-main-output">
|
<Transition name="canvas-node-handle-main-output">
|
||||||
<CanvasHandlePlus
|
<CanvasHandlePlus
|
||||||
|
@ -50,6 +64,7 @@ function onClickAdd() {
|
||||||
data-test-id="canvas-handle-plus"
|
data-test-id="canvas-handle-plus"
|
||||||
:line-size="plusLineSize"
|
:line-size="plusLineSize"
|
||||||
:handle-classes="handleClasses"
|
:handle-classes="handleClasses"
|
||||||
|
:state="plusState"
|
||||||
@mouseenter="onMouseEnter"
|
@mouseenter="onMouseEnter"
|
||||||
@mouseleave="onMouseLeave"
|
@mouseleave="onMouseLeave"
|
||||||
@click:plus="onClickAdd"
|
@click:plus="onClickAdd"
|
||||||
|
@ -68,11 +83,6 @@ function onClickAdd() {
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
|
||||||
left: var(--spacing-m);
|
|
||||||
transform: translate(0, -50%);
|
|
||||||
font-size: var(--font-size-2xs);
|
|
||||||
color: var(--color-foreground-xdark);
|
|
||||||
background: var(--color-canvas-label-background);
|
background: var(--color-canvas-label-background);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
max-width: calc(100% - var(--spacing-m) - 24px);
|
max-width: calc(100% - var(--spacing-m) - 24px);
|
||||||
|
@ -80,6 +90,23 @@ function onClickAdd() {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.outputLabel {
|
||||||
|
top: 50%;
|
||||||
|
left: var(--spacing-m);
|
||||||
|
transform: translate(0, -50%);
|
||||||
|
font-size: var(--font-size-2xs);
|
||||||
|
color: var(--color-foreground-xdark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.runDataLabel {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--color-success);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
@ -40,6 +40,14 @@ describe('CanvasHandlePlus', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should apply correct classes based on state', () => {
|
||||||
|
const { container } = renderComponent({
|
||||||
|
props: { state: 'success' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container.firstChild).toHaveClass('success');
|
||||||
|
});
|
||||||
|
|
||||||
it('should render SVG elements correctly', () => {
|
it('should render SVG elements correctly', () => {
|
||||||
const { container } = renderComponent();
|
const { container } = renderComponent();
|
||||||
|
|
||||||
|
|
|
@ -7,12 +7,14 @@ const props = withDefaults(
|
||||||
handleClasses?: string;
|
handleClasses?: string;
|
||||||
plusSize?: number;
|
plusSize?: number;
|
||||||
lineSize?: number;
|
lineSize?: number;
|
||||||
|
state?: 'success' | 'default';
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
position: 'right',
|
position: 'right',
|
||||||
handleClasses: undefined,
|
handleClasses: undefined,
|
||||||
plusSize: 24,
|
plusSize: 24,
|
||||||
lineSize: 46,
|
lineSize: 46,
|
||||||
|
state: 'default',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -22,7 +24,12 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const style = useCssModule();
|
const style = useCssModule();
|
||||||
|
|
||||||
const classes = computed(() => [style.wrapper, style[props.position], props.handleClasses]);
|
const classes = computed(() => [
|
||||||
|
style.wrapper,
|
||||||
|
style[props.position],
|
||||||
|
style[props.state],
|
||||||
|
props.handleClasses,
|
||||||
|
]);
|
||||||
|
|
||||||
const viewBox = computed(() => {
|
const viewBox = computed(() => {
|
||||||
switch (props.position) {
|
switch (props.position) {
|
||||||
|
@ -91,7 +98,7 @@ function onClick(event: MouseEvent) {
|
||||||
<template>
|
<template>
|
||||||
<svg :class="classes" :viewBox="`0 0 ${viewBox.width} ${viewBox.height}`" :style="styles">
|
<svg :class="classes" :viewBox="`0 0 ${viewBox.width} ${viewBox.height}`" :style="styles">
|
||||||
<line
|
<line
|
||||||
:class="handleClasses"
|
:class="[handleClasses, $style.line]"
|
||||||
:x1="linePosition[0][0]"
|
:x1="linePosition[0][0]"
|
||||||
:y1="linePosition[0][1]"
|
:y1="linePosition[0][1]"
|
||||||
:x2="linePosition[1][0]"
|
:x2="linePosition[1][0]"
|
||||||
|
@ -127,6 +134,10 @@ function onClick(event: MouseEvent) {
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.wrapper {
|
.wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
&.success .line {
|
||||||
|
stroke: var(--color-success);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.plus {
|
.plus {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`CanvasHandleDiamond > should render with default props 1`] = `
|
exports[`CanvasHandleDiamond > should render with default props 1`] = `
|
||||||
"<svg class="wrapper right" viewBox="0 0 70 24" style="width: 70px; height: 24px;">
|
"<svg class="wrapper right default" viewBox="0 0 70 24" style="width: 70px; height: 24px;">
|
||||||
<line class="" x1="0" y1="12" x2="47" y2="12" stroke="var(--color-foreground-xdark)" stroke-width="2"></line>
|
<line class="line" x1="0" y1="12" x2="47" y2="12" stroke="var(--color-foreground-xdark)" stroke-width="2"></line>
|
||||||
<g class="plus clickable" transform="translate(46, 0)">
|
<g class="plus clickable" transform="translate(46, 0)">
|
||||||
<rect class="clickable" x="2" y="2" width="20" height="20" stroke="var(--color-foreground-xdark)" stroke-width="2" rx="4" fill="#ffffff"></rect>
|
<rect class="clickable" x="2" y="2" width="20" height="20" stroke="var(--color-foreground-xdark)" stroke-width="2" rx="4" fill="#ffffff"></rect>
|
||||||
<path class="clickable" 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>
|
<path class="clickable" 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>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`CanvasHandlePlus > should render with default props 1`] = `
|
exports[`CanvasHandlePlus > should render with default props 1`] = `
|
||||||
"<svg class="wrapper right" viewBox="0 0 70 24" style="width: 70px; height: 24px;">
|
"<svg class="wrapper right default" viewBox="0 0 70 24" style="width: 70px; height: 24px;">
|
||||||
<line class="" x1="0" y1="12" x2="47" y2="12" stroke="var(--color-foreground-xdark)" stroke-width="2"></line>
|
<line class="line" x1="0" y1="12" x2="47" y2="12" stroke="var(--color-foreground-xdark)" stroke-width="2"></line>
|
||||||
<g class="plus clickable" transform="translate(46, 0)">
|
<g class="plus clickable" transform="translate(46, 0)">
|
||||||
<rect class="clickable" x="2" y="2" width="20" height="20" stroke="var(--color-foreground-xdark)" stroke-width="2" rx="4" fill="#ffffff"></rect>
|
<rect class="clickable" x="2" y="2" width="20" height="20" stroke="var(--color-foreground-xdark)" stroke-width="2" rx="4" fill="#ffffff"></rect>
|
||||||
<path class="clickable" 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>
|
<path class="clickable" 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>
|
||||||
|
|
|
@ -31,7 +31,9 @@ describe('CanvasNodeStatusIcons', () => {
|
||||||
it('should render correctly for a node that ran successfully', () => {
|
it('should render correctly for a node that ran successfully', () => {
|
||||||
const { getByTestId } = renderComponent({
|
const { getByTestId } = renderComponent({
|
||||||
global: {
|
global: {
|
||||||
provide: createCanvasNodeProvide({ data: { runData: { count: 15, visible: true } } }),
|
provide: createCanvasNodeProvide({
|
||||||
|
data: { runData: { outputMap: {}, iterations: 15, visible: true } },
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ const {
|
||||||
executionWaiting,
|
executionWaiting,
|
||||||
executionRunning,
|
executionRunning,
|
||||||
hasRunData,
|
hasRunData,
|
||||||
runDataCount,
|
runDataIterations,
|
||||||
} = useCanvasNode();
|
} = useCanvasNode();
|
||||||
|
|
||||||
const hideNodeIssues = computed(() => false); // @TODO Implement this
|
const hideNodeIssues = computed(() => false); // @TODO Implement this
|
||||||
|
@ -67,7 +67,7 @@ const hideNodeIssues = computed(() => false); // @TODO Implement this
|
||||||
:class="[$style.status, $style.runData]"
|
:class="[$style.status, $style.runData]"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon="check" />
|
<FontAwesomeIcon icon="check" />
|
||||||
<span v-if="runDataCount > 1" :class="$style.count"> {{ runDataCount }}</span>
|
<span v-if="runDataIterations > 1" :class="$style.count"> {{ runDataIterations }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -2,18 +2,18 @@ import type { Ref } from 'vue';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
import type { Workflow } from 'n8n-workflow';
|
import type { Workflow } from 'n8n-workflow';
|
||||||
import { createPinia, setActivePinia } from 'pinia';
|
import { setActivePinia } from 'pinia';
|
||||||
|
|
||||||
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
||||||
import type { INodeUi } from '@/Interface';
|
import type { INodeUi } from '@/Interface';
|
||||||
import {
|
import {
|
||||||
|
createTestNode,
|
||||||
createTestWorkflowObject,
|
createTestWorkflowObject,
|
||||||
mockNode,
|
mockNode,
|
||||||
mockNodes,
|
mockNodes,
|
||||||
mockNodeTypeDescription,
|
mockNodeTypeDescription,
|
||||||
} from '@/__tests__/mocks';
|
} from '@/__tests__/mocks';
|
||||||
import { MANUAL_TRIGGER_NODE_TYPE, SET_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants';
|
import { MANUAL_TRIGGER_NODE_TYPE, SET_NODE_TYPE, STICKY_NODE_TYPE, STORES } from '@/constants';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import {
|
import {
|
||||||
createCanvasConnectionHandleString,
|
createCanvasConnectionHandleString,
|
||||||
|
@ -21,19 +21,32 @@ import {
|
||||||
} from '@/utils/canvasUtilsV2';
|
} from '@/utils/canvasUtilsV2';
|
||||||
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
|
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
|
||||||
import { MarkerType } from '@vue-flow/core';
|
import { MarkerType } from '@vue-flow/core';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import { mockedStore } from '@/__tests__/utils';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const pinia = createPinia();
|
const pinia = createTestingPinia({
|
||||||
|
initialState: {
|
||||||
|
[STORES.WORKFLOWS]: {
|
||||||
|
workflowExecutionData: {},
|
||||||
|
},
|
||||||
|
[STORES.NODE_TYPES]: {
|
||||||
|
nodeTypes: {
|
||||||
|
[MANUAL_TRIGGER_NODE_TYPE]: {
|
||||||
|
1: mockNodeTypeDescription({
|
||||||
|
name: MANUAL_TRIGGER_NODE_TYPE,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
[SET_NODE_TYPE]: {
|
||||||
|
1: mockNodeTypeDescription({
|
||||||
|
name: SET_NODE_TYPE,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
setActivePinia(pinia);
|
setActivePinia(pinia);
|
||||||
|
|
||||||
useNodeTypesStore().setNodeTypes([
|
|
||||||
mockNodeTypeDescription({
|
|
||||||
name: MANUAL_TRIGGER_NODE_TYPE,
|
|
||||||
}),
|
|
||||||
mockNodeTypeDescription({
|
|
||||||
name: SET_NODE_TYPE,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -61,6 +74,7 @@ describe('useCanvasMapping', () => {
|
||||||
|
|
||||||
describe('nodes', () => {
|
describe('nodes', () => {
|
||||||
it('should map nodes to canvas nodes', () => {
|
it('should map nodes to canvas nodes', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
const manualTriggerNode = mockNode({
|
const manualTriggerNode = mockNode({
|
||||||
name: 'Manual Trigger',
|
name: 'Manual Trigger',
|
||||||
type: MANUAL_TRIGGER_NODE_TYPE,
|
type: MANUAL_TRIGGER_NODE_TYPE,
|
||||||
|
@ -79,6 +93,8 @@ describe('useCanvasMapping', () => {
|
||||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
workflowsStore.isNodeExecuting.mockReturnValue(false);
|
||||||
|
|
||||||
expect(mappedNodes.value).toEqual([
|
expect(mappedNodes.value).toEqual([
|
||||||
{
|
{
|
||||||
id: manualTriggerNode.id,
|
id: manualTriggerNode.id,
|
||||||
|
@ -106,7 +122,8 @@ describe('useCanvasMapping', () => {
|
||||||
visible: false,
|
visible: false,
|
||||||
},
|
},
|
||||||
runData: {
|
runData: {
|
||||||
count: 0,
|
iterations: 0,
|
||||||
|
outputMap: {},
|
||||||
visible: false,
|
visible: false,
|
||||||
},
|
},
|
||||||
inputs: [
|
inputs: [
|
||||||
|
@ -169,6 +186,7 @@ describe('useCanvasMapping', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle execution state', () => {
|
it('should handle execution state', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
const manualTriggerNode = mockNode({
|
const manualTriggerNode = mockNode({
|
||||||
name: 'Manual Trigger',
|
name: 'Manual Trigger',
|
||||||
type: MANUAL_TRIGGER_NODE_TYPE,
|
type: MANUAL_TRIGGER_NODE_TYPE,
|
||||||
|
@ -181,7 +199,7 @@ describe('useCanvasMapping', () => {
|
||||||
connections,
|
connections,
|
||||||
});
|
});
|
||||||
|
|
||||||
useWorkflowsStore().addExecutingNode(manualTriggerNode.name);
|
workflowsStore.isNodeExecuting.mockReturnValue(true);
|
||||||
|
|
||||||
const { nodes: mappedNodes } = useCanvasMapping({
|
const { nodes: mappedNodes } = useCanvasMapping({
|
||||||
nodes: ref(nodes),
|
nodes: ref(nodes),
|
||||||
|
@ -336,6 +354,195 @@ describe('useCanvasMapping', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('runData', () => {
|
||||||
|
describe('nodeExecutionRunDataOutputMapById', () => {
|
||||||
|
it('should return an empty object when there is no run data', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const nodes: INodeUi[] = [];
|
||||||
|
const connections = {};
|
||||||
|
const workflowObject = createTestWorkflowObject({
|
||||||
|
nodes,
|
||||||
|
connections,
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue(null);
|
||||||
|
|
||||||
|
const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({
|
||||||
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nodeExecutionRunDataOutputMapById.value).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate iterations and total correctly for single node', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const nodes = [createTestNode({ name: 'Node 1' })];
|
||||||
|
const connections = {};
|
||||||
|
const workflowObject = createTestWorkflowObject({
|
||||||
|
nodes,
|
||||||
|
connections,
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue([
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
source: [],
|
||||||
|
data: {
|
||||||
|
[NodeConnectionType.Main]: [[{ json: {} }, { json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({
|
||||||
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nodeExecutionRunDataOutputMapById.value).toEqual({
|
||||||
|
[nodes[0].id]: {
|
||||||
|
[NodeConnectionType.Main]: {
|
||||||
|
0: {
|
||||||
|
iterations: 1,
|
||||||
|
total: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple nodes with different connection types', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const nodes = [
|
||||||
|
createTestNode({ id: 'node1', name: 'Node 1' }),
|
||||||
|
createTestNode({ id: 'node2', name: 'Node 2' }),
|
||||||
|
];
|
||||||
|
const connections = {};
|
||||||
|
const workflowObject = createTestWorkflowObject({
|
||||||
|
nodes,
|
||||||
|
connections,
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowsStore.getWorkflowResultDataByNodeName.mockImplementation((nodeName: string) => {
|
||||||
|
if (nodeName === 'Node 1') {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
source: [],
|
||||||
|
data: {
|
||||||
|
[NodeConnectionType.Main]: [[{ json: {} }]],
|
||||||
|
[NodeConnectionType.AiAgent]: [[{ json: {} }, { json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else if (nodeName === 'Node 2') {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
source: [],
|
||||||
|
data: {
|
||||||
|
[NodeConnectionType.Main]: [[{ json: {} }, { json: {} }, { json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({
|
||||||
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nodeExecutionRunDataOutputMapById.value).toEqual({
|
||||||
|
node1: {
|
||||||
|
[NodeConnectionType.Main]: {
|
||||||
|
0: {
|
||||||
|
iterations: 1,
|
||||||
|
total: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[NodeConnectionType.AiAgent]: {
|
||||||
|
0: {
|
||||||
|
iterations: 1,
|
||||||
|
total: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
node2: {
|
||||||
|
[NodeConnectionType.Main]: {
|
||||||
|
0: {
|
||||||
|
iterations: 1,
|
||||||
|
total: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles multiple iterations correctly', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const nodes = [createTestNode({ name: 'Node 1' })];
|
||||||
|
const connections = {};
|
||||||
|
const workflowObject = createTestWorkflowObject({
|
||||||
|
nodes,
|
||||||
|
connections,
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue([
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
source: [],
|
||||||
|
data: {
|
||||||
|
[NodeConnectionType.Main]: [[{ json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
source: [],
|
||||||
|
data: {
|
||||||
|
[NodeConnectionType.Main]: [[{ json: {} }, { json: {} }, { json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
source: [],
|
||||||
|
data: {
|
||||||
|
[NodeConnectionType.Main]: [[{ json: {} }, { json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({
|
||||||
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nodeExecutionRunDataOutputMapById.value).toEqual({
|
||||||
|
[nodes[0].id]: {
|
||||||
|
[NodeConnectionType.Main]: {
|
||||||
|
0: {
|
||||||
|
iterations: 3,
|
||||||
|
total: 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('connections', () => {
|
describe('connections', () => {
|
||||||
|
|
|
@ -18,11 +18,13 @@ import type {
|
||||||
CanvasNodeDefaultRender,
|
CanvasNodeDefaultRender,
|
||||||
CanvasNodeDefaultRenderLabelSize,
|
CanvasNodeDefaultRenderLabelSize,
|
||||||
CanvasNodeStickyNoteRender,
|
CanvasNodeStickyNoteRender,
|
||||||
|
ExecutionOutputMap,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
|
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
|
||||||
import {
|
import {
|
||||||
mapLegacyConnectionsToCanvasConnections,
|
mapLegacyConnectionsToCanvasConnections,
|
||||||
mapLegacyEndpointsToCanvasConnectionPort,
|
mapLegacyEndpointsToCanvasConnectionPort,
|
||||||
|
parseCanvasConnectionHandleString,
|
||||||
} from '@/utils/canvasUtilsV2';
|
} from '@/utils/canvasUtilsV2';
|
||||||
import type {
|
import type {
|
||||||
ExecutionStatus,
|
ExecutionStatus,
|
||||||
|
@ -241,6 +243,39 @@ export function useCanvasMapping({
|
||||||
}, {}),
|
}, {}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const nodeExecutionRunDataOutputMapById = computed(() =>
|
||||||
|
Object.keys(nodeExecutionRunDataById.value).reduce<Record<string, ExecutionOutputMap>>(
|
||||||
|
(acc, nodeId) => {
|
||||||
|
acc[nodeId] = {};
|
||||||
|
|
||||||
|
const outputData = { iterations: 0, total: 0 };
|
||||||
|
for (const runIteration of nodeExecutionRunDataById.value[nodeId] ?? []) {
|
||||||
|
const data = runIteration.data ?? {};
|
||||||
|
|
||||||
|
for (const connectionType of Object.keys(data)) {
|
||||||
|
const connectionTypeData = data[connectionType] ?? {};
|
||||||
|
acc[nodeId][connectionType] = acc[nodeId][connectionType] ?? {};
|
||||||
|
|
||||||
|
for (const outputIndex of Object.keys(connectionTypeData)) {
|
||||||
|
const parsedOutputIndex = parseInt(outputIndex, 10);
|
||||||
|
const connectionTypeOutputIndexData = connectionTypeData[parsedOutputIndex] ?? [];
|
||||||
|
|
||||||
|
acc[nodeId][connectionType][outputIndex] = acc[nodeId][connectionType][
|
||||||
|
outputIndex
|
||||||
|
] ?? { ...outputData };
|
||||||
|
acc[nodeId][connectionType][outputIndex].iterations += 1;
|
||||||
|
acc[nodeId][connectionType][outputIndex].total +=
|
||||||
|
connectionTypeOutputIndexData.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const nodeIssuesById = computed(() =>
|
const nodeIssuesById = computed(() =>
|
||||||
nodes.value.reduce<Record<string, string[]>>((acc, node) => {
|
nodes.value.reduce<Record<string, string[]>>((acc, node) => {
|
||||||
const issues: string[] = [];
|
const issues: string[] = [];
|
||||||
|
@ -359,7 +394,8 @@ export function useCanvasMapping({
|
||||||
running: nodeExecutionRunningById.value[node.id],
|
running: nodeExecutionRunningById.value[node.id],
|
||||||
},
|
},
|
||||||
runData: {
|
runData: {
|
||||||
count: nodeExecutionRunDataById.value[node.id]?.length ?? 0,
|
outputMap: nodeExecutionRunDataOutputMapById.value[node.id],
|
||||||
|
iterations: nodeExecutionRunDataById.value[node.id]?.length ?? 0,
|
||||||
visible: !!nodeExecutionRunDataById.value[node.id],
|
visible: !!nodeExecutionRunDataById.value[node.id],
|
||||||
},
|
},
|
||||||
render: renderTypeByNodeId.value[node.id] ?? { type: 'default', options: {} },
|
render: renderTypeByNodeId.value[node.id] ?? { type: 'default', options: {} },
|
||||||
|
@ -426,7 +462,6 @@ export function useCanvasMapping({
|
||||||
|
|
||||||
function getConnectionLabel(connection: CanvasConnection): string {
|
function getConnectionLabel(connection: CanvasConnection): string {
|
||||||
const fromNode = nodes.value.find((node) => node.name === connection.data?.fromNodeName);
|
const fromNode = nodes.value.find((node) => node.name === connection.data?.fromNodeName);
|
||||||
|
|
||||||
if (!fromNode) {
|
if (!fromNode) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
@ -438,10 +473,13 @@ export function useCanvasMapping({
|
||||||
interpolate: { count: String(pinnedDataCount) },
|
interpolate: { count: String(pinnedDataCount) },
|
||||||
});
|
});
|
||||||
} else if (nodeExecutionRunDataById.value[fromNode.id]) {
|
} else if (nodeExecutionRunDataById.value[fromNode.id]) {
|
||||||
const runDataCount = nodeExecutionRunDataById.value[fromNode.id]?.length ?? 0;
|
const { type, index } = parseCanvasConnectionHandleString(connection.sourceHandle);
|
||||||
|
const runDataTotal =
|
||||||
|
nodeExecutionRunDataOutputMapById.value[fromNode.id]?.[type]?.[index]?.total ?? 0;
|
||||||
|
|
||||||
return i18n.baseText('ndv.output.items', {
|
return i18n.baseText('ndv.output.items', {
|
||||||
adjustToNumber: runDataCount,
|
adjustToNumber: runDataTotal,
|
||||||
interpolate: { count: String(runDataCount) },
|
interpolate: { count: String(runDataTotal) },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -449,6 +487,7 @@ export function useCanvasMapping({
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
nodeExecutionRunDataOutputMapById,
|
||||||
connections: mappedConnections,
|
connections: mappedConnections,
|
||||||
nodes: mappedNodes,
|
nodes: mappedNodes,
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,7 +27,8 @@ describe('useCanvasNode', () => {
|
||||||
expect(result.isSelected.value).toBeUndefined();
|
expect(result.isSelected.value).toBeUndefined();
|
||||||
expect(result.pinnedDataCount.value).toBe(0);
|
expect(result.pinnedDataCount.value).toBe(0);
|
||||||
expect(result.hasPinnedData.value).toBe(false);
|
expect(result.hasPinnedData.value).toBe(false);
|
||||||
expect(result.runDataCount.value).toBe(0);
|
expect(result.runDataOutputMap.value).toEqual({});
|
||||||
|
expect(result.runDataIterations.value).toBe(0);
|
||||||
expect(result.hasRunData.value).toBe(false);
|
expect(result.hasRunData.value).toBe(false);
|
||||||
expect(result.issues.value).toEqual([]);
|
expect(result.issues.value).toEqual([]);
|
||||||
expect(result.hasIssues.value).toBe(false);
|
expect(result.hasIssues.value).toBe(false);
|
||||||
|
@ -54,7 +55,7 @@ describe('useCanvasNode', () => {
|
||||||
},
|
},
|
||||||
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: { outputMap: {}, iterations: 1, visible: true },
|
||||||
pinnedData: { count: 1, visible: true },
|
pinnedData: { count: 1, visible: true },
|
||||||
render: {
|
render: {
|
||||||
type: CanvasNodeRenderType.Default,
|
type: CanvasNodeRenderType.Default,
|
||||||
|
@ -86,7 +87,8 @@ describe('useCanvasNode', () => {
|
||||||
expect(result.isSelected.value).toBe(true);
|
expect(result.isSelected.value).toBe(true);
|
||||||
expect(result.pinnedDataCount.value).toBe(1);
|
expect(result.pinnedDataCount.value).toBe(1);
|
||||||
expect(result.hasPinnedData.value).toBe(true);
|
expect(result.hasPinnedData.value).toBe(true);
|
||||||
expect(result.runDataCount.value).toBe(1);
|
expect(result.runDataOutputMap.value).toEqual({});
|
||||||
|
expect(result.runDataIterations.value).toBe(1);
|
||||||
expect(result.hasRunData.value).toBe(true);
|
expect(result.hasRunData.value).toBe(true);
|
||||||
expect(result.issues.value).toEqual(['issue1']);
|
expect(result.issues.value).toEqual(['issue1']);
|
||||||
expect(result.hasIssues.value).toBe(true);
|
expect(result.hasIssues.value).toBe(true);
|
||||||
|
|
|
@ -10,9 +10,10 @@ import { CanvasNodeRenderType, CanvasConnectionMode } from '@/types';
|
||||||
|
|
||||||
export function useCanvasNode() {
|
export function useCanvasNode() {
|
||||||
const node = inject(CanvasNodeKey);
|
const node = inject(CanvasNodeKey);
|
||||||
const data = computed<CanvasNodeData>(
|
const data = computed(
|
||||||
() =>
|
() =>
|
||||||
node?.data.value ?? {
|
node?.data.value ??
|
||||||
|
({
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
subtitle: '',
|
subtitle: '',
|
||||||
|
@ -27,12 +28,12 @@ export function useCanvasNode() {
|
||||||
execution: {
|
execution: {
|
||||||
running: false,
|
running: false,
|
||||||
},
|
},
|
||||||
runData: { count: 0, visible: false },
|
runData: { iterations: 0, outputMap: {}, visible: false },
|
||||||
render: {
|
render: {
|
||||||
type: CanvasNodeRenderType.Default,
|
type: CanvasNodeRenderType.Default,
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
},
|
} satisfies CanvasNodeData),
|
||||||
);
|
);
|
||||||
|
|
||||||
const id = computed(() => node?.id.value ?? '');
|
const id = computed(() => node?.id.value ?? '');
|
||||||
|
@ -58,7 +59,8 @@ export function useCanvasNode() {
|
||||||
const executionWaiting = computed(() => data.value.execution.waiting);
|
const executionWaiting = computed(() => data.value.execution.waiting);
|
||||||
const executionRunning = computed(() => data.value.execution.running);
|
const executionRunning = computed(() => data.value.execution.running);
|
||||||
|
|
||||||
const runDataCount = computed(() => data.value.runData.count);
|
const runDataOutputMap = computed(() => data.value.runData.outputMap);
|
||||||
|
const runDataIterations = computed(() => data.value.runData.iterations);
|
||||||
const hasRunData = computed(() => data.value.runData.visible);
|
const hasRunData = computed(() => data.value.runData.visible);
|
||||||
|
|
||||||
const render = computed(() => data.value.render);
|
const render = computed(() => data.value.render);
|
||||||
|
@ -79,7 +81,8 @@ export function useCanvasNode() {
|
||||||
isSelected,
|
isSelected,
|
||||||
pinnedDataCount,
|
pinnedDataCount,
|
||||||
hasPinnedData,
|
hasPinnedData,
|
||||||
runDataCount,
|
runDataIterations,
|
||||||
|
runDataOutputMap,
|
||||||
hasRunData,
|
hasRunData,
|
||||||
issues,
|
issues,
|
||||||
hasIssues,
|
hasIssues,
|
||||||
|
|
|
@ -14,9 +14,11 @@ export function useCanvasNodeHandle() {
|
||||||
const label = computed(() => handle?.label.value ?? '');
|
const label = computed(() => handle?.label.value ?? '');
|
||||||
const isConnected = computed(() => handle?.isConnected.value ?? false);
|
const isConnected = computed(() => handle?.isConnected.value ?? false);
|
||||||
const isConnecting = computed(() => handle?.isConnecting.value ?? false);
|
const isConnecting = computed(() => handle?.isConnecting.value ?? false);
|
||||||
|
const isReadOnly = computed(() => handle?.isReadOnly.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 isReadOnly = computed(() => handle?.isReadOnly.value);
|
const index = computed(() => handle?.index.value ?? 0);
|
||||||
|
const runData = computed(() => handle?.runData.value);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
|
@ -25,5 +27,7 @@ export function useCanvasNodeHandle() {
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
type,
|
type,
|
||||||
mode,
|
mode,
|
||||||
|
index,
|
||||||
|
runData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,7 +103,8 @@ export interface CanvasNodeData {
|
||||||
running: boolean;
|
running: boolean;
|
||||||
};
|
};
|
||||||
runData: {
|
runData: {
|
||||||
count: number;
|
outputMap: ExecutionOutputMap;
|
||||||
|
iterations: number;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
};
|
};
|
||||||
render: CanvasNodeDefaultRender | CanvasNodeStickyNoteRender | CanvasNodeAddNodesRender;
|
render: CanvasNodeDefaultRender | CanvasNodeStickyNoteRender | CanvasNodeAddNodesRender;
|
||||||
|
@ -163,11 +164,24 @@ export interface CanvasNodeHandleInjectionData {
|
||||||
label: Ref<string | undefined>;
|
label: Ref<string | undefined>;
|
||||||
mode: Ref<CanvasConnectionMode>;
|
mode: Ref<CanvasConnectionMode>;
|
||||||
type: Ref<NodeConnectionType>;
|
type: Ref<NodeConnectionType>;
|
||||||
|
index: Ref<number>;
|
||||||
isConnected: Ref<boolean | undefined>;
|
isConnected: Ref<boolean | undefined>;
|
||||||
isConnecting: Ref<boolean | undefined>;
|
isConnecting: Ref<boolean | undefined>;
|
||||||
isReadOnly: Ref<boolean | undefined>;
|
isReadOnly: Ref<boolean | undefined>;
|
||||||
|
runData: Ref<ExecutionOutputMapData | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConnectStartEvent = { handleId: string; handleType: string; nodeId: string };
|
export type ConnectStartEvent = { handleId: string; handleType: string; nodeId: string };
|
||||||
|
|
||||||
export type CanvasNodeMoveEvent = { id: string; position: CanvasNode['position'] };
|
export type CanvasNodeMoveEvent = { id: string; position: CanvasNode['position'] };
|
||||||
|
|
||||||
|
export type ExecutionOutputMapData = {
|
||||||
|
total: number;
|
||||||
|
iterations: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExecutionOutputMap = {
|
||||||
|
[connectionType: string]: {
|
||||||
|
[outputIndex: string]: ExecutionOutputMapData;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in a new issue