mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
feat(editor): Add support for configurable configuration nodes in new canvas (no-changelog) (#9991)
This commit is contained in:
parent
d2ca8b4b42
commit
a45899765c
|
@ -14,7 +14,10 @@ export function createCanvasNodeData({
|
|||
issues = { items: [], visible: false },
|
||||
pinnedData = { count: 0, visible: false },
|
||||
runData = { count: 0, visible: false },
|
||||
renderType = 'default',
|
||||
render = {
|
||||
type: 'default',
|
||||
options: { configurable: false, configuration: false, trigger: false },
|
||||
},
|
||||
}: Partial<CanvasElementData> = {}): CanvasElementData {
|
||||
return {
|
||||
execution,
|
||||
|
@ -28,7 +31,7 @@ export function createCanvasNodeData({
|
|||
inputs,
|
||||
outputs,
|
||||
connections,
|
||||
renderType,
|
||||
render,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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 () => {
|
||||
|
@ -30,14 +30,17 @@ describe('CanvasNodeRenderer', () => {
|
|||
provide: {
|
||||
...createCanvasNodeProvide({
|
||||
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 () => {
|
||||
|
@ -46,13 +49,16 @@ describe('CanvasNodeRenderer', () => {
|
|||
provide: {
|
||||
...createCanvasNodeProvide({
|
||||
data: {
|
||||
renderType: 'configurable',
|
||||
render: {
|
||||
type: 'default',
|
||||
options: { configurable: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByTestId('canvas-node-configurable')).toBeInTheDocument();
|
||||
expect(getByTestId('canvas-configurable-node')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<script lang="ts" setup>
|
||||
import { h, inject } from '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';
|
||||
|
||||
const node = inject(CanvasNodeKey);
|
||||
|
@ -13,19 +11,8 @@ const slots = defineSlots<{
|
|||
|
||||
const Render = () => {
|
||||
let Component;
|
||||
switch (node?.data.value.renderType) {
|
||||
case 'configurable':
|
||||
Component = CanvasNodeConfigurable;
|
||||
break;
|
||||
|
||||
case 'configuration':
|
||||
Component = CanvasNodeConfiguration;
|
||||
break;
|
||||
|
||||
case 'trigger':
|
||||
Component = CanvasNodeDefault;
|
||||
break;
|
||||
|
||||
switch (node?.data.value.render.type) {
|
||||
// @TODO Add support for sticky notes here
|
||||
default:
|
||||
Component = CanvasNodeDefault;
|
||||
}
|
||||
|
|
|
@ -24,7 +24,10 @@ describe('CanvasNodeToolbar', () => {
|
|||
provide: {
|
||||
...createCanvasNodeProvide({
|
||||
data: {
|
||||
renderType: 'configuration',
|
||||
render: {
|
||||
type: 'default',
|
||||
options: { configuration: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, inject, useCssModule } from 'vue';
|
||||
import { CanvasNodeKey } from '@/constants';
|
||||
import { useCssModule } from 'vue';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||
|
||||
const emit = defineEmits<{
|
||||
delete: [];
|
||||
|
@ -11,9 +11,8 @@ const emit = defineEmits<{
|
|||
|
||||
const $style = useCssModule();
|
||||
const i18n = useI18n();
|
||||
const node = inject(CanvasNodeKey);
|
||||
|
||||
const data = computed(() => node?.data.value);
|
||||
const { renderOptions } = useCanvasNode();
|
||||
|
||||
// @TODO
|
||||
const workflowRunning = false;
|
||||
|
@ -41,7 +40,7 @@ function openContextMenu(_e: MouseEvent, _type: string) {}
|
|||
<div :class="$style.canvasNodeToolbar">
|
||||
<div :class="$style.canvasNodeToolbarItems">
|
||||
<N8nIconButton
|
||||
v-if="data?.renderType !== 'configuration'"
|
||||
v-if="!renderOptions.configuration"
|
||||
data-test-id="execute-node-button"
|
||||
type="tertiary"
|
||||
text
|
||||
|
|
|
@ -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' });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
|
@ -14,7 +14,7 @@ beforeEach(() => {
|
|||
|
||||
describe('CanvasNodeDefault', () => {
|
||||
it('should render node correctly', () => {
|
||||
const { getByText } = renderComponent({
|
||||
const { getByTestId } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide(),
|
||||
|
@ -22,7 +22,7 @@ describe('CanvasNodeDefault', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(getByText('Test Node')).toBeInTheDocument();
|
||||
expect(getByTestId('canvas-default-node')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('outputs', () => {
|
||||
|
@ -40,7 +40,7 @@ describe('CanvasNodeDefault', () => {
|
|||
});
|
||||
|
||||
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)', () => {
|
||||
|
@ -61,7 +61,7 @@ describe('CanvasNodeDefault', () => {
|
|||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@ 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';
|
||||
import { NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS } from '@/constants';
|
||||
|
||||
const $style = useCssModule();
|
||||
const i18n = useI18n();
|
||||
|
@ -20,8 +21,9 @@ const {
|
|||
executionRunning,
|
||||
hasRunData,
|
||||
hasIssues,
|
||||
renderOptions,
|
||||
} = useCanvasNode();
|
||||
const { mainOutputs } = useNodeConnections({
|
||||
const { mainOutputs, nonMainInputs, requiredNonMainInputs } = useNodeConnections({
|
||||
inputs,
|
||||
outputs,
|
||||
connections,
|
||||
|
@ -36,18 +38,47 @@ const classes = computed(() => {
|
|||
[$style.error]: hasIssues.value,
|
||||
[$style.pinned]: hasPinnedData.value,
|
||||
[$style.running]: executionRunning.value,
|
||||
[$style.configurable]: renderOptions.value.configurable,
|
||||
[$style.configuration]: renderOptions.value.configuration,
|
||||
[$style.trigger]: renderOptions.value.trigger,
|
||||
};
|
||||
});
|
||||
|
||||
const styles = computed(() => {
|
||||
return {
|
||||
'--node-main-output-count': mainOutputs.value.length,
|
||||
};
|
||||
const stylesObject: Record<string, string | number> = {};
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div :class="classes" :style="styles" data-test-id="canvas-node-default">
|
||||
<div :class="classes" :style="styles" :data-test-id="dataTestId">
|
||||
<slot />
|
||||
<CanvasNodeStatusIcons :class="$style.statusIcons" />
|
||||
<CanvasNodeDisabledStrikeThrough v-if="isDisabled" />
|
||||
|
@ -60,8 +91,12 @@ const styles = computed(() => {
|
|||
|
||||
<style lang="scss" module>
|
||||
.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;
|
||||
--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);
|
||||
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-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
|
||||
* The reverse order defines the priority in case multiple states are active
|
||||
|
|
|
@ -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>
|
||||
`;
|
|
@ -114,7 +114,14 @@ describe('useCanvasMapping', () => {
|
|||
input: {},
|
||||
output: {},
|
||||
},
|
||||
renderType: 'trigger',
|
||||
render: {
|
||||
type: 'default',
|
||||
options: {
|
||||
configurable: false,
|
||||
configuration: false,
|
||||
trigger: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -44,21 +44,18 @@ export function useCanvasMapping({
|
|||
|
||||
const renderTypeByNodeType = computed(
|
||||
() =>
|
||||
workflow.value.nodes.reduce<Record<string, CanvasElementData['renderType']>>((acc, node) => {
|
||||
let renderType: CanvasElementData['renderType'] = 'default';
|
||||
switch (true) {
|
||||
case nodeTypesStore.isTriggerNode(node.type):
|
||||
renderType = 'trigger';
|
||||
break;
|
||||
case nodeTypesStore.isConfigNode(workflowObject.value, node, node.type):
|
||||
renderType = 'configuration';
|
||||
break;
|
||||
case nodeTypesStore.isConfigurableNode(workflowObject.value, node, node.type):
|
||||
renderType = 'configurable';
|
||||
break;
|
||||
}
|
||||
workflow.value.nodes.reduce<Record<string, CanvasElementData['render']>>((acc, node) => {
|
||||
// @TODO Add support for sticky notes here
|
||||
|
||||
acc[node.type] = {
|
||||
type: 'default',
|
||||
options: {
|
||||
trigger: nodeTypesStore.isTriggerNode(node.type),
|
||||
configuration: nodeTypesStore.isConfigNode(workflowObject.value, node, node.type),
|
||||
configurable: nodeTypesStore.isConfigurableNode(workflowObject.value, node, node.type),
|
||||
},
|
||||
};
|
||||
|
||||
acc[node.type] = renderType;
|
||||
return acc;
|
||||
}, {}) ?? {},
|
||||
);
|
||||
|
@ -234,7 +231,7 @@ export function useCanvasMapping({
|
|||
count: nodeExecutionRunDataById.value[node.id]?.length ?? 0,
|
||||
visible: !!nodeExecutionRunDataById.value[node.id],
|
||||
},
|
||||
renderType: renderTypeByNodeType.value[node.type] ?? 'default',
|
||||
render: renderTypeByNodeType.value[node.type] ?? { type: 'default', options: {} },
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
|
@ -29,6 +29,7 @@ describe('useCanvasNode', () => {
|
|||
expect(result.executionStatus.value).toBeUndefined();
|
||||
expect(result.executionWaiting.value).toBeUndefined();
|
||||
expect(result.executionRunning.value).toBe(false);
|
||||
expect(result.renderOptions.value).toEqual({});
|
||||
});
|
||||
|
||||
it('should return node data when node is provided', () => {
|
||||
|
@ -45,7 +46,14 @@ describe('useCanvasNode', () => {
|
|||
execution: { status: 'running', waiting: 'waiting', running: true },
|
||||
runData: { count: 1, visible: true },
|
||||
pinnedData: { count: 1, visible: true },
|
||||
renderType: 'default',
|
||||
render: {
|
||||
type: 'default',
|
||||
options: {
|
||||
configurable: false,
|
||||
configuration: false,
|
||||
trigger: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
id: ref('1'),
|
||||
label: ref('Node 1'),
|
||||
|
@ -71,5 +79,6 @@ describe('useCanvasNode', () => {
|
|||
expect(result.executionStatus.value).toBe('running');
|
||||
expect(result.executionWaiting.value).toBe('waiting');
|
||||
expect(result.executionRunning.value).toBe(true);
|
||||
expect(result.renderOptions.value).toBe(node.data.value.render.options);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,7 +25,10 @@ export function useCanvasNode() {
|
|||
running: 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 hasRunData = computed(() => data.value.runData.visible);
|
||||
|
||||
const renderOptions = computed(() => data.value.render.options);
|
||||
|
||||
return {
|
||||
node,
|
||||
label,
|
||||
|
@ -69,5 +74,6 @@ export function useCanvasNode() {
|
|||
executionStatus,
|
||||
executionWaiting,
|
||||
executionRunning,
|
||||
renderOptions,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -64,7 +64,10 @@ export interface CanvasElementData {
|
|||
count: number;
|
||||
visible: boolean;
|
||||
};
|
||||
renderType: 'default' | 'trigger' | 'configuration' | 'configurable';
|
||||
render: {
|
||||
type: 'default';
|
||||
options: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
export type CanvasElement = Node<CanvasElementData>;
|
||||
|
|
Loading…
Reference in a new issue