feat(editor): Add support for configurable configuration nodes in new canvas (no-changelog) (#9991)

This commit is contained in:
Alex Grozav 2024-07-10 17:51:18 +03:00 committed by GitHub
parent d2ca8b4b42
commit a45899765c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 318 additions and 457 deletions

View file

@ -14,7 +14,10 @@ export function createCanvasNodeData({
issues = { items: [], visible: false }, issues = { items: [], visible: false },
pinnedData = { count: 0, visible: false }, pinnedData = { count: 0, visible: false },
runData = { count: 0, visible: false }, runData = { count: 0, visible: false },
renderType = 'default', render = {
type: 'default',
options: { configurable: false, configuration: false, trigger: false },
},
}: Partial<CanvasElementData> = {}): CanvasElementData { }: Partial<CanvasElementData> = {}): CanvasElementData {
return { return {
execution, execution,
@ -28,7 +31,7 @@ export function createCanvasNodeData({
inputs, inputs,
outputs, outputs,
connections, connections,
renderType, render,
}; };
} }

View file

@ -21,7 +21,7 @@ describe('CanvasNodeRenderer', () => {
}, },
}); });
expect(getByTestId('canvas-node-default')).toBeInTheDocument(); expect(getByTestId('canvas-default-node')).toBeInTheDocument();
}); });
it('should render configuration node correctly', async () => { it('should render configuration node correctly', async () => {
@ -30,14 +30,17 @@ describe('CanvasNodeRenderer', () => {
provide: { provide: {
...createCanvasNodeProvide({ ...createCanvasNodeProvide({
data: { data: {
renderType: 'configuration', render: {
type: 'default',
options: { configuration: true },
},
}, },
}), }),
}, },
}, },
}); });
expect(getByTestId('canvas-node-configuration')).toBeInTheDocument(); expect(getByTestId('canvas-configuration-node')).toBeInTheDocument();
}); });
it('should render configurable node correctly', async () => { it('should render configurable node correctly', async () => {
@ -46,13 +49,16 @@ describe('CanvasNodeRenderer', () => {
provide: { provide: {
...createCanvasNodeProvide({ ...createCanvasNodeProvide({
data: { data: {
renderType: 'configurable', render: {
type: 'default',
options: { configurable: true },
},
}, },
}), }),
}, },
}, },
}); });
expect(getByTestId('canvas-node-configurable')).toBeInTheDocument(); expect(getByTestId('canvas-configurable-node')).toBeInTheDocument();
}); });
}); });

View file

