mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 20:54:07 -08:00
feat(editor): Add support for fallback nodes and new addNodes node render type in new canvas (no-changelog) (#10004)
This commit is contained in:
parent
f9e9d274b9
commit
57dfefd0f6
|
@ -1,6 +1,7 @@
|
||||||
import { CanvasNodeKey } from '@/constants';
|
import { CanvasNodeKey } from '@/constants';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import type { CanvasElement, CanvasElementData } from '@/types';
|
import type { CanvasNode, CanvasNodeData } from '@/types';
|
||||||
|
import { CanvasNodeRenderType } from '@/types';
|
||||||
|
|
||||||
export function createCanvasNodeData({
|
export function createCanvasNodeData({
|
||||||
id = 'node',
|
id = 'node',
|
||||||
|
@ -15,10 +16,10 @@ export function createCanvasNodeData({
|
||||||
pinnedData = { count: 0, visible: false },
|
pinnedData = { count: 0, visible: false },
|
||||||
runData = { count: 0, visible: false },
|
runData = { count: 0, visible: false },
|
||||||
render = {
|
render = {
|
||||||
type: 'default',
|
type: CanvasNodeRenderType.Default,
|
||||||
options: { configurable: false, configuration: false, trigger: false },
|
options: { configurable: false, configuration: false, trigger: false },
|
||||||
},
|
},
|
||||||
}: Partial<CanvasElementData> = {}): CanvasElementData {
|
}: Partial<CanvasNodeData> = {}): CanvasNodeData {
|
||||||
return {
|
return {
|
||||||
execution,
|
execution,
|
||||||
issues,
|
issues,
|
||||||
|
@ -41,9 +42,7 @@ export function createCanvasNodeElement({
|
||||||
label = 'Node',
|
label = 'Node',
|
||||||
position = { x: 100, y: 100 },
|
position = { x: 100, y: 100 },
|
||||||
data,
|
data,
|
||||||
}: Partial<
|
}: Partial<Omit<CanvasNode, 'data'> & { data: Partial<CanvasNodeData> }> = {}): CanvasNode {
|
||||||
Omit<CanvasElement, 'data'> & { data: Partial<CanvasElementData> }
|
|
||||||
> = {}): CanvasElement {
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
type,
|
type,
|
||||||
|
@ -58,7 +57,7 @@ export function createCanvasNodeProps({
|
||||||
label = 'Test Node',
|
label = 'Test Node',
|
||||||
selected = false,
|
selected = false,
|
||||||
data = {},
|
data = {},
|
||||||
}: { id?: string; label?: string; selected?: boolean; data?: Partial<CanvasElementData> } = {}) {
|
}: { id?: string; label?: string; selected?: boolean; data?: Partial<CanvasNodeData> } = {}) {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
label,
|
label,
|
||||||
|
@ -72,7 +71,7 @@ export function createCanvasNodeProvide({
|
||||||
label = 'Test Node',
|
label = 'Test Node',
|
||||||
selected = false,
|
selected = false,
|
||||||
data = {},
|
data = {},
|
||||||
}: { id?: string; label?: string; selected?: boolean; data?: Partial<CanvasElementData> } = {}) {
|
}: { id?: string; label?: string; selected?: boolean; data?: Partial<CanvasNodeData> } = {}) {
|
||||||
const props = createCanvasNodeProps({ id, label, selected, data });
|
const props = createCanvasNodeProps({ id, label, selected, data });
|
||||||
return {
|
return {
|
||||||
[`${CanvasNodeKey}`]: {
|
[`${CanvasNodeKey}`]: {
|
||||||
|
@ -85,8 +84,8 @@ export function createCanvasNodeProvide({
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createCanvasConnection(
|
export function createCanvasConnection(
|
||||||
nodeA: CanvasElement,
|
nodeA: CanvasNode,
|
||||||
nodeB: CanvasElement,
|
nodeB: CanvasNode,
|
||||||
{ sourceIndex = 0, targetIndex = 0 } = {},
|
{ sourceIndex = 0, targetIndex = 0 } = {},
|
||||||
) {
|
) {
|
||||||
const nodeAOutput = nodeA.data?.outputs[sourceIndex];
|
const nodeAOutput = nodeA.data?.outputs[sourceIndex];
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { fireEvent, waitFor } from '@testing-library/vue';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import Canvas from '@/components/canvas/Canvas.vue';
|
import Canvas from '@/components/canvas/Canvas.vue';
|
||||||
import { createPinia, setActivePinia } from 'pinia';
|
import { createPinia, setActivePinia } from 'pinia';
|
||||||
import type { CanvasConnection, CanvasElement } from '@/types';
|
import type { CanvasConnection, CanvasNode } from '@/types';
|
||||||
import { createCanvasConnection, createCanvasNodeElement } from '@/__tests__/data';
|
import { createCanvasConnection, createCanvasNodeElement } from '@/__tests__/data';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ describe('Canvas', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render nodes and edges', async () => {
|
it('should render nodes and edges', async () => {
|
||||||
const elements: CanvasElement[] = [
|
const nodes: CanvasNode[] = [
|
||||||
createCanvasNodeElement({
|
createCanvasNodeElement({
|
||||||
id: '1',
|
id: '1',
|
||||||
label: 'Node 1',
|
label: 'Node 1',
|
||||||
|
@ -72,33 +72,33 @@ describe('Canvas', () => {
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
const connections: CanvasConnection[] = [createCanvasConnection(elements[0], elements[1])];
|
const connections: CanvasConnection[] = [createCanvasConnection(nodes[0], nodes[1])];
|
||||||
|
|
||||||
const { container } = renderComponent({
|
const { container } = renderComponent({
|
||||||
props: {
|
props: {
|
||||||
elements,
|
nodes,
|
||||||
connections,
|
connections,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => expect(container.querySelectorAll('.vue-flow__node')).toHaveLength(2));
|
await waitFor(() => expect(container.querySelectorAll('.vue-flow__node')).toHaveLength(2));
|
||||||
|
|
||||||
expect(container.querySelector(`[data-id="${elements[0].id}"]`)).toBeInTheDocument();
|
expect(container.querySelector(`[data-id="${nodes[0].id}"]`)).toBeInTheDocument();
|
||||||
expect(container.querySelector(`[data-id="${elements[1].id}"]`)).toBeInTheDocument();
|
expect(container.querySelector(`[data-id="${nodes[1].id}"]`)).toBeInTheDocument();
|
||||||
expect(container.querySelector(`[data-id="${connections[0].id}"]`)).toBeInTheDocument();
|
expect(container.querySelector(`[data-id="${connections[0].id}"]`)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle node drag stop event', async () => {
|
it('should handle node drag stop event', async () => {
|
||||||
const elements = [createCanvasNodeElement()];
|
const nodes = [createCanvasNodeElement()];
|
||||||
const { container, emitted } = renderComponent({
|
const { container, emitted } = renderComponent({
|
||||||
props: {
|
props: {
|
||||||
elements,
|
nodes,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => expect(container.querySelectorAll('.vue-flow__node')).toHaveLength(1));
|
await waitFor(() => expect(container.querySelectorAll('.vue-flow__node')).toHaveLength(1));
|
||||||
|
|
||||||
const node = container.querySelector(`[data-id="${elements[0].id}"]`) as Element;
|
const node = container.querySelector(`[data-id="${nodes[0].id}"]`) as Element;
|
||||||
await fireEvent.mouseDown(node, { view: window });
|
await fireEvent.mouseDown(node, { view: window });
|
||||||
await fireEvent.mouseMove(node, {
|
await fireEvent.mouseMove(node, {
|
||||||
view: window,
|
view: window,
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { CanvasConnection, CanvasElement, ConnectStartEvent } from '@/types';
|
import type { CanvasConnection, CanvasNode, ConnectStartEvent } from '@/types';
|
||||||
import type { EdgeMouseEvent, NodeDragEvent, Connection, XYPosition } from '@vue-flow/core';
|
import type { EdgeMouseEvent, NodeDragEvent, Connection, XYPosition } from '@vue-flow/core';
|
||||||
import { useVueFlow, VueFlow, PanelPosition } from '@vue-flow/core';
|
import { useVueFlow, VueFlow, PanelPosition } from '@vue-flow/core';
|
||||||
import { Background } from '@vue-flow/background';
|
import { Background } from '@vue-flow/background';
|
||||||
import { Controls } from '@vue-flow/controls';
|
import { Controls } from '@vue-flow/controls';
|
||||||
import { MiniMap } from '@vue-flow/minimap';
|
import { MiniMap } from '@vue-flow/minimap';
|
||||||
import CanvasNode from './elements/nodes/CanvasNode.vue';
|
import Node from './elements/nodes/CanvasNode.vue';
|
||||||
import CanvasEdge from './elements/edges/CanvasEdge.vue';
|
import Edge from './elements/edges/CanvasEdge.vue';
|
||||||
import { onMounted, onUnmounted, ref, useCssModule } from 'vue';
|
import { onMounted, onUnmounted, ref, useCssModule } from 'vue';
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:modelValue': [elements: CanvasElement[]];
|
'update:modelValue': [elements: CanvasNode[]];
|
||||||
'update:node:position': [id: string, position: XYPosition];
|
'update:node:position': [id: string, position: XYPosition];
|
||||||
'update:node:active': [id: string];
|
'update:node:active': [id: string];
|
||||||
'update:node:enabled': [id: string];
|
'update:node:enabled': [id: string];
|
||||||
|
@ -30,13 +30,13 @@ const emit = defineEmits<{
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
id?: string;
|
id?: string;
|
||||||
elements: CanvasElement[];
|
nodes: CanvasNode[];
|
||||||
connections: CanvasConnection[];
|
connections: CanvasConnection[];
|
||||||
controlsPosition?: PanelPosition;
|
controlsPosition?: PanelPosition;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
id: 'canvas',
|
id: 'canvas',
|
||||||
elements: () => [],
|
nodes: () => [],
|
||||||
connections: () => [],
|
connections: () => [],
|
||||||
controlsPosition: PanelPosition.BottomLeft,
|
controlsPosition: PanelPosition.BottomLeft,
|
||||||
},
|
},
|
||||||
|
@ -156,7 +156,7 @@ function onClickPane(event: MouseEvent) {
|
||||||
<template>
|
<template>
|
||||||
<VueFlow
|
<VueFlow
|
||||||
:id="id"
|
:id="id"
|
||||||
:nodes="elements"
|
:nodes="nodes"
|
||||||
:edges="connections"
|
:edges="connections"
|
||||||
:apply-changes="false"
|
:apply-changes="false"
|
||||||
fit-view-on-init
|
fit-view-on-init
|
||||||
|
@ -176,7 +176,7 @@ function onClickPane(event: MouseEvent) {
|
||||||
@pane-click="onClickPane"
|
@pane-click="onClickPane"
|
||||||
>
|
>
|
||||||
<template #node-canvas-node="canvasNodeProps">
|
<template #node-canvas-node="canvasNodeProps">
|
||||||
<CanvasNode
|
<Node
|
||||||
v-bind="canvasNodeProps"
|
v-bind="canvasNodeProps"
|
||||||
@delete="onDeleteNode"
|
@delete="onDeleteNode"
|
||||||
@run="onRunNode"
|
@run="onRunNode"
|
||||||
|
@ -187,7 +187,7 @@ function onClickPane(event: MouseEvent) {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #edge-canvas-edge="canvasEdgeProps">
|
<template #edge-canvas-edge="canvasEdgeProps">
|
||||||
<CanvasEdge
|
<Edge
|
||||||
v-bind="canvasEdgeProps"
|
v-bind="canvasEdgeProps"
|
||||||
:hovered="hoveredEdges[canvasEdgeProps.id]"
|
:hovered="hoveredEdges[canvasEdgeProps.id]"
|
||||||
@delete="onDeleteConnection"
|
@delete="onDeleteConnection"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Canvas from '@/components/canvas/Canvas.vue';
|
import Canvas from '@/components/canvas/Canvas.vue';
|
||||||
import { toRef, useCssModule } from 'vue';
|
import { computed, toRef, useCssModule } from 'vue';
|
||||||
import type { Workflow } from 'n8n-workflow';
|
import type { Workflow } from 'n8n-workflow';
|
||||||
import type { IWorkflowDb } from '@/Interface';
|
import type { IWorkflowDb } from '@/Interface';
|
||||||
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
||||||
|
@ -9,24 +9,45 @@ defineOptions({
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
id?: string;
|
defineProps<{
|
||||||
workflow: IWorkflowDb;
|
id?: string;
|
||||||
workflowObject: Workflow;
|
workflow: IWorkflowDb;
|
||||||
}>();
|
workflowObject: Workflow;
|
||||||
|
fallbackNodes?: IWorkflowDb['nodes'];
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
id: 'canvas',
|
||||||
|
fallbackNodes: () => [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
|
||||||
const workflow = toRef(props, 'workflow');
|
const workflow = toRef(props, 'workflow');
|
||||||
const workflowObject = toRef(props, 'workflowObject');
|
const workflowObject = toRef(props, 'workflowObject');
|
||||||
|
|
||||||
const { elements, connections } = useCanvasMapping({ workflow, workflowObject });
|
const nodes = computed(() =>
|
||||||
|
props.workflow.nodes.length > 0 ? props.workflow.nodes : props.fallbackNodes,
|
||||||
|
);
|
||||||
|
const connections = computed(() => props.workflow.connections);
|
||||||
|
|
||||||
|
const { nodes: mappedNodes, connections: mappedConnections } = useCanvasMapping({
|
||||||
|
nodes,
|
||||||
|
connections,
|
||||||
|
workflowObject,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.wrapper">
|
<div :class="$style.wrapper">
|
||||||
<div :class="$style.canvas">
|
<div :class="$style.canvas">
|
||||||
<Canvas v-if="workflow" :elements="elements" :connections="connections" v-bind="$attrs" />
|
<Canvas
|
||||||
|
v-if="workflow"
|
||||||
|
:nodes="mappedNodes"
|
||||||
|
:connections="mappedConnections"
|
||||||
|
v-bind="$attrs"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import CanvasExecuteWorkflowButton from './CanvasExecuteWorkflowButton.vue';
|
import CanvasRunWorkflowButton from './CanvasRunWorkflowButton.vue';
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(CanvasExecuteWorkflowButton);
|
const renderComponent = createComponentRenderer(CanvasRunWorkflowButton);
|
||||||
|
|
||||||
describe('CanvasExecuteWorkflowButton', () => {
|
describe('CanvasRunWorkflowButton', () => {
|
||||||
it('should render correctly', () => {
|
it('should render correctly', () => {
|
||||||
const wrapper = renderComponent();
|
const wrapper = renderComponent();
|
||||||
|
|
|
@ -4,12 +4,15 @@ import { computed } from 'vue';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
|
mouseenter: [event: MouseEvent];
|
||||||
|
mouseleave: [event: MouseEvent];
|
||||||
click: [event: MouseEvent];
|
click: [event: MouseEvent];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
waitingForWebhook: boolean;
|
waitingForWebhook: boolean;
|
||||||
executing: boolean;
|
executing: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
@ -32,10 +35,13 @@ const label = computed(() => {
|
||||||
<N8nButton
|
<N8nButton
|
||||||
:loading="executing"
|
:loading="executing"
|
||||||
:label="label"
|
:label="label"
|
||||||
|
:disabled="disabled"
|
||||||
size="large"
|
size="large"
|
||||||
icon="flask"
|
icon="flask"
|
||||||
type="primary"
|
type="primary"
|
||||||
data-test-id="execute-workflow-button"
|
data-test-id="execute-workflow-button"
|
||||||
|
@mouseenter="$emit('mouseenter', $event)"
|
||||||
|
@mouseleave="$emit('mouseleave', $event)"
|
||||||
@click.stop="$emit('click', $event)"
|
@click.stop="$emit('click', $event)"
|
||||||
/>
|
/>
|
||||||
</KeyboardShortcutTooltip>
|
</KeyboardShortcutTooltip>
|
|
@ -0,0 +1,7 @@
|
||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`CanvasRunWorkflowButton > should render correctly 1`] = `
|
||||||
|
"<button class="button button primary large withIcon el-tooltip__trigger el-tooltip__trigger" aria-live="polite" data-test-id="execute-workflow-button"><span class="icon"><span class="n8n-text compact size-large regular n8n-icon n8n-icon"><svg class="svg-inline--fa fa-flask fa-w-14 large" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="flask" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path class="" fill="currentColor" d="M437.2 403.5L320 215V64h8c13.3 0 24-10.7 24-24V24c0-13.3-10.7-24-24-24H120c-13.3 0-24 10.7-24 24v16c0 13.3 10.7 24 24 24h8v151L10.8 403.5C-18.5 450.6 15.3 512 70.9 512h306.2c55.7 0 89.4-61.5 60.1-108.5zM137.9 320l48.2-77.6c3.7-5.2 5.8-11.6 5.8-18.4V64h64v160c0 6.9 2.2 13.2 5.8 18.4l48.2 77.6h-172z"></path></svg></span></span><span>Test workflow</span></button>
|
||||||
|
<!--teleport start-->
|
||||||
|
<!--teleport end-->"
|
||||||
|
`;
|
|
@ -1,11 +1,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Position } from '@vue-flow/core';
|
import { Position } from '@vue-flow/core';
|
||||||
import { computed, provide, toRef, watch } from 'vue';
|
import { computed, provide, toRef, watch } from 'vue';
|
||||||
import type {
|
import type { CanvasNodeData, CanvasConnectionPort, CanvasElementPortWithPosition } from '@/types';
|
||||||
CanvasElementData,
|
|
||||||
CanvasConnectionPort,
|
|
||||||
CanvasElementPortWithPosition,
|
|
||||||
} from '@/types';
|
|
||||||
import NodeIcon from '@/components/NodeIcon.vue';
|
import NodeIcon from '@/components/NodeIcon.vue';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import CanvasNodeToolbar from '@/components/canvas/elements/nodes/CanvasNodeToolbar.vue';
|
import CanvasNodeToolbar from '@/components/canvas/elements/nodes/CanvasNodeToolbar.vue';
|
||||||
|
@ -22,7 +18,7 @@ const emit = defineEmits<{
|
||||||
toggle: [id: string];
|
toggle: [id: string];
|
||||||
activate: [id: string];
|
activate: [id: string];
|
||||||
}>();
|
}>();
|
||||||
const props = defineProps<NodeProps<CanvasElementData>>();
|
const props = defineProps<NodeProps<CanvasNodeData>>();
|
||||||
|
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
|
||||||
|
@ -161,9 +157,15 @@ function onActivate() {
|
||||||
@run="onRun"
|
@run="onRun"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CanvasNodeRenderer v-if="nodeType" @dblclick="onActivate">
|
<CanvasNodeRenderer @dblclick="onActivate">
|
||||||
<NodeIcon :node-type="nodeType" :size="nodeIconSize" :shrink="false" :disabled="isDisabled" />
|
<NodeIcon
|
||||||
<!-- :color-default="iconColorDefault"-->
|
v-if="nodeType"
|
||||||
|
:node-type="nodeType"
|
||||||
|
:size="nodeIconSize"
|
||||||
|
:shrink="false"
|
||||||
|
:disabled="isDisabled"
|
||||||
|
/>
|
||||||
|
<!-- @TODO :color-default="iconColorDefault"-->
|
||||||
</CanvasNodeRenderer>
|
</CanvasNodeRenderer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { createCanvasNodeProvide } from '@/__tests__/data';
|
import { createCanvasNodeProvide } from '@/__tests__/data';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import { setActivePinia } from 'pinia';
|
import { setActivePinia } from 'pinia';
|
||||||
|
import { CanvasNodeRenderType } from '@/types';
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(CanvasNodeRenderer);
|
const renderComponent = createComponentRenderer(CanvasNodeRenderer);
|
||||||
|
|
||||||
|
@ -31,7 +32,7 @@ describe('CanvasNodeRenderer', () => {
|
||||||
...createCanvasNodeProvide({
|
...createCanvasNodeProvide({
|
||||||
data: {
|
data: {
|
||||||
render: {
|
render: {
|
||||||
type: 'default',
|
type: CanvasNodeRenderType.Default,
|
||||||
options: { configuration: true },
|
options: { configuration: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -50,7 +51,7 @@ describe('CanvasNodeRenderer', () => {
|
||||||
...createCanvasNodeProvide({
|
...createCanvasNodeProvide({
|
||||||
data: {
|
data: {
|
||||||
render: {
|
render: {
|
||||||
type: 'default',
|
type: CanvasNodeRenderType.Default,
|
||||||
options: { configurable: true },
|
options: { configurable: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
<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 CanvasNodeAddNodes from '@/components/canvas/elements/nodes/render-types/CanvasNodeAddNodes.vue';
|
||||||
import { CanvasNodeKey } from '@/constants';
|
import { CanvasNodeKey } from '@/constants';
|
||||||
|
import { CanvasNodeRenderType } from '@/types';
|
||||||
|
|
||||||
const node = inject(CanvasNodeKey);
|
const node = inject(CanvasNodeKey);
|
||||||
|
|
||||||
|
@ -13,6 +15,9 @@ const Render = () => {
|
||||||
let Component;
|
let Component;
|
||||||
switch (node?.data.value.render.type) {
|
switch (node?.data.value.render.type) {
|
||||||
// @TODO Add support for sticky notes here
|
// @TODO Add support for sticky notes here
|
||||||
|
case CanvasNodeRenderType.AddNodes:
|
||||||
|
Component = CanvasNodeAddNodes;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
Component = CanvasNodeDefault;
|
Component = CanvasNodeDefault;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { fireEvent } from '@testing-library/vue';
|
||||||
import CanvasNodeToolbar from '@/components/canvas/elements/nodes/CanvasNodeToolbar.vue';
|
import CanvasNodeToolbar from '@/components/canvas/elements/nodes/CanvasNodeToolbar.vue';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { createCanvasNodeProvide } from '@/__tests__/data';
|
import { createCanvasNodeProvide } from '@/__tests__/data';
|
||||||
|
import { CanvasNodeRenderType } from '@/types';
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(CanvasNodeToolbar);
|
const renderComponent = createComponentRenderer(CanvasNodeToolbar);
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ describe('CanvasNodeToolbar', () => {
|
||||||
...createCanvasNodeProvide({
|
...createCanvasNodeProvide({
|
||||||
data: {
|
data: {
|
||||||
render: {
|
render: {
|
||||||
type: 'default',
|
type: CanvasNodeRenderType.Default,
|
||||||
options: { configuration: true },
|
options: { configuration: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
|
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
||||||
|
import { NODE_CREATOR_OPEN_SOURCES } from '@/constants';
|
||||||
|
import { nodeViewEventBus } from '@/event-bus';
|
||||||
|
|
||||||
|
const nodeCreatorStore = useNodeCreatorStore();
|
||||||
|
|
||||||
|
const isTooltipVisible = ref(false);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nodeViewEventBus.on('runWorkflowButton:mouseenter', onShowTooltip);
|
||||||
|
nodeViewEventBus.on('runWorkflowButton:mouseleave', onHideTooltip);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
nodeViewEventBus.off('runWorkflowButton:mouseenter', onShowTooltip);
|
||||||
|
nodeViewEventBus.off('runWorkflowButton:mouseleave', onHideTooltip);
|
||||||
|
});
|
||||||
|
|
||||||
|
function onShowTooltip() {
|
||||||
|
isTooltipVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onHideTooltip() {
|
||||||
|
isTooltipVisible.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClick() {
|
||||||
|
nodeCreatorStore.openNodeCreatorForTriggerNodes(
|
||||||
|
NODE_CREATOR_OPEN_SOURCES.TRIGGER_PLACEHOLDER_BUTTON,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div ref="container" :class="$style.addNodes" data-test-id="canvas-add-button">
|
||||||
|
<N8nTooltip
|
||||||
|
placement="top"
|
||||||
|
:visible="isTooltipVisible"
|
||||||
|
:disabled="nodeCreatorStore.showScrim"
|
||||||
|
:popper-class="$style.tooltip"
|
||||||
|
:show-after="700"
|
||||||
|
>
|
||||||
|
<button :class="$style.button" data-test-id="canvas-plus-button" @click="onClick">
|
||||||
|
<FontAwesomeIcon icon="plus" size="lg" />
|
||||||
|
</button>
|
||||||
|
<template #content>
|
||||||
|
{{ $locale.baseText('nodeView.canvasAddButton.addATriggerNodeBeforeExecuting') }}
|
||||||
|
</template>
|
||||||
|
</N8nTooltip>
|
||||||
|
<p :class="$style.label" v-text="$locale.baseText('nodeView.canvasAddButton.addFirstStep')" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.addNodes {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
|
||||||
|
&:hover .button svg path {
|
||||||
|
fill: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background: var(--color-foreground-xlight);
|
||||||
|
border: 2px dashed var(--color-foreground-xdark);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
min-width: 100px;
|
||||||
|
min-height: 100px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 26px !important;
|
||||||
|
height: 40px;
|
||||||
|
path {
|
||||||
|
fill: var(--color-foreground-xdark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
width: max-content;
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
font-size: var(--font-size-m);
|
||||||
|
line-height: var(--font-line-height-xloose);
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
margin-top: var(--spacing-2xs);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -4,6 +4,7 @@ import { NodeConnectionType } from 'n8n-workflow';
|
||||||
import { createCanvasNodeProvide } from '@/__tests__/data';
|
import { createCanvasNodeProvide } from '@/__tests__/data';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import { setActivePinia } from 'pinia';
|
import { setActivePinia } from 'pinia';
|
||||||
|
import { CanvasNodeRenderType } from '@/types';
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(CanvasNodeDefault);
|
const renderComponent = createComponentRenderer(CanvasNodeDefault);
|
||||||
|
|
||||||
|
@ -140,7 +141,7 @@ describe('CanvasNodeDefault', () => {
|
||||||
...createCanvasNodeProvide({
|
...createCanvasNodeProvide({
|
||||||
data: {
|
data: {
|
||||||
render: {
|
render: {
|
||||||
type: 'default',
|
type: CanvasNodeRenderType.Default,
|
||||||
options: { configurable: true },
|
options: { configurable: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -166,7 +167,7 @@ describe('CanvasNodeDefault', () => {
|
||||||
{ type: NodeConnectionType.AiMemory, index: 0, required: true },
|
{ type: NodeConnectionType.AiMemory, index: 0, required: true },
|
||||||
],
|
],
|
||||||
render: {
|
render: {
|
||||||
type: 'default',
|
type: CanvasNodeRenderType.Default,
|
||||||
options: {
|
options: {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
},
|
},
|
||||||
|
@ -191,7 +192,7 @@ describe('CanvasNodeDefault', () => {
|
||||||
...createCanvasNodeProvide({
|
...createCanvasNodeProvide({
|
||||||
data: {
|
data: {
|
||||||
render: {
|
render: {
|
||||||
type: 'default',
|
type: CanvasNodeRenderType.Default,
|
||||||
options: { configuration: true },
|
options: { configuration: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -210,7 +211,7 @@ describe('CanvasNodeDefault', () => {
|
||||||
...createCanvasNodeProvide({
|
...createCanvasNodeProvide({
|
||||||
data: {
|
data: {
|
||||||
render: {
|
render: {
|
||||||
type: 'default',
|
type: CanvasNodeRenderType.Default,
|
||||||
options: { configurable: true, configuration: true },
|
options: { configurable: true, configuration: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -231,7 +232,7 @@ describe('CanvasNodeDefault', () => {
|
||||||
...createCanvasNodeProvide({
|
...createCanvasNodeProvide({
|
||||||
data: {
|
data: {
|
||||||
render: {
|
render: {
|
||||||
type: 'default',
|
type: CanvasNodeRenderType.Default,
|
||||||
options: { trigger: true },
|
options: { trigger: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,7 +3,7 @@ import type { Connection } from '@vue-flow/core';
|
||||||
import type { IConnection, Workflow } from 'n8n-workflow';
|
import type { IConnection, Workflow } from 'n8n-workflow';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
import { useCanvasOperations } from '@/composables/useCanvasOperations';
|
import { useCanvasOperations } from '@/composables/useCanvasOperations';
|
||||||
import type { CanvasElement } from '@/types';
|
import type { CanvasNode } from '@/types';
|
||||||
import type { ICredentialsResponse, INodeUi, IWorkflowDb, XYPosition } from '@/Interface';
|
import type { ICredentialsResponse, INodeUi, IWorkflowDb, XYPosition } from '@/Interface';
|
||||||
import { RemoveNodeCommand } from '@/models/history';
|
import { RemoveNodeCommand } from '@/models/history';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
@ -76,7 +76,7 @@ describe('useCanvasOperations', () => {
|
||||||
.spyOn(workflowsStore, 'setNodePositionById')
|
.spyOn(workflowsStore, 'setNodePositionById')
|
||||||
.mockImplementation(() => {});
|
.mockImplementation(() => {});
|
||||||
const id = 'node1';
|
const id = 'node1';
|
||||||
const position: CanvasElement['position'] = { x: 10, y: 20 };
|
const position: CanvasNode['position'] = { x: 10, y: 20 };
|
||||||
const node = createTestNode({
|
const node = createTestNode({
|
||||||
id,
|
id,
|
||||||
type: 'node',
|
type: 'node',
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
import { useNodeConnections } from '@/composables/useNodeConnections';
|
import { useNodeConnections } from '@/composables/useNodeConnections';
|
||||||
import type { CanvasElementData } from '@/types';
|
import type { CanvasNodeData } from '@/types';
|
||||||
|
|
||||||
describe('useNodeConnections', () => {
|
describe('useNodeConnections', () => {
|
||||||
const defaultConnections = { input: {}, output: {} };
|
const defaultConnections = { input: {}, output: {} };
|
||||||
describe('mainInputs', () => {
|
describe('mainInputs', () => {
|
||||||
it('should return main inputs when provided with main inputs', () => {
|
it('should return main inputs when provided with main inputs', () => {
|
||||||
const inputs = ref<CanvasElementData['inputs']>([
|
const inputs = ref<CanvasNodeData['inputs']>([
|
||||||
{ type: NodeConnectionType.Main, index: 0 },
|
{ type: NodeConnectionType.Main, index: 0 },
|
||||||
{ type: NodeConnectionType.Main, index: 1 },
|
{ type: NodeConnectionType.Main, index: 1 },
|
||||||
{ type: NodeConnectionType.Main, index: 2 },
|
{ type: NodeConnectionType.Main, index: 2 },
|
||||||
{ type: NodeConnectionType.AiAgent, index: 0 },
|
{ type: NodeConnectionType.AiAgent, index: 0 },
|
||||||
]);
|
]);
|
||||||
const outputs = ref<CanvasElementData['outputs']>([]);
|
const outputs = ref<CanvasNodeData['outputs']>([]);
|
||||||
|
|
||||||
const { mainInputs } = useNodeConnections({
|
const { mainInputs } = useNodeConnections({
|
||||||
inputs,
|
inputs,
|
||||||
|
@ -28,12 +28,12 @@ describe('useNodeConnections', () => {
|
||||||
|
|
||||||
describe('nonMainInputs', () => {
|
describe('nonMainInputs', () => {
|
||||||
it('should return non-main inputs when provided with non-main inputs', () => {
|
it('should return non-main inputs when provided with non-main inputs', () => {
|
||||||
const inputs = ref<CanvasElementData['inputs']>([
|
const inputs = ref<CanvasNodeData['inputs']>([
|
||||||
{ type: NodeConnectionType.Main, index: 0 },
|
{ type: NodeConnectionType.Main, index: 0 },
|
||||||
{ type: NodeConnectionType.AiAgent, index: 0 },
|
{ type: NodeConnectionType.AiAgent, index: 0 },
|
||||||
{ type: NodeConnectionType.AiAgent, index: 1 },
|
{ type: NodeConnectionType.AiAgent, index: 1 },
|
||||||
]);
|
]);
|
||||||
const outputs = ref<CanvasElementData['outputs']>([]);
|
const outputs = ref<CanvasNodeData['outputs']>([]);
|
||||||
|
|
||||||
const { nonMainInputs } = useNodeConnections({
|
const { nonMainInputs } = useNodeConnections({
|
||||||
inputs,
|
inputs,
|
||||||
|
@ -48,12 +48,12 @@ describe('useNodeConnections', () => {
|
||||||
|
|
||||||
describe('requiredNonMainInputs', () => {
|
describe('requiredNonMainInputs', () => {
|
||||||
it('should return required non-main inputs when provided with required non-main inputs', () => {
|
it('should return required non-main inputs when provided with required non-main inputs', () => {
|
||||||
const inputs = ref<CanvasElementData['inputs']>([
|
const inputs = ref<CanvasNodeData['inputs']>([
|
||||||
{ type: NodeConnectionType.Main, index: 0 },
|
{ type: NodeConnectionType.Main, index: 0 },
|
||||||
{ type: NodeConnectionType.AiAgent, required: true, index: 0 },
|
{ type: NodeConnectionType.AiAgent, required: true, index: 0 },
|
||||||
{ type: NodeConnectionType.AiAgent, required: false, index: 1 },
|
{ type: NodeConnectionType.AiAgent, required: false, index: 1 },
|
||||||
]);
|
]);
|
||||||
const outputs = ref<CanvasElementData['outputs']>([]);
|
const outputs = ref<CanvasNodeData['outputs']>([]);
|
||||||
|
|
||||||
const { requiredNonMainInputs } = useNodeConnections({
|
const { requiredNonMainInputs } = useNodeConnections({
|
||||||
inputs,
|
inputs,
|
||||||
|
@ -68,9 +68,9 @@ describe('useNodeConnections', () => {
|
||||||
|
|
||||||
describe('mainInputConnections', () => {
|
describe('mainInputConnections', () => {
|
||||||
it('should return main input connections when provided with main input connections', () => {
|
it('should return main input connections when provided with main input connections', () => {
|
||||||
const inputs = ref<CanvasElementData['inputs']>([]);
|
const inputs = ref<CanvasNodeData['inputs']>([]);
|
||||||
const outputs = ref<CanvasElementData['outputs']>([]);
|
const outputs = ref<CanvasNodeData['outputs']>([]);
|
||||||
const connections = ref<CanvasElementData['connections']>({
|
const connections = ref<CanvasNodeData['connections']>({
|
||||||
input: {
|
input: {
|
||||||
[NodeConnectionType.Main]: [
|
[NodeConnectionType.Main]: [
|
||||||
[{ node: 'node1', type: NodeConnectionType.Main, index: 0 }],
|
[{ node: 'node1', type: NodeConnectionType.Main, index: 0 }],
|
||||||
|
@ -93,8 +93,8 @@ describe('useNodeConnections', () => {
|
||||||
|
|
||||||
describe('mainOutputs', () => {
|
describe('mainOutputs', () => {
|
||||||
it('should return main outputs when provided with main outputs', () => {
|
it('should return main outputs when provided with main outputs', () => {
|
||||||
const inputs = ref<CanvasElementData['inputs']>([]);
|
const inputs = ref<CanvasNodeData['inputs']>([]);
|
||||||
const outputs = ref<CanvasElementData['outputs']>([
|
const outputs = ref<CanvasNodeData['outputs']>([
|
||||||
{ type: NodeConnectionType.Main, index: 0 },
|
{ type: NodeConnectionType.Main, index: 0 },
|
||||||
{ type: NodeConnectionType.Main, index: 1 },
|
{ type: NodeConnectionType.Main, index: 1 },
|
||||||
{ type: NodeConnectionType.Main, index: 2 },
|
{ type: NodeConnectionType.Main, index: 2 },
|
||||||
|
@ -114,8 +114,8 @@ describe('useNodeConnections', () => {
|
||||||
|
|
||||||
describe('nonMainOutputs', () => {
|
describe('nonMainOutputs', () => {
|
||||||
it('should return non-main outputs when provided with non-main outputs', () => {
|
it('should return non-main outputs when provided with non-main outputs', () => {
|
||||||
const inputs = ref<CanvasElementData['inputs']>([]);
|
const inputs = ref<CanvasNodeData['inputs']>([]);
|
||||||
const outputs = ref<CanvasElementData['outputs']>([
|
const outputs = ref<CanvasNodeData['outputs']>([
|
||||||
{ type: NodeConnectionType.Main, index: 0 },
|
{ type: NodeConnectionType.Main, index: 0 },
|
||||||
{ type: NodeConnectionType.AiAgent, index: 0 },
|
{ type: NodeConnectionType.AiAgent, index: 0 },
|
||||||
{ type: NodeConnectionType.AiAgent, index: 1 },
|
{ type: NodeConnectionType.AiAgent, index: 1 },
|
||||||
|
@ -134,9 +134,9 @@ describe('useNodeConnections', () => {
|
||||||
|
|
||||||
describe('mainOutputConnections', () => {
|
describe('mainOutputConnections', () => {
|
||||||
it('should return main output connections when provided with main output connections', () => {
|
it('should return main output connections when provided with main output connections', () => {
|
||||||
const inputs = ref<CanvasElementData['inputs']>([]);
|
const inputs = ref<CanvasNodeData['inputs']>([]);
|
||||||
const outputs = ref<CanvasElementData['outputs']>([]);
|
const outputs = ref<CanvasNodeData['outputs']>([]);
|
||||||
const connections = ref<CanvasElementData['connections']>({
|
const connections = ref<CanvasNodeData['connections']>({
|
||||||
input: {},
|
input: {},
|
||||||
output: {
|
output: {
|
||||||
[NodeConnectionType.Main]: [
|
[NodeConnectionType.Main]: [
|
||||||
|
|
|
@ -3,10 +3,9 @@ import { ref } from 'vue';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
import type { Workflow } from 'n8n-workflow';
|
import type { Workflow } from 'n8n-workflow';
|
||||||
import { createPinia, setActivePinia } from 'pinia';
|
import { createPinia, setActivePinia } from 'pinia';
|
||||||
import { mock } from 'vitest-mock-extended';
|
|
||||||
|
|
||||||
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
||||||
import type { IWorkflowDb } from '@/Interface';
|
import type { INodeUi } from '@/Interface';
|
||||||
import {
|
import {
|
||||||
createTestWorkflowObject,
|
createTestWorkflowObject,
|
||||||
mockNode,
|
mockNode,
|
||||||
|
@ -15,7 +14,7 @@ import {
|
||||||
} from '@/__tests__/mocks';
|
} from '@/__tests__/mocks';
|
||||||
import { MANUAL_TRIGGER_NODE_TYPE, SET_NODE_TYPE } from '@/constants';
|
import { MANUAL_TRIGGER_NODE_TYPE, SET_NODE_TYPE } from '@/constants';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useWorkflowsStore } from '../stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
|
@ -37,38 +36,44 @@ afterEach(() => {
|
||||||
|
|
||||||
describe('useCanvasMapping', () => {
|
describe('useCanvasMapping', () => {
|
||||||
it('should initialize with default props', () => {
|
it('should initialize with default props', () => {
|
||||||
const workflow = mock<IWorkflowDb>({
|
const nodes: INodeUi[] = [];
|
||||||
nodes: [],
|
const connections = {};
|
||||||
|
const workflowObject = createTestWorkflowObject({
|
||||||
|
nodes,
|
||||||
|
connections,
|
||||||
});
|
});
|
||||||
const workflowObject = createTestWorkflowObject(workflow);
|
|
||||||
|
|
||||||
const { elements, connections } = useCanvasMapping({
|
const { nodes: mappedNodes, connections: mappedConnections } = useCanvasMapping({
|
||||||
workflow: ref(workflow),
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(elements.value).toEqual([]);
|
expect(mappedNodes.value).toEqual([]);
|
||||||
expect(connections.value).toEqual([]);
|
expect(mappedConnections.value).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('elements', () => {
|
describe('nodes', () => {
|
||||||
it('should map nodes to canvas elements', () => {
|
it('should map nodes to canvas nodes', () => {
|
||||||
const manualTriggerNode = mockNode({
|
const manualTriggerNode = mockNode({
|
||||||
name: 'Manual Trigger',
|
name: 'Manual Trigger',
|
||||||
type: MANUAL_TRIGGER_NODE_TYPE,
|
type: MANUAL_TRIGGER_NODE_TYPE,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
});
|
});
|
||||||
const workflow = mock<IWorkflowDb>({
|
const nodes = [manualTriggerNode];
|
||||||
nodes: [manualTriggerNode],
|
const connections = {};
|
||||||
|
const workflowObject = createTestWorkflowObject({
|
||||||
|
nodes,
|
||||||
|
connections,
|
||||||
});
|
});
|
||||||
const workflowObject = createTestWorkflowObject(workflow);
|
|
||||||
|
|
||||||
const { elements } = useCanvasMapping({
|
const { nodes: mappedNodes } = useCanvasMapping({
|
||||||
workflow: ref(workflow),
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(elements.value).toEqual([
|
expect(mappedNodes.value).toEqual([
|
||||||
{
|
{
|
||||||
id: manualTriggerNode.id,
|
id: manualTriggerNode.id,
|
||||||
label: manualTriggerNode.name,
|
label: manualTriggerNode.name,
|
||||||
|
@ -133,17 +138,20 @@ describe('useCanvasMapping', () => {
|
||||||
type: MANUAL_TRIGGER_NODE_TYPE,
|
type: MANUAL_TRIGGER_NODE_TYPE,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
});
|
});
|
||||||
const workflow = mock<IWorkflowDb>({
|
const nodes = [manualTriggerNode];
|
||||||
nodes: [manualTriggerNode],
|
const connections = {};
|
||||||
|
const workflowObject = createTestWorkflowObject({
|
||||||
|
nodes,
|
||||||
|
connections,
|
||||||
});
|
});
|
||||||
const workflowObject = createTestWorkflowObject(workflow);
|
|
||||||
|
|
||||||
const { elements } = useCanvasMapping({
|
const { nodes: mappedNodes } = useCanvasMapping({
|
||||||
workflow: ref(workflow),
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(elements.value[0]?.data?.disabled).toEqual(true);
|
expect(mappedNodes.value[0]?.data?.disabled).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle execution state', () => {
|
it('should handle execution state', () => {
|
||||||
|
@ -152,42 +160,49 @@ describe('useCanvasMapping', () => {
|
||||||
type: MANUAL_TRIGGER_NODE_TYPE,
|
type: MANUAL_TRIGGER_NODE_TYPE,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
});
|
});
|
||||||
const workflow = mock<IWorkflowDb>({
|
const nodes = [manualTriggerNode];
|
||||||
nodes: [manualTriggerNode],
|
const connections = {};
|
||||||
|
const workflowObject = createTestWorkflowObject({
|
||||||
|
nodes,
|
||||||
|
connections,
|
||||||
});
|
});
|
||||||
const workflowObject = createTestWorkflowObject(workflow);
|
|
||||||
|
|
||||||
useWorkflowsStore().addExecutingNode(manualTriggerNode.name);
|
useWorkflowsStore().addExecutingNode(manualTriggerNode.name);
|
||||||
|
|
||||||
const { elements } = useCanvasMapping({
|
const { nodes: mappedNodes } = useCanvasMapping({
|
||||||
workflow: ref(workflow),
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(elements.value[0]?.data?.execution.running).toEqual(true);
|
expect(mappedNodes.value[0]?.data?.execution.running).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle input and output connections', () => {
|
it('should handle input and output connections', () => {
|
||||||
const [manualTriggerNode, setNode] = mockNodes.slice(0, 2);
|
const [manualTriggerNode, setNode] = mockNodes.slice(0, 2);
|
||||||
const workflow = mock<IWorkflowDb>({
|
const nodes = [manualTriggerNode, setNode];
|
||||||
nodes: [manualTriggerNode, setNode],
|
const connections = {
|
||||||
connections: {
|
[manualTriggerNode.name]: {
|
||||||
[manualTriggerNode.name]: {
|
[NodeConnectionType.Main]: [
|
||||||
[NodeConnectionType.Main]: [
|
[{ node: setNode.name, type: NodeConnectionType.Main, index: 0 }],
|
||||||
[{ node: setNode.name, type: NodeConnectionType.Main, index: 0 }],
|
],
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
const workflowObject = createTestWorkflowObject({
|
||||||
|
nodes,
|
||||||
|
connections,
|
||||||
});
|
});
|
||||||
const workflowObject = createTestWorkflowObject(workflow);
|
|
||||||
|
|
||||||
const { elements } = useCanvasMapping({
|
const { nodes: mappedNodes } = useCanvasMapping({
|
||||||
workflow: ref(workflow),
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(elements.value[0]?.data?.connections.output).toHaveProperty(NodeConnectionType.Main);
|
expect(mappedNodes.value[0]?.data?.connections.output).toHaveProperty(
|
||||||
expect(elements.value[0]?.data?.connections.output[NodeConnectionType.Main][0][0]).toEqual(
|
NodeConnectionType.Main,
|
||||||
|
);
|
||||||
|
expect(mappedNodes.value[0]?.data?.connections.output[NodeConnectionType.Main][0][0]).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
node: setNode.name,
|
node: setNode.name,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
|
@ -195,8 +210,8 @@ describe('useCanvasMapping', () => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(elements.value[1]?.data?.connections.input).toHaveProperty(NodeConnectionType.Main);
|
expect(mappedNodes.value[1]?.data?.connections.input).toHaveProperty(NodeConnectionType.Main);
|
||||||
expect(elements.value[1]?.data?.connections.input[NodeConnectionType.Main][0][0]).toEqual(
|
expect(mappedNodes.value[1]?.data?.connections.input[NodeConnectionType.Main][0][0]).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
node: manualTriggerNode.name,
|
node: manualTriggerNode.name,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
|
@ -209,24 +224,26 @@ describe('useCanvasMapping', () => {
|
||||||
describe('connections', () => {
|
describe('connections', () => {
|
||||||
it('should map connections to canvas connections', () => {
|
it('should map connections to canvas connections', () => {
|
||||||
const [manualTriggerNode, setNode] = mockNodes.slice(0, 2);
|
const [manualTriggerNode, setNode] = mockNodes.slice(0, 2);
|
||||||
const workflow = mock<IWorkflowDb>({
|
const nodes = [manualTriggerNode, setNode];
|
||||||
nodes: [manualTriggerNode, setNode],
|
const connections = {
|
||||||
connections: {
|
[manualTriggerNode.name]: {
|
||||||
[manualTriggerNode.name]: {
|
[NodeConnectionType.Main]: [
|
||||||
[NodeConnectionType.Main]: [
|
[{ node: setNode.name, type: NodeConnectionType.Main, index: 0 }],
|
||||||
[{ node: setNode.name, type: NodeConnectionType.Main, index: 0 }],
|
],
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
const workflowObject = createTestWorkflowObject({
|
||||||
|
nodes,
|
||||||
|
connections,
|
||||||
});
|
});
|
||||||
const workflowObject = createTestWorkflowObject(workflow);
|
|
||||||
|
|
||||||
const { connections } = useCanvasMapping({
|
const { connections: mappedConnections } = useCanvasMapping({
|
||||||
workflow: ref(workflow),
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(connections.value).toEqual([
|
expect(mappedConnections.value).toEqual([
|
||||||
{
|
{
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: manualTriggerNode.name,
|
fromNodeName: manualTriggerNode.name,
|
||||||
|
@ -254,27 +271,29 @@ describe('useCanvasMapping', () => {
|
||||||
|
|
||||||
it('should map multiple input types to canvas connections', () => {
|
it('should map multiple input types to canvas connections', () => {
|
||||||
const [manualTriggerNode, setNode] = mockNodes.slice(0, 2);
|
const [manualTriggerNode, setNode] = mockNodes.slice(0, 2);
|
||||||
const workflow = mock<IWorkflowDb>({
|
const nodes = [manualTriggerNode, setNode];
|
||||||
nodes: [manualTriggerNode, setNode],
|
const connections = {
|
||||||
connections: {
|
[manualTriggerNode.name]: {
|
||||||
[manualTriggerNode.name]: {
|
[NodeConnectionType.AiTool]: [
|
||||||
[NodeConnectionType.AiTool]: [
|
[{ node: setNode.name, type: NodeConnectionType.AiTool, index: 0 }],
|
||||||
[{ node: setNode.name, type: NodeConnectionType.AiTool, index: 0 }],
|
],
|
||||||
],
|
[NodeConnectionType.AiDocument]: [
|
||||||
[NodeConnectionType.AiDocument]: [
|
[{ node: setNode.name, type: NodeConnectionType.AiDocument, index: 1 }],
|
||||||
[{ node: setNode.name, type: NodeConnectionType.AiDocument, index: 1 }],
|
],
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
const workflowObject = createTestWorkflowObject({
|
||||||
|
nodes,
|
||||||
|
connections,
|
||||||
});
|
});
|
||||||
const workflowObject = createTestWorkflowObject(workflow);
|
|
||||||
|
|
||||||
const { connections } = useCanvasMapping({
|
const { connections: mappedConnections } = useCanvasMapping({
|
||||||
workflow: ref(workflow),
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(connections.value).toEqual([
|
expect(mappedConnections.value).toEqual([
|
||||||
{
|
{
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: manualTriggerNode.name,
|
fromNodeName: manualTriggerNode.name,
|
||||||
|
|
|
@ -12,9 +12,10 @@ import type {
|
||||||
CanvasConnection,
|
CanvasConnection,
|
||||||
CanvasConnectionData,
|
CanvasConnectionData,
|
||||||
CanvasConnectionPort,
|
CanvasConnectionPort,
|
||||||
CanvasElement,
|
CanvasNode,
|
||||||
CanvasElementData,
|
CanvasNodeData,
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
|
import { CanvasNodeRenderType } from '@/types';
|
||||||
import {
|
import {
|
||||||
mapLegacyConnectionsToCanvasConnections,
|
mapLegacyConnectionsToCanvasConnections,
|
||||||
mapLegacyEndpointsToCanvasConnectionPort,
|
mapLegacyEndpointsToCanvasConnectionPort,
|
||||||
|
@ -22,20 +23,23 @@ import {
|
||||||
import type {
|
import type {
|
||||||
ExecutionStatus,
|
ExecutionStatus,
|
||||||
ExecutionSummary,
|
ExecutionSummary,
|
||||||
|
IConnections,
|
||||||
INodeExecutionData,
|
INodeExecutionData,
|
||||||
ITaskData,
|
ITaskData,
|
||||||
Workflow,
|
Workflow,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { NodeHelpers } from 'n8n-workflow';
|
import { NodeHelpers } from 'n8n-workflow';
|
||||||
import type { IWorkflowDb } from '@/Interface';
|
import type { INodeUi } from '@/Interface';
|
||||||
import { WAIT_TIME_UNLIMITED } from '@/constants';
|
import { WAIT_TIME_UNLIMITED } from '@/constants';
|
||||||
import { sanitizeHtml } from '@/utils/htmlUtils';
|
import { sanitizeHtml } from '@/utils/htmlUtils';
|
||||||
|
|
||||||
export function useCanvasMapping({
|
export function useCanvasMapping({
|
||||||
workflow,
|
nodes,
|
||||||
|
connections,
|
||||||
workflowObject,
|
workflowObject,
|
||||||
}: {
|
}: {
|
||||||
workflow: Ref<IWorkflowDb>;
|
nodes: Ref<INodeUi[]>;
|
||||||
|
connections: Ref<IConnections>;
|
||||||
workflowObject: Ref<Workflow>;
|
workflowObject: Ref<Workflow>;
|
||||||
}) {
|
}) {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
@ -44,24 +48,36 @@ export function useCanvasMapping({
|
||||||
|
|
||||||
const renderTypeByNodeType = computed(
|
const renderTypeByNodeType = computed(
|
||||||
() =>
|
() =>
|
||||||
workflow.value.nodes.reduce<Record<string, CanvasElementData['render']>>((acc, node) => {
|
nodes.value.reduce<Record<string, CanvasNodeData['render']>>((acc, node) => {
|
||||||
// @TODO Add support for sticky notes here
|
// @TODO Add support for sticky notes here
|
||||||
|
switch (node.type) {
|
||||||
acc[node.type] = {
|
case `${CanvasNodeRenderType.AddNodes}`:
|
||||||
type: 'default',
|
acc[node.type] = {
|
||||||
options: {
|
type: CanvasNodeRenderType.AddNodes,
|
||||||
trigger: nodeTypesStore.isTriggerNode(node.type),
|
options: {},
|
||||||
configuration: nodeTypesStore.isConfigNode(workflowObject.value, node, node.type),
|
};
|
||||||
configurable: nodeTypesStore.isConfigurableNode(workflowObject.value, node, node.type),
|
break;
|
||||||
},
|
default:
|
||||||
};
|
acc[node.type] = {
|
||||||
|
type: CanvasNodeRenderType.Default,
|
||||||
|
options: {
|
||||||
|
trigger: nodeTypesStore.isTriggerNode(node.type),
|
||||||
|
configuration: nodeTypesStore.isConfigNode(workflowObject.value, node, node.type),
|
||||||
|
configurable: nodeTypesStore.isConfigurableNode(
|
||||||
|
workflowObject.value,
|
||||||
|
node,
|
||||||
|
node.type,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, {}) ?? {},
|
}, {}) ?? {},
|
||||||
);
|
);
|
||||||
|
|
||||||
const nodeInputsById = computed(() =>
|
const nodeInputsById = computed(() =>
|
||||||
workflow.value.nodes.reduce<Record<string, CanvasConnectionPort[]>>((acc, node) => {
|
nodes.value.reduce<Record<string, CanvasConnectionPort[]>>((acc, node) => {
|
||||||
const nodeTypeDescription = nodeTypesStore.getNodeType(node.type);
|
const nodeTypeDescription = nodeTypesStore.getNodeType(node.type);
|
||||||
const workflowObjectNode = workflowObject.value.getNode(node.name);
|
const workflowObjectNode = workflowObject.value.getNode(node.name);
|
||||||
|
|
||||||
|
@ -81,7 +97,7 @@ export function useCanvasMapping({
|
||||||
);
|
);
|
||||||
|
|
||||||
const nodeOutputsById = computed(() =>
|
const nodeOutputsById = computed(() =>
|
||||||
workflow.value.nodes.reduce<Record<string, CanvasConnectionPort[]>>((acc, node) => {
|
nodes.value.reduce<Record<string, CanvasConnectionPort[]>>((acc, node) => {
|
||||||
const nodeTypeDescription = nodeTypesStore.getNodeType(node.type);
|
const nodeTypeDescription = nodeTypesStore.getNodeType(node.type);
|
||||||
const workflowObjectNode = workflowObject.value.getNode(node.name);
|
const workflowObjectNode = workflowObject.value.getNode(node.name);
|
||||||
|
|
||||||
|
@ -101,21 +117,21 @@ export function useCanvasMapping({
|
||||||
);
|
);
|
||||||
|
|
||||||
const nodePinnedDataById = computed(() =>
|
const nodePinnedDataById = computed(() =>
|
||||||
workflow.value.nodes.reduce<Record<string, INodeExecutionData[] | undefined>>((acc, node) => {
|
nodes.value.reduce<Record<string, INodeExecutionData[] | undefined>>((acc, node) => {
|
||||||
acc[node.id] = workflowsStore.pinDataByNodeName(node.name);
|
acc[node.id] = workflowsStore.pinDataByNodeName(node.name);
|
||||||
return acc;
|
return acc;
|
||||||
}, {}),
|
}, {}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const nodeExecutionRunningById = computed(() =>
|
const nodeExecutionRunningById = computed(() =>
|
||||||
workflow.value.nodes.reduce<Record<string, boolean>>((acc, node) => {
|
nodes.value.reduce<Record<string, boolean>>((acc, node) => {
|
||||||
acc[node.id] = workflowsStore.isNodeExecuting(node.name);
|
acc[node.id] = workflowsStore.isNodeExecuting(node.name);
|
||||||
return acc;
|
return acc;
|
||||||
}, {}),
|
}, {}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const nodeExecutionStatusById = computed(() =>
|
const nodeExecutionStatusById = computed(() =>
|
||||||
workflow.value.nodes.reduce<Record<string, ExecutionStatus>>((acc, node) => {
|
nodes.value.reduce<Record<string, ExecutionStatus>>((acc, node) => {
|
||||||
acc[node.id] =
|
acc[node.id] =
|
||||||
workflowsStore.getWorkflowRunData?.[node.name]?.filter(Boolean)[0].executionStatus ?? 'new';
|
workflowsStore.getWorkflowRunData?.[node.name]?.filter(Boolean)[0].executionStatus ?? 'new';
|
||||||
return acc;
|
return acc;
|
||||||
|
@ -123,14 +139,14 @@ export function useCanvasMapping({
|
||||||
);
|
);
|
||||||
|
|
||||||
const nodeExecutionRunDataById = computed(() =>
|
const nodeExecutionRunDataById = computed(() =>
|
||||||
workflow.value.nodes.reduce<Record<string, ITaskData[] | null>>((acc, node) => {
|
nodes.value.reduce<Record<string, ITaskData[] | null>>((acc, node) => {
|
||||||
acc[node.id] = workflowsStore.getWorkflowResultDataByNodeName(node.name);
|
acc[node.id] = workflowsStore.getWorkflowResultDataByNodeName(node.name);
|
||||||
return acc;
|
return acc;
|
||||||
}, {}),
|
}, {}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const nodeIssuesById = computed(() =>
|
const nodeIssuesById = computed(() =>
|
||||||
workflow.value.nodes.reduce<Record<string, string[]>>((acc, node) => {
|
nodes.value.reduce<Record<string, string[]>>((acc, node) => {
|
||||||
const issues: string[] = [];
|
const issues: string[] = [];
|
||||||
const nodeExecutionRunData = workflowsStore.getWorkflowRunData?.[node.name];
|
const nodeExecutionRunData = workflowsStore.getWorkflowRunData?.[node.name];
|
||||||
if (nodeExecutionRunData) {
|
if (nodeExecutionRunData) {
|
||||||
|
@ -154,7 +170,7 @@ export function useCanvasMapping({
|
||||||
);
|
);
|
||||||
|
|
||||||
const nodeHasIssuesById = computed(() =>
|
const nodeHasIssuesById = computed(() =>
|
||||||
workflow.value.nodes.reduce<Record<string, boolean>>((acc, node) => {
|
nodes.value.reduce<Record<string, boolean>>((acc, node) => {
|
||||||
if (['crashed', 'error'].includes(nodeExecutionStatusById.value[node.id])) {
|
if (['crashed', 'error'].includes(nodeExecutionStatusById.value[node.id])) {
|
||||||
acc[node.id] = true;
|
acc[node.id] = true;
|
||||||
} else if (nodePinnedDataById.value[node.id]) {
|
} else if (nodePinnedDataById.value[node.id]) {
|
||||||
|
@ -168,7 +184,7 @@ export function useCanvasMapping({
|
||||||
);
|
);
|
||||||
|
|
||||||
const nodeExecutionWaitingById = computed(() =>
|
const nodeExecutionWaitingById = computed(() =>
|
||||||
workflow.value.nodes.reduce<Record<string, string | undefined>>((acc, node) => {
|
nodes.value.reduce<Record<string, string | undefined>>((acc, node) => {
|
||||||
const isExecutionSummary = (execution: object): execution is ExecutionSummary =>
|
const isExecutionSummary = (execution: object): execution is ExecutionSummary =>
|
||||||
'waitTill' in execution;
|
'waitTill' in execution;
|
||||||
|
|
||||||
|
@ -198,12 +214,12 @@ export function useCanvasMapping({
|
||||||
}, {}),
|
}, {}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const elements = computed<CanvasElement[]>(() => [
|
const mappedNodes = computed<CanvasNode[]>(() => [
|
||||||
...workflow.value.nodes.map<CanvasElement>((node) => {
|
...nodes.value.map<CanvasNode>((node) => {
|
||||||
const inputConnections = workflowObject.value.connectionsByDestinationNode[node.name] ?? {};
|
const inputConnections = workflowObject.value.connectionsByDestinationNode[node.name] ?? {};
|
||||||
const outputConnections = workflowObject.value.connectionsBySourceNode[node.name] ?? {};
|
const outputConnections = workflowObject.value.connectionsBySourceNode[node.name] ?? {};
|
||||||
|
|
||||||
const data: CanvasElementData = {
|
const data: CanvasNodeData = {
|
||||||
id: node.id,
|
id: node.id,
|
||||||
type: node.type,
|
type: node.type,
|
||||||
typeVersion: node.typeVersion,
|
typeVersion: node.typeVersion,
|
||||||
|
@ -244,31 +260,26 @@ export function useCanvasMapping({
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const connections = computed<CanvasConnection[]>(() => {
|
const mappedConnections = computed<CanvasConnection[]>(() => {
|
||||||
const mappedConnections = mapLegacyConnectionsToCanvasConnections(
|
return mapLegacyConnectionsToCanvasConnections(connections.value ?? [], nodes.value ?? []).map(
|
||||||
workflow.value.connections ?? [],
|
(connection) => {
|
||||||
workflow.value.nodes ?? [],
|
const type = getConnectionType(connection);
|
||||||
|
const label = getConnectionLabel(connection);
|
||||||
|
const data = getConnectionData(connection);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...connection,
|
||||||
|
data,
|
||||||
|
type,
|
||||||
|
label,
|
||||||
|
animated: data.status === 'running',
|
||||||
|
};
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return mappedConnections.map((connection) => {
|
|
||||||
const type = getConnectionType(connection);
|
|
||||||
const label = getConnectionLabel(connection);
|
|
||||||
const data = getConnectionData(connection);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...connection,
|
|
||||||
data,
|
|
||||||
type,
|
|
||||||
label,
|
|
||||||
animated: data.status === 'running',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function getConnectionData(connection: CanvasConnection): CanvasConnectionData {
|
function getConnectionData(connection: CanvasConnection): CanvasConnectionData {
|
||||||
const fromNode = workflow.value.nodes.find(
|
const fromNode = nodes.value.find((node) => node.name === connection.data?.fromNodeName);
|
||||||
(node) => node.name === connection.data?.fromNodeName,
|
|
||||||
);
|
|
||||||
|
|
||||||
let status: CanvasConnectionData['status'];
|
let status: CanvasConnectionData['status'];
|
||||||
if (fromNode) {
|
if (fromNode) {
|
||||||
|
@ -297,9 +308,7 @@ export function useCanvasMapping({
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConnectionLabel(connection: CanvasConnection): string {
|
function getConnectionLabel(connection: CanvasConnection): string {
|
||||||
const fromNode = workflow.value.nodes.find(
|
const fromNode = nodes.value.find((node) => node.name === connection.data?.fromNodeName);
|
||||||
(node) => node.name === connection.data?.fromNodeName,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!fromNode) {
|
if (!fromNode) {
|
||||||
return '';
|
return '';
|
||||||
|
@ -323,7 +332,7 @@ export function useCanvasMapping({
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connections,
|
connections: mappedConnections,
|
||||||
elements,
|
nodes: mappedNodes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||||
import { inject, ref } from 'vue';
|
import { inject, ref } from 'vue';
|
||||||
import type { CanvasNodeInjectionData } from '../types';
|
import type { CanvasNodeInjectionData } from '../types';
|
||||||
|
import { CanvasNodeRenderType } from '../types';
|
||||||
|
|
||||||
vi.mock('vue', async () => {
|
vi.mock('vue', async () => {
|
||||||
const actual = await vi.importActual('vue');
|
const actual = await vi.importActual('vue');
|
||||||
|
@ -47,7 +48,7 @@ describe('useCanvasNode', () => {
|
||||||
runData: { count: 1, visible: true },
|
runData: { count: 1, visible: true },
|
||||||
pinnedData: { count: 1, visible: true },
|
pinnedData: { count: 1, visible: true },
|
||||||
render: {
|
render: {
|
||||||
type: 'default',
|
type: CanvasNodeRenderType.Default,
|
||||||
options: {
|
options: {
|
||||||
configurable: false,
|
configurable: false,
|
||||||
configuration: false,
|
configuration: false,
|
||||||
|
|
|
@ -5,11 +5,12 @@
|
||||||
|
|
||||||
import { CanvasNodeKey } from '@/constants';
|
import { CanvasNodeKey } from '@/constants';
|
||||||
import { computed, inject } from 'vue';
|
import { computed, inject } from 'vue';
|
||||||
import type { CanvasElementData } from '@/types';
|
import type { CanvasNodeData } from '@/types';
|
||||||
|
import { CanvasNodeRenderType } from '@/types';
|
||||||
|
|
||||||
export function useCanvasNode() {
|
export function useCanvasNode() {
|
||||||
const node = inject(CanvasNodeKey);
|
const node = inject(CanvasNodeKey);
|
||||||
const data = computed<CanvasElementData>(
|
const data = computed<CanvasNodeData>(
|
||||||
() =>
|
() =>
|
||||||
node?.data.value ?? {
|
node?.data.value ?? {
|
||||||
id: '',
|
id: '',
|
||||||
|
@ -26,7 +27,7 @@ export function useCanvasNode() {
|
||||||
},
|
},
|
||||||
runData: { count: 0, visible: false },
|
runData: { count: 0, visible: false },
|
||||||
render: {
|
render: {
|
||||||
type: 'default',
|
type: CanvasNodeRenderType.Default,
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* @TODO Remove this notice when Canvas V2 is the only one in use
|
* @TODO Remove this notice when Canvas V2 is the only one in use
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { CanvasElement } from '@/types';
|
import type { CanvasNode } from '@/types';
|
||||||
import { CanvasConnectionMode } from '@/types';
|
import { CanvasConnectionMode } from '@/types';
|
||||||
import type {
|
import type {
|
||||||
AddedNodesAndConnections,
|
AddedNodesAndConnections,
|
||||||
|
@ -105,7 +105,7 @@ export function useCanvasOperations({
|
||||||
|
|
||||||
function updateNodePosition(
|
function updateNodePosition(
|
||||||
id: string,
|
id: string,
|
||||||
position: CanvasElement['position'],
|
position: CanvasNode['position'],
|
||||||
{ trackHistory = false, trackBulk = true } = {},
|
{ trackHistory = false, trackBulk = true } = {},
|
||||||
) {
|
) {
|
||||||
const node = workflowsStore.getNodeById(id);
|
const node = workflowsStore.getNodeById(id);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { CanvasElementData } from '@/types';
|
import type { CanvasNodeData } from '@/types';
|
||||||
import type { MaybeRef } from 'vue';
|
import type { MaybeRef } from 'vue';
|
||||||
import { computed, unref } from 'vue';
|
import { computed, unref } from 'vue';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
@ -8,9 +8,9 @@ export function useNodeConnections({
|
||||||
outputs,
|
outputs,
|
||||||
connections,
|
connections,
|
||||||
}: {
|
}: {
|
||||||
inputs: MaybeRef<CanvasElementData['inputs']>;
|
inputs: MaybeRef<CanvasNodeData['inputs']>;
|
||||||
outputs: MaybeRef<CanvasElementData['outputs']>;
|
outputs: MaybeRef<CanvasNodeData['outputs']>;
|
||||||
connections: MaybeRef<CanvasElementData['connections']>;
|
connections: MaybeRef<CanvasNodeData['connections']>;
|
||||||
}) {
|
}) {
|
||||||
/**
|
/**
|
||||||
* Inputs
|
* Inputs
|
||||||
|
|
|
@ -211,6 +211,17 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openNodeCreatorForTriggerNodes(source: NodeCreatorOpenSource) {
|
||||||
|
ndvStore.activeNodeName = null;
|
||||||
|
setSelectedView(TRIGGER_NODE_CREATOR_VIEW);
|
||||||
|
setShowScrim(true);
|
||||||
|
openNodeCreator({
|
||||||
|
source,
|
||||||
|
createNodeActive: true,
|
||||||
|
nodeCreatorView: TRIGGER_NODE_CREATOR_VIEW,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getNodeCreatorFilter(nodeName: string, outputType?: NodeConnectionType) {
|
function getNodeCreatorFilter(nodeName: string, outputType?: NodeConnectionType) {
|
||||||
let filter;
|
let filter;
|
||||||
const workflow = workflowsStore.getCurrentWorkflow();
|
const workflow = workflowsStore.getCurrentWorkflow();
|
||||||
|
@ -252,6 +263,7 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
|
||||||
openNodeCreator,
|
openNodeCreator,
|
||||||
openSelectiveNodeCreator,
|
openSelectiveNodeCreator,
|
||||||
openNodeCreatorForConnectingNode,
|
openNodeCreatorForConnectingNode,
|
||||||
|
openNodeCreatorForTriggerNodes,
|
||||||
allNodeCreatorNodes,
|
allNodeCreatorNodes,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,8 +10,6 @@ import type { DefaultEdge, Node, NodeProps, Position } from '@vue-flow/core';
|
||||||
import type { INodeUi } from '@/Interface';
|
import type { INodeUi } from '@/Interface';
|
||||||
import type { ComputedRef, Ref } from 'vue';
|
import type { ComputedRef, Ref } from 'vue';
|
||||||
|
|
||||||
export type CanvasElementType = 'node' | 'note';
|
|
||||||
|
|
||||||
export type CanvasConnectionPortType = ConnectionTypes;
|
export type CanvasConnectionPortType = ConnectionTypes;
|
||||||
|
|
||||||
export const enum CanvasConnectionMode {
|
export const enum CanvasConnectionMode {
|
||||||
|
@ -36,7 +34,26 @@ export interface CanvasElementPortWithPosition extends CanvasConnectionPort {
|
||||||
offset?: { top?: string; left?: string };
|
offset?: { top?: string; left?: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CanvasElementData {
|
export const enum CanvasNodeRenderType {
|
||||||
|
Default = 'default',
|
||||||
|
AddNodes = 'n8n-nodes-internal.addNodes',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CanvasNodeDefaultRender = {
|
||||||
|
type: CanvasNodeRenderType.Default;
|
||||||
|
options: Partial<{
|
||||||
|
configurable: boolean;
|
||||||
|
configuration: boolean;
|
||||||
|
trigger: boolean;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CanvasNodeAddNodesRender = {
|
||||||
|
type: CanvasNodeRenderType.AddNodes;
|
||||||
|
options: Record<string, never>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CanvasNodeData {
|
||||||
id: INodeUi['id'];
|
id: INodeUi['id'];
|
||||||
type: INodeUi['type'];
|
type: INodeUi['type'];
|
||||||
typeVersion: INodeUi['typeVersion'];
|
typeVersion: INodeUi['typeVersion'];
|
||||||
|
@ -64,13 +81,10 @@ export interface CanvasElementData {
|
||||||
count: number;
|
count: number;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
};
|
};
|
||||||
render: {
|
render: CanvasNodeDefaultRender | CanvasNodeAddNodesRender;
|
||||||
type: 'default';
|
|
||||||
options: Partial<{ configurable: boolean; configuration: boolean; trigger: boolean }>;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CanvasElement = Node<CanvasElementData>;
|
export type CanvasNode = Node<CanvasNodeData>;
|
||||||
|
|
||||||
export interface CanvasConnectionData {
|
export interface CanvasConnectionData {
|
||||||
source: CanvasConnectionPort;
|
source: CanvasConnectionPort;
|
||||||
|
@ -91,7 +105,7 @@ export interface CanvasPlugin {
|
||||||
|
|
||||||
export interface CanvasNodeInjectionData {
|
export interface CanvasNodeInjectionData {
|
||||||
id: Ref<string>;
|
id: Ref<string>;
|
||||||
data: Ref<CanvasElementData>;
|
data: Ref<CanvasNodeData>;
|
||||||
label: Ref<NodeProps['label']>;
|
label: Ref<NodeProps['label']>;
|
||||||
selected: Ref<NodeProps['selected']>;
|
selected: Ref<NodeProps['selected']>;
|
||||||
nodeType: ComputedRef<INodeTypeDescription | null>;
|
nodeType: ComputedRef<INodeTypeDescription | null>;
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router';
|
||||||
import WorkflowCanvas from '@/components/canvas/WorkflowCanvas.vue';
|
import WorkflowCanvas from '@/components/canvas/WorkflowCanvas.vue';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import CanvasExecuteWorkflowButton from '@/components/canvas/elements/buttons/CanvasExecuteWorkflowButton.vue';
|
import CanvasRunWorkflowButton from '@/components/canvas/elements/buttons/CanvasRunWorkflowButton.vue';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
||||||
|
@ -27,15 +27,19 @@ import type {
|
||||||
XYPosition,
|
XYPosition,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
import type { Connection } from '@vue-flow/core';
|
import type { Connection } from '@vue-flow/core';
|
||||||
import type { CanvasElement, ConnectStartEvent } from '@/types';
|
import type { CanvasNode, ConnectStartEvent } from '@/types';
|
||||||
|
import { CanvasNodeRenderType } from '@/types';
|
||||||
import {
|
import {
|
||||||
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT,
|
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT,
|
||||||
|
CHAT_TRIGGER_NODE_TYPE,
|
||||||
EnterpriseEditionFeature,
|
EnterpriseEditionFeature,
|
||||||
MAIN_HEADER_TABS,
|
MAIN_HEADER_TABS,
|
||||||
|
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||||
MODAL_CANCEL,
|
MODAL_CANCEL,
|
||||||
MODAL_CONFIRM,
|
MODAL_CONFIRM,
|
||||||
NODE_CREATOR_OPEN_SOURCES,
|
NODE_CREATOR_OPEN_SOURCES,
|
||||||
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
||||||
|
START_NODE_TYPE,
|
||||||
VIEWS,
|
VIEWS,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
|
@ -79,6 +83,7 @@ import { getNodeViewTab } from '@/utils/canvasUtils';
|
||||||
import { parseCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
|
import { parseCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
|
||||||
import CanvasStopCurrentExecutionButton from '@/components/canvas/elements/buttons/CanvasStopCurrentExecutionButton.vue';
|
import CanvasStopCurrentExecutionButton from '@/components/canvas/elements/buttons/CanvasStopCurrentExecutionButton.vue';
|
||||||
import CanvasStopWaitingForWebhookButton from '@/components/canvas/elements/buttons/CanvasStopWaitingForWebhookButton.vue';
|
import CanvasStopWaitingForWebhookButton from '@/components/canvas/elements/buttons/CanvasStopWaitingForWebhookButton.vue';
|
||||||
|
import { nodeViewEventBus } from '@/event-bus';
|
||||||
|
|
||||||
const NodeCreation = defineAsyncComponent(
|
const NodeCreation = defineAsyncComponent(
|
||||||
async () => await import('@/components/Node/NodeCreation.vue'),
|
async () => await import('@/components/Node/NodeCreation.vue'),
|
||||||
|
@ -165,6 +170,25 @@ const isReadOnlyEnvironment = computed(() => {
|
||||||
return sourceControlStore.preferences.branchReadOnly;
|
return sourceControlStore.preferences.branchReadOnly;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isCanvasReadOnly = computed(() => {
|
||||||
|
return isLoading.value || isDemoRoute.value || isReadOnlyEnvironment.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const fallbackNodes = computed<INodeUi[]>(() =>
|
||||||
|
isCanvasReadOnly.value
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
id: CanvasNodeRenderType.AddNodes,
|
||||||
|
name: CanvasNodeRenderType.AddNodes,
|
||||||
|
type: CanvasNodeRenderType.AddNodes,
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [0, 0],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialization
|
* Initialization
|
||||||
*/
|
*/
|
||||||
|
@ -412,7 +436,20 @@ function makeNewWorkflowShareable() {
|
||||||
* Nodes
|
* Nodes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function onUpdateNodePosition(id: string, position: CanvasElement['position']) {
|
const triggerNodes = computed(() => {
|
||||||
|
return editableWorkflow.value.nodes.filter(
|
||||||
|
(node) => node.type === START_NODE_TYPE || nodeTypesStore.isTriggerNode(node.type),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const containsTriggerNodes = computed(() => triggerNodes.value.length > 0);
|
||||||
|
|
||||||
|
const allTriggerNodesDisabled = computed(() => {
|
||||||
|
const disabledTriggerNodes = triggerNodes.value.filter((node) => node.disabled);
|
||||||
|
return disabledTriggerNodes.length === triggerNodes.value.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
function onUpdateNodePosition(id: string, position: CanvasNode['position']) {
|
||||||
updateNodePosition(id, position, { trackHistory: true });
|
updateNodePosition(id, position, { trackHistory: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -559,6 +596,18 @@ const isStoppingExecution = ref(false);
|
||||||
const isWorkflowRunning = computed(() => uiStore.isActionActive.workflowRunning);
|
const isWorkflowRunning = computed(() => uiStore.isActionActive.workflowRunning);
|
||||||
const isExecutionWaitingForWebhook = computed(() => workflowsStore.executionWaitingForWebhook);
|
const isExecutionWaitingForWebhook = computed(() => workflowsStore.executionWaitingForWebhook);
|
||||||
|
|
||||||
|
const isExecutionDisabled = computed(() => {
|
||||||
|
if (
|
||||||
|
containsChatTriggerNodes.value &&
|
||||||
|
isOnlyChatTriggerNodeActive.value &&
|
||||||
|
!chatTriggerNodePinnedData.value
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !containsTriggerNodes.value || allTriggerNodesDisabled.value;
|
||||||
|
});
|
||||||
|
|
||||||
const isStopExecutionButtonVisible = computed(
|
const isStopExecutionButtonVisible = computed(
|
||||||
() => isWorkflowRunning.value && !isExecutionWaitingForWebhook.value,
|
() => isWorkflowRunning.value && !isExecutionWaitingForWebhook.value,
|
||||||
);
|
);
|
||||||
|
@ -623,6 +672,43 @@ async function onStopWaitingForWebhook() {
|
||||||
await stopWaitingForWebhook();
|
await stopWaitingForWebhook();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onRunWorkflowButtonMouseEnter() {
|
||||||
|
nodeViewEventBus.emit('runWorkflowButton:mouseenter');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRunWorkflowButtonMouseLeave() {
|
||||||
|
nodeViewEventBus.emit('runWorkflowButton:mouseleave');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chat
|
||||||
|
*/
|
||||||
|
|
||||||
|
const chatTriggerNode = computed(() => {
|
||||||
|
return editableWorkflow.value.nodes.find((node) => node.type === CHAT_TRIGGER_NODE_TYPE);
|
||||||
|
});
|
||||||
|
|
||||||
|
const containsChatTriggerNodes = computed(() => {
|
||||||
|
return (
|
||||||
|
!isExecutionWaitingForWebhook.value &&
|
||||||
|
!!editableWorkflow.value.nodes.find(
|
||||||
|
(node) =>
|
||||||
|
[MANUAL_CHAT_TRIGGER_NODE_TYPE, CHAT_TRIGGER_NODE_TYPE].includes(node.type) &&
|
||||||
|
node.disabled !== true,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isOnlyChatTriggerNodeActive = computed(() => {
|
||||||
|
return triggerNodes.value.every((node) => node.disabled || node.type === CHAT_TRIGGER_NODE_TYPE);
|
||||||
|
});
|
||||||
|
|
||||||
|
const chatTriggerNodePinnedData = computed(() => {
|
||||||
|
if (!chatTriggerNode.value) return null;
|
||||||
|
|
||||||
|
return workflowsStore.pinDataByNodeName(chatTriggerNode.value.name);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keyboard
|
* Keyboard
|
||||||
*/
|
*/
|
||||||
|
@ -834,7 +920,7 @@ async function checkAndInitDebugMode() {
|
||||||
* Mouse events
|
* Mouse events
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function onClickPane(position: CanvasElement['position']) {
|
function onClickPane(position: CanvasNode['position']) {
|
||||||
lastClickPosition.value = [position.x, position.y];
|
lastClickPosition.value = [position.x, position.y];
|
||||||
canvasStore.newNodeInsertPosition = [position.x, position.y];
|
canvasStore.newNodeInsertPosition = [position.x, position.y];
|
||||||
}
|
}
|
||||||
|
@ -967,6 +1053,7 @@ onBeforeUnmount(() => {
|
||||||
v-if="editableWorkflow && editableWorkflowObject"
|
v-if="editableWorkflow && editableWorkflowObject"
|
||||||
:workflow="editableWorkflow"
|
:workflow="editableWorkflow"
|
||||||
:workflow-object="editableWorkflowObject"
|
:workflow-object="editableWorkflowObject"
|
||||||
|
:fallback-nodes="fallbackNodes"
|
||||||
@update:node:position="onUpdateNodePosition"
|
@update:node:position="onUpdateNodePosition"
|
||||||
@update:node:active="onSetNodeActive"
|
@update:node:active="onSetNodeActive"
|
||||||
@update:node:selected="onSetNodeSelected"
|
@update:node:selected="onSetNodeSelected"
|
||||||
|
@ -979,9 +1066,12 @@ onBeforeUnmount(() => {
|
||||||
@click:pane="onClickPane"
|
@click:pane="onClickPane"
|
||||||
>
|
>
|
||||||
<div :class="$style.executionButtons">
|
<div :class="$style.executionButtons">
|
||||||
<CanvasExecuteWorkflowButton
|
<CanvasRunWorkflowButton
|
||||||
:waiting-for-webhook="isExecutionWaitingForWebhook"
|
:waiting-for-webhook="isExecutionWaitingForWebhook"
|
||||||
|
:disabled="isExecutionDisabled"
|
||||||
:executing="isWorkflowRunning"
|
:executing="isWorkflowRunning"
|
||||||
|
@mouseenter="onRunWorkflowButtonMouseEnter"
|
||||||
|
@mouseleave="onRunWorkflowButtonMouseLeave"
|
||||||
@click="onRunWorkflow"
|
@click="onRunWorkflow"
|
||||||
/>
|
/>
|
||||||
<CanvasStopCurrentExecutionButton
|
<CanvasStopCurrentExecutionButton
|
||||||
|
|
Loading…
Reference in a new issue