mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -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 },
|
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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,10 @@ describe('CanvasNodeToolbar', () => {
|
||||||
provide: {
|
provide: {
|
||||||
...createCanvasNodeProvide({
|
...createCanvasNodeProvide({
|
||||||
data: {
|
data: {
|
||||||
renderType: 'configuration',
|
render: {
|
||||||
|
type: 'default',
|
||||||
|
options: { configuration: true },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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', () => {
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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: {},
|
input: {},
|
||||||
output: {},
|
output: {},
|
||||||
},
|
},
|
||||||
renderType: 'trigger',
|
render: {
|
||||||
|
type: 'default',
|
||||||
|
options: {
|
||||||
|
configurable: false,
|
||||||
|
configuration: false,
|
||||||
|
trigger: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
Loading…
Reference in a new issue