@ -1,8 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { h, inject } from 'vue'; import { h, inject } from 'vue';
import CanvasNodeDefault from '@/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue'; import CanvasNodeDefault from '@/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue';
import CanvasNodeConfiguration from '@/components/canvas/elements/nodes/render-types/CanvasNodeConfiguration.vue';
import CanvasNodeConfigurable from '@/components/canvas/elements/nodes/render-types/CanvasNodeConfigurable.vue';
import { CanvasNodeKey } from '@/constants'; import { CanvasNodeKey } from '@/constants';
const node = inject(CanvasNodeKey); const node = inject(CanvasNodeKey);
@ -13,19 +11,8 @@ const slots = defineSlots<{
const Render = () => { const Render = () => {
let Component; let Component;
switch (node?.data.value.renderType) { switch (node?.data.value.render.type) {
case 'configurable': // @TODO Add support for sticky notes here
Component = CanvasNodeConfigurable;
break;
case 'configuration':
Component = CanvasNodeConfiguration;
break;
case 'trigger':
Component = CanvasNodeDefault;
break;
default: default:
Component = CanvasNodeDefault; Component = CanvasNodeDefault;
} }

View file

@ -24,7 +24,10 @@ describe('CanvasNodeToolbar', () => {
provide: { provide: {
...createCanvasNodeProvide({ ...createCanvasNodeProvide({
data: { data: {
renderType: 'configuration', render: {
type: 'default',
options: { configuration: true },
},
}, },
}), }),
}, },

View file

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject, useCssModule } from 'vue'; import { useCssModule } from 'vue';
import { CanvasNodeKey } from '@/constants';
import { useI18n } from '@/composables/useI18n'; import { useI18n } from '@/composables/useI18n';
import { useCanvasNode } from '@/composables/useCanvasNode';
const emit = defineEmits<{ const emit = defineEmits<{
delete: []; delete: [];
@ -11,9 +11,8 @@ const emit = defineEmits<{
const $style = useCssModule(); const $style = useCssModule();
const i18n = useI18n(); const i18n = useI18n();
const node = inject(CanvasNodeKey);
const data = computed(() => node?.data.value); const { renderOptions } = useCanvasNode();
// @TODO // @TODO
const workflowRunning = false; const workflowRunning = false;
@ -41,7 +40,7 @@ function openContextMenu(_e: MouseEvent, _type: string) {}
<div :class="$style.canvasNodeToolbar"> <div :class="$style.canvasNodeToolbar">
<div :class="$style.canvasNodeToolbarItems"> <div :class="$style.canvasNodeToolbarItems">
<N8nIconButton <N8nIconButton
v-if="data?.renderType !== 'configuration'" v-if="!renderOptions.configuration"
data-test-id="execute-node-button" data-test-id="execute-node-button"
type="tertiary" type="tertiary"
text text

View file

@ -1,107 +0,0 @@
import CanvasNodeConfigurable from '@/components/canvas/elements/nodes/render-types/CanvasNodeConfigurable.vue';
import { createComponentRenderer } from '@/__tests__/render';
import { NodeConnectionType } from 'n8n-workflow';
import { createCanvasNodeProvide } from '@/__tests__/data';
import { setActivePinia } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
const renderComponent = createComponentRenderer(CanvasNodeConfigurable);
beforeEach(() => {
const pinia = createTestingPinia();
setActivePinia(pinia);
});
describe('CanvasNodeConfigurable', () => {
it('should render node correctly', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide(),
},
},
});
expect(getByText('Test Node')).toBeInTheDocument();
});
describe('selected', () => {
it('should apply selected class when node is selected', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide({
selected: true,
}),
},
},
});
expect(getByText('Test Node').closest('.node')).toHaveClass('selected');
});
it('should not apply selected class when node is not selected', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide(),
},
},
});
expect(getByText('Test Node').closest('.node')).not.toHaveClass('selected');
});
});
describe('disabled', () => {
it('should apply disabled class when node is disabled', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide({
data: {
disabled: true,
},
}),
},
},
});
expect(getByText('Test Node').closest('.node')).toHaveClass('disabled');
expect(getByText('(Deactivated)')).toBeVisible();
});
it('should not apply disabled class when node is enabled', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide(),
},
},
});
expect(getByText('Test Node').closest('.node')).not.toHaveClass('disabled');
});
});
describe('inputs', () => {
it('should adjust width css variable based on the number of non-main inputs', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide({
data: {
inputs: [
{ type: NodeConnectionType.Main, index: 0 },
{ type: NodeConnectionType.AiTool, index: 0 },
{ type: NodeConnectionType.AiDocument, index: 0, required: true },
{ type: NodeConnectionType.AiMemory, index: 0, required: true },
],
},
}),
},
},
});
const nodeElement = getByText('Test Node').closest('.node');
expect(nodeElement).toHaveStyle({ '--configurable-node-input-count': '3' });
});
});
});

View file

@ -1,134 +0,0 @@
<script lang="ts" setup>
import { computed, useCssModule } from 'vue';
import { NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS } from '@/constants';
import { useNodeConnections } from '@/composables/useNodeConnections';
import { useI18n } from '@/composables/useI18n';
import CanvasNodeDisabledStrikeThrough from './parts/CanvasNodeDisabledStrikeThrough.vue';
import CanvasNodeStatusIcons from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue';
import { useCanvasNode } from '@/composables/useCanvasNode';
const $style = useCssModule();
const i18n = useI18n();
const {
label,
inputs,
outputs,
connections,
isDisabled,
isSelected,
hasPinnedData,
hasRunData,
hasIssues,
} = useCanvasNode();
const { nonMainInputs, requiredNonMainInputs } = useNodeConnections({
inputs,
outputs,
connections,
});
const classes = computed(() => {
return {
[$style.node]: true,
[$style.selected]: isSelected.value,
[$style.disabled]: isDisabled.value,
[$style.success]: hasRunData.value,
[$style.error]: hasIssues.value,
[$style.pinned]: hasPinnedData.value,
};
});
const styles = computed(() => {
const stylesObject: {
[key: string]: string | number;
} = {};
if (requiredNonMainInputs.value.length > 0) {
let spacerCount = 0;
if (NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS) {
const requiredNonMainInputsCount = requiredNonMainInputs.value.length;
const optionalNonMainInputsCount = nonMainInputs.value.length - requiredNonMainInputsCount;
spacerCount = requiredNonMainInputsCount > 0 && optionalNonMainInputsCount > 0 ? 1 : 0;
}
stylesObject['--configurable-node-input-count'] = nonMainInputs.value.length + spacerCount;
}
return stylesObject;
});
</script>
<template>
<div :class="classes" :style="styles" data-test-id="canvas-node-configurable">
<slot />
<CanvasNodeStatusIcons :class="$style.statusIcons" />
<CanvasNodeDisabledStrikeThrough v-if="isDisabled" />
<div :class="$style.label">
{{ label }}
<div v-if="isDisabled">({{ i18n.baseText('node.disabled') }})</div>
</div>
</div>
</template>
<style lang="scss" module>
.node {
--configurable-node-min-input-count: 4;
--configurable-node-input-width: 65px;
--canvas-node--height: 100px;
--canvas-node--width: calc(
max(var(--configurable-node-input-count, 5), var(--configurable-node-min-input-count)) *
var(--configurable-node-input-width)
);
width: var(--canvas-node--width);
height: var(--canvas-node--height);
display: flex;
align-items: center;
justify-content: center;
background: var(--canvas-node--background, var(--color-node-background));
border: 2px solid var(--canvas-node--border-color, var(--color-foreground-xdark));
border-radius: var(--border-radius-large);
&.selected {
box-shadow: 0 0 0 4px var(--color-canvas-selected);
}
/**
* State classes
* The reverse order defines the priority in case multiple states are active
*/
&.error {
border-color: var(--color-canvas-node-error-border-color, var(--color-danger));
}
&.success {
border-color: var(--color-canvas-node-success-border-color, var(--color-success));
}
&.pinned {
border-color: var(--color-canvas-node-pinned-border, var(--color-node-pinned-border));
}
&.disabled {
border-color: var(--color-canvas-node-disabled-border, var(--color-foreground-base));
}
}
.label {
top: 100%;
font-size: var(--font-size-m);
text-align: center;
margin-left: var(--spacing-s);
max-width: calc(
var(--node-width) - var(--configurable-node-icon-offset) - var(--configurable-node-icon-size) -
2 * var(--spacing-s)
);
}
.statusIcons {
position: absolute;
top: calc(var(--canvas-node--height) - 24px);
right: var(--spacing-xs);
}
</style>

View file

@ -1,80 +0,0 @@
import CanvasNodeConfiguration from '@/components/canvas/elements/nodes/render-types/CanvasNodeConfiguration.vue';
import { createComponentRenderer } from '@/__tests__/render';
import { createCanvasNodeProvide } from '@/__tests__/data';
import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia';
const renderComponent = createComponentRenderer(CanvasNodeConfiguration);
beforeEach(() => {
const pinia = createTestingPinia();
setActivePinia(pinia);
});
describe('CanvasNodeConfiguration', () => {
it('should render node correctly', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide(),
},
},
});
expect(getByText('Test Node')).toBeInTheDocument();
});
describe('selected', () => {
it('should apply selected class when node is selected', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide({ selected: true }),
},
},
});
expect(getByText('Test Node').closest('.node')).toHaveClass('selected');
});
it('should not apply selected class when node is not selected', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide(),
},
},
});
expect(getByText('Test Node').closest('.node')).not.toHaveClass('selected');
});
});
describe('disabled', () => {
it('should apply disabled class when node is disabled', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide({
data: {
disabled: true,
},
}),
},
},
});
expect(getByText('Test Node').closest('.node')).toHaveClass('disabled');
expect(getByText('(Deactivated)')).toBeVisible();
});
it('should not apply disabled class when node is enabled', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide(),
},
},
});
expect(getByText('Test Node').closest('.node')).not.toHaveClass('disabled');
});
});
});

View file

@ -1,79 +0,0 @@
<script lang="ts" setup>
import { computed, useCssModule } from 'vue';
import { useI18n } from '@/composables/useI18n';
import CanvasNodeStatusIcons from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue';
import { useCanvasNode } from '@/composables/useCanvasNode';
const $style = useCssModule();
const i18n = useI18n();
const { label, isDisabled, isSelected, hasIssues } = useCanvasNode();
const classes = computed(() => {
return {
[$style.node]: true,
[$style.selected]: isSelected.value,
[$style.disabled]: isDisabled.value,
[$style.error]: hasIssues.value,
};
});
</script>
<template>
<div :class="classes" data-test-id="canvas-node-configuration">
<slot />
<CanvasNodeStatusIcons :class="$style.statusIcons" />
<div v-if="label" :class="$style.label">
{{ label }}
<div v-if="isDisabled">({{ i18n.baseText('node.disabled') }})</div>
</div>
</div>
</template>
<style lang="scss" module>
.node {
--canvas-node--width: 75px;
--canvas-node--height: 75px;
width: var(--canvas-node--width);
height: var(--canvas-node--height);
display: flex;
align-items: center;
justify-content: center;
background: var(--canvas-node--background, var(--node-type-supplemental-background));
border: 2px solid var(--canvas-node--border-color, var(--color-foreground-dark));
border-radius: 50%;
/**
* State classes
* The reverse order defines the priority in case multiple states are active
*/
&.selected {
box-shadow: 0 0 0 4px var(--color-canvas-selected);
}
&.error {
border-color: var(--color-canvas-node-error-border-color, var(--color-danger));
}
&.disabled {
border-color: var(--color-canvas-node-disabled-border, var(--color-foreground-base));
}
}
.label {
top: 100%;
position: absolute;
font-size: var(--font-size-m);
text-align: center;
width: 100%;
min-width: 200px;
margin-top: var(--spacing-2xs);
}
.statusIcons {
position: absolute;
top: calc(var(--canvas-node--height) - 24px);
}
</style>

View file

@ -14,7 +14,7 @@ beforeEach(() => {
describe('CanvasNodeDefault', () => { describe('CanvasNodeDefault', () => {
it('should render node correctly', () => { it('should render node correctly', () => {
const { getByText } = renderComponent({ const { getByTestId } = renderComponent({
global: { global: {
provide: { provide: {
...createCanvasNodeProvide(), ...createCanvasNodeProvide(),
@ -22,7 +22,7 @@ describe('CanvasNodeDefault', () => {
}, },
}); });
expect(getByText('Test Node')).toBeInTheDocument(); expect(getByTestId('canvas-default-node')).toMatchSnapshot();
}); });
describe('outputs', () => { describe('outputs', () => {
@ -40,7 +40,7 @@ describe('CanvasNodeDefault', () => {
}); });
const nodeElement = getByText('Test Node').closest('.node'); const nodeElement = getByText('Test Node').closest('.node');
expect(nodeElement).toHaveStyle({ '--node-main-output-count': '1' }); // height calculation based on the number of outputs expect(nodeElement).toHaveStyle({ '--canvas-node--main-output-count': '1' }); // height calculation based on the number of outputs
}); });
it('should adjust height css variable based on the number of outputs (multiple outputs)', () => { it('should adjust height css variable based on the number of outputs (multiple outputs)', () => {
@ -61,7 +61,7 @@ describe('CanvasNodeDefault', () => {
}); });
const nodeElement = getByText('Test Node').closest('.node'); const nodeElement = getByText('Test Node').closest('.node');
expect(nodeElement).toHaveStyle({ '--node-main-output-count': '3' }); // height calculation based on the number of outputs expect(nodeElement).toHaveStyle({ '--canvas-node--main-output-count': '3' }); // height calculation based on the number of outputs
}); });
}); });
@ -131,4 +131,95 @@ describe('CanvasNodeDefault', () => {
expect(getByText('Test Node').closest('.node')).toHaveClass('running'); expect(getByText('Test Node').closest('.node')).toHaveClass('running');
}); });
}); });
describe('configurable', () => {
it('should render configurable node correctly', () => {
const { getByTestId } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide({
data: {
render: {
type: 'default',
options: { configurable: true },
},
},
}),
},
},
});
expect(getByTestId('canvas-configurable-node')).toMatchSnapshot();
});
describe('inputs', () => {
it('should adjust width css variable based on the number of non-main inputs', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide({
data: {
inputs: [
{ type: NodeConnectionType.Main, index: 0 },
{ type: NodeConnectionType.AiTool, index: 0 },
{ type: NodeConnectionType.AiDocument, index: 0, required: true },
{ type: NodeConnectionType.AiMemory, index: 0, required: true },
],
render: {
type: 'default',
options: {
configurable: true,
},
},
},
}),
},
},
});
const nodeElement = getByText('Test Node').closest('.node');
expect(nodeElement).toHaveStyle({ '--configurable-node--input-count': '3' });
});
});
});
describe('configuration', () => {
it('should render configuration node correctly', () => {
const { getByTestId } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide({
data: {
render: {
type: 'default',
options: { configuration: true },
},
},
}),
},
},
});
expect(getByTestId('canvas-configuration-node')).toMatchSnapshot();
});
it('should render configurable configuration node correctly', () => {
const { getByTestId } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide({
data: {
render: {
type: 'default',
options: { configurable: true, configuration: true },
},
},
}),
},
},
});
expect(getByTestId('canvas-configurable-node')).toMatchSnapshot();
});
});
}); });

View file

@ -5,6 +5,7 @@ import { useI18n } from '@/composables/useI18n';
import CanvasNodeDisabledStrikeThrough from './parts/CanvasNodeDisabledStrikeThrough.vue'; import CanvasNodeDisabledStrikeThrough from './parts/CanvasNodeDisabledStrikeThrough.vue';
import CanvasNodeStatusIcons from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue'; import CanvasNodeStatusIcons from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue';
import { useCanvasNode } from '@/composables/useCanvasNode'; import { useCanvasNode } from '@/composables/useCanvasNode';
import { NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS } from '@/constants';
const $style = useCssModule(); const $style = useCssModule();
const i18n = useI18n(); const i18n = useI18n();
@ -20,8 +21,9 @@ const {
executionRunning, executionRunning,
hasRunData, hasRunData,
hasIssues, hasIssues,
renderOptions,
} = useCanvasNode(); } = useCanvasNode();
const { mainOutputs } = useNodeConnections({ const { mainOutputs, nonMainInputs, requiredNonMainInputs } = useNodeConnections({
inputs, inputs,
outputs, outputs,
connections, connections,
@ -36,18 +38,47 @@ const classes = computed(() => {
[$style.error]: hasIssues.value, [$style.error]: hasIssues.value,
[$style.pinned]: hasPinnedData.value, [$style.pinned]: hasPinnedData.value,
[$style.running]: executionRunning.value, [$style.running]: executionRunning.value,
[$style.configurable]: renderOptions.value.configurable,
[$style.configuration]: renderOptions.value.configuration,
[$style.trigger]: renderOptions.value.trigger,
}; };
}); });
const styles = computed(() => { const styles = computed(() => {
return { const stylesObject: Record<string, string | number> = {};
'--node-main-output-count': mainOutputs.value.length,
}; if (renderOptions.value.configurable && requiredNonMainInputs.value.length > 0) {
let spacerCount = 0;
if (NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS) {
const requiredNonMainInputsCount = requiredNonMainInputs.value.length;
const optionalNonMainInputsCount = nonMainInputs.value.length - requiredNonMainInputsCount;
spacerCount = requiredNonMainInputsCount > 0 && optionalNonMainInputsCount > 0 ? 1 : 0;
}
stylesObject['--configurable-node--input-count'] = nonMainInputs.value.length + spacerCount;
}
stylesObject['--canvas-node--main-output-count'] = mainOutputs.value.length;
return stylesObject;
});
const dataTestId = computed(() => {
let type = 'default';
if (renderOptions.value.configurable) {
type = 'configurable';
} else if (renderOptions.value.configuration) {
type = 'configuration';
} else if (renderOptions.value.trigger) {
type = 'trigger';
}
return `canvas-${type}-node`;
}); });
</script> </script>
<template> <template>
<div :class="classes" :style="styles" data-test-id="canvas-node-default"> <div :class="classes" :style="styles" :data-test-id="dataTestId">
<slot /> <slot />
<CanvasNodeStatusIcons :class="$style.statusIcons" /> <CanvasNodeStatusIcons :class="$style.statusIcons" />
<CanvasNodeDisabledStrikeThrough v-if="isDisabled" /> <CanvasNodeDisabledStrikeThrough v-if="isDisabled" />
@ -60,8 +91,12 @@ const styles = computed(() => {
<style lang="scss" module> <style lang="scss" module>
.node { .node {
--canvas-node--height: calc(100px + max(0, var(--node-main-output-count, 1) - 4) * 50px); --canvas-node--height: calc(100px + max(0, var(--canvas-node--main-output-count, 1) - 4) * 50px);
--canvas-node--width: 100px; --canvas-node--width: 100px;
--configurable-node--min-input-count: 4;
--configurable-node--input-width: 65px;
--configurable-node--icon-offset: 40px;
--configurable-node--icon-size: 30px;
height: var(--canvas-node--height); height: var(--canvas-node--height);
width: var(--canvas-node--width); width: var(--canvas-node--width);
@ -72,6 +107,44 @@ const styles = computed(() => {
border: 2px solid var(--canvas-node--border-color, var(--color-foreground-xdark)); border: 2px solid var(--canvas-node--border-color, var(--color-foreground-xdark));
border-radius: var(--border-radius-large); border-radius: var(--border-radius-large);
/**
* Node types
*/
&.configuration {
--canvas-node--width: 75px;
--canvas-node--height: 75px;
background: var(--canvas-node--background, var(--node-type-supplemental-background));
border: 2px solid var(--canvas-node--border-color, var(--color-foreground-dark));
border-radius: 50px;
.statusIcons {
right: unset;
}
}
&.configurable {
--canvas-node--height: 100px;
--canvas-node--width: calc(
max(var(--configurable-node--input-count, 5), var(--configurable-node--min-input-count)) *
var(--configurable-node--input-width)
);
.label {
top: unset;
position: relative;
margin-left: var(--spacing-s);
width: auto;
min-width: unset;
max-width: calc(
var(--canvas-node--width) - var(--configurable-node--icon-offset) - var(
--configurable-node--icon-size
) - 2 * var(--spacing-s)
);
}
}
/** /**
* State classes * State classes
* The reverse order defines the priority in case multiple states are active * The reverse order defines the priority in case multiple states are active

View file

@ -0,0 +1,77 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`CanvasNodeDefault > configurable > should render configurable node correctly 1`] = `
<div
class="node configurable"
data-test-id="canvas-configurable-node"
style="--canvas-node--main-output-count: 0;"
>
<!--v-if-->
<!--v-if-->
<div
class="label"
>
Test Node
<!--v-if-->
</div>
</div>
`;
exports[`CanvasNodeDefault > configuration > should render configurable configuration node correctly 1`] = `
<div
class="node configurable configuration"
data-test-id="canvas-configurable-node"
style="--canvas-node--main-output-count: 0;"
>
<!--v-if-->
<!--v-if-->
<div
class="label"
>
Test Node
<!--v-if-->
</div>
</div>
`;
exports[`CanvasNodeDefault > configuration > should render configuration node correctly 1`] = `
<div
class="node configuration"
data-test-id="canvas-configuration-node"
style="--canvas-node--main-output-count: 0;"
>
<!--v-if-->
<!--v-if-->
<div
class="label"
>
Test Node
<!--v-if-->
</div>
</div>
`;
exports[`CanvasNodeDefault > should render node correctly 1`] = `
<div
class="node"
data-test-id="canvas-default-node"
style="--canvas-node--main-output-count: 0;"
>
<!--v-if-->
<!--v-if-->
<div
class="label"
>
Test Node
<!--v-if-->
</div>
</div>
`;

View file

@ -114,7 +114,14 @@ describe('useCanvasMapping', () => {
input: {}, input: {},
output: {}, output: {},
}, },
renderType: 'trigger', render: {
type: 'default',
options: {
configurable: false,
configuration: false,
trigger: true,
},
},
}, },
}, },
]); ]);

View file

@ -44,21 +44,18 @@ export function useCanvasMapping({
const renderTypeByNodeType = computed( const renderTypeByNodeType = computed(
() => () =>
workflow.value.nodes.reduce<Record<string, CanvasElementData['renderType']>>((acc, node) => { workflow.value.nodes.reduce<Record<string, CanvasElementData['render']>>((acc, node) => {
let renderType: CanvasElementData['renderType'] = 'default'; // @TODO Add support for sticky notes here
switch (true) {
case nodeTypesStore.isTriggerNode(node.type): acc[node.type] = {
renderType = 'trigger'; type: 'default',
break; options: {
case nodeTypesStore.isConfigNode(workflowObject.value, node, node.type): trigger: nodeTypesStore.isTriggerNode(node.type),
renderType = 'configuration'; configuration: nodeTypesStore.isConfigNode(workflowObject.value, node, node.type),
break; configurable: nodeTypesStore.isConfigurableNode(workflowObject.value, node, node.type),
case nodeTypesStore.isConfigurableNode(workflowObject.value, node, node.type): },
renderType = 'configurable'; };
break;
}
acc[node.type] = renderType;
return acc; return acc;
}, {}) ?? {}, }, {}) ?? {},
); );
@ -234,7 +231,7 @@ export function useCanvasMapping({
count: nodeExecutionRunDataById.value[node.id]?.length ?? 0, count: nodeExecutionRunDataById.value[node.id]?.length ?? 0,
visible: !!nodeExecutionRunDataById.value[node.id], visible: !!nodeExecutionRunDataById.value[node.id],
}, },
renderType: renderTypeByNodeType.value[node.type] ?? 'default', render: renderTypeByNodeType.value[node.type] ?? { type: 'default', options: {} },
}; };
return { return {

View file

@ -29,6 +29,7 @@ describe('useCanvasNode', () => {
expect(result.executionStatus.value).toBeUndefined(); expect(result.executionStatus.value).toBeUndefined();
expect(result.executionWaiting.value).toBeUndefined(); expect(result.executionWaiting.value).toBeUndefined();
expect(result.executionRunning.value).toBe(false); expect(result.executionRunning.value).toBe(false);
expect(result.renderOptions.value).toEqual({});
}); });
it('should return node data when node is provided', () => { it('should return node data when node is provided', () => {
@ -45,7 +46,14 @@ describe('useCanvasNode', () => {
execution: { status: 'running', waiting: 'waiting', running: true }, execution: { status: 'running', waiting: 'waiting', running: true },
runData: { count: 1, visible: true }, runData: { count: 1, visible: true },
pinnedData: { count: 1, visible: true }, pinnedData: { count: 1, visible: true },
renderType: 'default', render: {
type: 'default',
options: {
configurable: false,
configuration: false,
trigger: false,
},
},
}), }),
id: ref('1'), id: ref('1'),
label: ref('Node 1'), label: ref('Node 1'),
@ -71,5 +79,6 @@ describe('useCanvasNode', () => {
expect(result.executionStatus.value).toBe('running'); expect(result.executionStatus.value).toBe('running');
expect(result.executionWaiting.value).toBe('waiting'); expect(result.executionWaiting.value).toBe('waiting');
expect(result.executionRunning.value).toBe(true); expect(result.executionRunning.value).toBe(true);
expect(result.renderOptions.value).toBe(node.data.value.render.options);
}); });
}); });

View file

@ -25,7 +25,10 @@ export function useCanvasNode() {
running: false, running: false,
}, },
runData: { count: 0, visible: false }, runData: { count: 0, visible: false },
renderType: 'default', render: {
type: 'default',
options: {},
},
}, },
); );
@ -52,6 +55,8 @@ export function useCanvasNode() {
const runDataCount = computed(() => data.value.runData.count); const runDataCount = computed(() => data.value.runData.count);
const hasRunData = computed(() => data.value.runData.visible); const hasRunData = computed(() => data.value.runData.visible);
const renderOptions = computed(() => data.value.render.options);
return { return {
node, node,
label, label,
@ -69,5 +74,6 @@ export function useCanvasNode() {
executionStatus, executionStatus,
executionWaiting, executionWaiting,
executionRunning, executionRunning,
renderOptions,
}; };
} }

View file

@ -64,7 +64,10 @@ export interface CanvasElementData {
count: number; count: number;
visible: boolean; visible: boolean;
}; };
renderType: 'default' | 'trigger' | 'configuration' | 'configurable'; render: {
type: 'default';
options: Record<string, unknown>;
};
} }
export type CanvasElement = Node<CanvasElementData>; export type CanvasElement = Node<CanvasElementData>;