mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(editor): Add execute workflow functionality and statuses to new canvas (no-changelog) (#9902)
Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> Co-authored-by: Oleg Ivaniv <me@olegivaniv.com>
This commit is contained in:
parent
1807835740
commit
8f970b5d37
|
@ -43,6 +43,7 @@
|
||||||
"@jsplumb/util": "^5.13.2",
|
"@jsplumb/util": "^5.13.2",
|
||||||
"@lezer/common": "^1.0.4",
|
"@lezer/common": "^1.0.4",
|
||||||
"@n8n/chat": "workspace:*",
|
"@n8n/chat": "workspace:*",
|
||||||
|
"@n8n/codemirror-lang": "workspace:*",
|
||||||
"@n8n/codemirror-lang-sql": "^1.0.2",
|
"@n8n/codemirror-lang-sql": "^1.0.2",
|
||||||
"@n8n/permissions": "workspace:*",
|
"@n8n/permissions": "workspace:*",
|
||||||
"@vue-flow/background": "^1.3.0",
|
"@vue-flow/background": "^1.3.0",
|
||||||
|
@ -55,7 +56,6 @@
|
||||||
"axios": "1.6.7",
|
"axios": "1.6.7",
|
||||||
"chart.js": "^4.4.0",
|
"chart.js": "^4.4.0",
|
||||||
"codemirror-lang-html-n8n": "^1.0.0",
|
"codemirror-lang-html-n8n": "^1.0.0",
|
||||||
"@n8n/codemirror-lang": "workspace:*",
|
|
||||||
"dateformat": "^3.0.3",
|
"dateformat": "^3.0.3",
|
||||||
"email-providers": "^2.0.1",
|
"email-providers": "^2.0.1",
|
||||||
"esprima-next": "5.8.4",
|
"esprima-next": "5.8.4",
|
||||||
|
|
|
@ -10,9 +10,17 @@ export function createCanvasNodeData({
|
||||||
inputs = [],
|
inputs = [],
|
||||||
outputs = [],
|
outputs = [],
|
||||||
connections = { input: {}, output: {} },
|
connections = { input: {}, output: {} },
|
||||||
|
execution = {},
|
||||||
|
issues = { items: [], visible: false },
|
||||||
|
pinnedData = { count: 0, visible: false },
|
||||||
|
runData = { count: 0, visible: false },
|
||||||
renderType = 'default',
|
renderType = 'default',
|
||||||
}: Partial<CanvasElementData> = {}): CanvasElementData {
|
}: Partial<CanvasElementData> = {}): CanvasElementData {
|
||||||
return {
|
return {
|
||||||
|
execution,
|
||||||
|
issues,
|
||||||
|
pinnedData,
|
||||||
|
runData,
|
||||||
id,
|
id,
|
||||||
type,
|
type,
|
||||||
typeVersion,
|
typeVersion,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import type {
|
||||||
IWorkflowSettings,
|
IWorkflowSettings,
|
||||||
LoadedClass,
|
LoadedClass,
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
|
INodeIssues,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { NodeHelpers, Workflow } from 'n8n-workflow';
|
import { NodeHelpers, Workflow } from 'n8n-workflow';
|
||||||
import { uuid } from '@jsplumb/util';
|
import { uuid } from '@jsplumb/util';
|
||||||
|
@ -23,6 +24,7 @@ import {
|
||||||
NO_OP_NODE_TYPE,
|
NO_OP_NODE_TYPE,
|
||||||
SET_NODE_TYPE,
|
SET_NODE_TYPE,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
|
import type { INodeUi } from '@/Interface';
|
||||||
|
|
||||||
export const mockNode = ({
|
export const mockNode = ({
|
||||||
id = uuid(),
|
id = uuid(),
|
||||||
|
@ -30,22 +32,30 @@ export const mockNode = ({
|
||||||
type,
|
type,
|
||||||
position = [0, 0],
|
position = [0, 0],
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
issues = undefined,
|
||||||
|
typeVersion = 1,
|
||||||
}: {
|
}: {
|
||||||
id?: INode['id'];
|
id?: INodeUi['id'];
|
||||||
name: INode['name'];
|
name: INodeUi['name'];
|
||||||
type: INode['type'];
|
type: INodeUi['type'];
|
||||||
position?: INode['position'];
|
position?: INodeUi['position'];
|
||||||
disabled?: INode['disabled'];
|
disabled?: INodeUi['disabled'];
|
||||||
}) => mock<INode>({ id, name, type, position, disabled });
|
issues?: INodeIssues;
|
||||||
|
typeVersion?: INodeUi['typeVersion'];
|
||||||
|
}) => mock<INodeUi>({ id, name, type, position, disabled, issues, typeVersion });
|
||||||
|
|
||||||
export const mockNodeTypeDescription = ({
|
export const mockNodeTypeDescription = ({
|
||||||
name,
|
name,
|
||||||
version = 1,
|
version = 1,
|
||||||
credentials = [],
|
credentials = [],
|
||||||
|
inputs = ['main'],
|
||||||
|
outputs = ['main'],
|
||||||
}: {
|
}: {
|
||||||
name: INodeTypeDescription['name'];
|
name: INodeTypeDescription['name'];
|
||||||
version?: INodeTypeDescription['version'];
|
version?: INodeTypeDescription['version'];
|
||||||
credentials?: INodeTypeDescription['credentials'];
|
credentials?: INodeTypeDescription['credentials'];
|
||||||
|
inputs?: INodeTypeDescription['inputs'];
|
||||||
|
outputs?: INodeTypeDescription['outputs'];
|
||||||
}) =>
|
}) =>
|
||||||
mock<INodeTypeDescription>({
|
mock<INodeTypeDescription>({
|
||||||
name,
|
name,
|
||||||
|
@ -58,8 +68,8 @@ export const mockNodeTypeDescription = ({
|
||||||
properties: [],
|
properties: [],
|
||||||
maxNodes: Infinity,
|
maxNodes: Infinity,
|
||||||
group: EXECUTABLE_TRIGGER_NODE_TYPES.includes(name) ? ['trigger'] : [],
|
group: EXECUTABLE_TRIGGER_NODE_TYPES.includes(name) ? ['trigger'] : [],
|
||||||
inputs: ['main'],
|
inputs,
|
||||||
outputs: ['main'],
|
outputs,
|
||||||
credentials,
|
credentials,
|
||||||
documentationUrl: 'https://docs',
|
documentationUrl: 'https://docs',
|
||||||
webhooks: undefined,
|
webhooks: undefined,
|
||||||
|
|
|
@ -58,6 +58,10 @@ function onNodeDragStop(e: NodeDragEvent) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onSelectionDragStop(e: NodeDragEvent) {
|
||||||
|
onNodeDragStop(e);
|
||||||
|
}
|
||||||
|
|
||||||
function onSetNodeActive(id: string) {
|
function onSetNodeActive(id: string) {
|
||||||
emit('update:node:active', id);
|
emit('update:node:active', id);
|
||||||
}
|
}
|
||||||
|
@ -121,6 +125,7 @@ function onClickPane(event: MouseEvent) {
|
||||||
:max-zoom="2"
|
:max-zoom="2"
|
||||||
data-test-id="canvas"
|
data-test-id="canvas"
|
||||||
@node-drag-stop="onNodeDragStop"
|
@node-drag-stop="onNodeDragStop"
|
||||||
|
@selection-drag-stop="onSelectionDragStop"
|
||||||
@edge-mouse-enter="onMouseEnterEdge"
|
@edge-mouse-enter="onMouseEnterEdge"
|
||||||
@edge-mouse-leave="onMouseLeaveEdge"
|
@edge-mouse-leave="onMouseLeaveEdge"
|
||||||
@pane-click="onClickPane"
|
@pane-click="onClickPane"
|
||||||
|
@ -156,8 +161,6 @@ function onClickPane(event: MouseEvent) {
|
||||||
</VueFlow>
|
</VueFlow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" module></style>
|
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.vue-flow__controls {
|
.vue-flow__controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { fireEvent } from '@testing-library/vue';
|
import { fireEvent } from '@testing-library/vue';
|
||||||
import CanvasEdge from './CanvasEdge.vue';
|
import CanvasEdge from './CanvasEdge.vue';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import { setActivePinia } from 'pinia';
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(CanvasEdge, {
|
const renderComponent = createComponentRenderer(CanvasEdge, {
|
||||||
props: {
|
props: {
|
||||||
|
@ -10,7 +12,15 @@ const renderComponent = createComponentRenderer(CanvasEdge, {
|
||||||
targetX: 100,
|
targetX: 100,
|
||||||
targetY: 100,
|
targetY: 100,
|
||||||
targetPosition: 'bottom',
|
targetPosition: 'bottom',
|
||||||
|
data: {
|
||||||
|
status: undefined,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const pinia = createTestingPinia();
|
||||||
|
setActivePinia(pinia);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('CanvasEdge', () => {
|
describe('CanvasEdge', () => {
|
||||||
|
@ -24,19 +34,12 @@ describe('CanvasEdge', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should compute edgeStyle correctly', () => {
|
it('should compute edgeStyle correctly', () => {
|
||||||
const { container } = renderComponent({
|
const { container } = renderComponent();
|
||||||
props: {
|
|
||||||
style: {
|
|
||||||
stroke: 'red',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const edge = container.querySelector('.vue-flow__edge-path');
|
const edge = container.querySelector('.vue-flow__edge-path');
|
||||||
|
|
||||||
expect(edge).toHaveStyle({
|
expect(edge).toHaveStyle({
|
||||||
stroke: 'red',
|
stroke: 'var(--color-foreground-xdark)',
|
||||||
strokeWidth: 2,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,12 +17,40 @@ const props = defineProps<
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
|
||||||
|
const isFocused = computed(() => props.selected || props.hovered);
|
||||||
|
|
||||||
|
const status = computed(() => props.data.status);
|
||||||
|
const statusColor = computed(() => {
|
||||||
|
if (props.selected) {
|
||||||
|
return 'var(--color-background-dark)';
|
||||||
|
} else if (status.value === 'success') {
|
||||||
|
return 'var(--color-success)';
|
||||||
|
} else if (status.value === 'pinned') {
|
||||||
|
return 'var(--color-secondary)';
|
||||||
|
} else {
|
||||||
|
return 'var(--color-foreground-xdark)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const edgeStyle = computed(() => ({
|
const edgeStyle = computed(() => ({
|
||||||
strokeWidth: 2,
|
|
||||||
...props.style,
|
...props.style,
|
||||||
|
strokeWidth: 2,
|
||||||
|
stroke: statusColor.value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const isEdgeToolbarVisible = computed(() => props.selected || props.hovered);
|
const edgeLabel = computed(() => {
|
||||||
|
if (isFocused.value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.label;
|
||||||
|
});
|
||||||
|
|
||||||
|
const edgeLabelStyle = computed(() => ({
|
||||||
|
fill: statusColor.value,
|
||||||
|
transform: 'translateY(calc(var(--spacing-xs) * -1))',
|
||||||
|
fontSize: 'var(--font-size-xs)',
|
||||||
|
}));
|
||||||
|
|
||||||
const edgeToolbarStyle = computed(() => {
|
const edgeToolbarStyle = computed(() => {
|
||||||
return {
|
return {
|
||||||
|
@ -32,7 +60,7 @@ const edgeToolbarStyle = computed(() => {
|
||||||
|
|
||||||
const edgeToolbarClasses = computed(() => ({
|
const edgeToolbarClasses = computed(() => ({
|
||||||
[$style.edgeToolbar]: true,
|
[$style.edgeToolbar]: true,
|
||||||
[$style.edgeToolbarVisible]: isEdgeToolbarVisible.value,
|
[$style.edgeToolbarVisible]: isFocused.value,
|
||||||
nodrag: true,
|
nodrag: true,
|
||||||
nopan: true,
|
nopan: true,
|
||||||
}));
|
}));
|
||||||
|
@ -63,18 +91,15 @@ function onDelete() {
|
||||||
<template>
|
<template>
|
||||||
<BaseEdge
|
<BaseEdge
|
||||||
:id="id"
|
:id="id"
|
||||||
|
:class="$style.edge"
|
||||||
:style="edgeStyle"
|
:style="edgeStyle"
|
||||||
:path="path[0]"
|
:path="path[0]"
|
||||||
:marker-end="markerEnd"
|
:marker-end="markerEnd"
|
||||||
:label="data?.label"
|
:label="edgeLabel"
|
||||||
:label-x="path[1]"
|
:label-x="path[1]"
|
||||||
:label-y="path[2]"
|
:label-y="path[2]"
|
||||||
:label-style="{ fill: 'white' }"
|
:label-style="edgeLabelStyle"
|
||||||
:label-show-bg="true"
|
:label-show-bg="false"
|
||||||
:label-bg-style="{ fill: 'red' }"
|
|
||||||
:label-bg-padding="[2, 4]"
|
|
||||||
:label-bg-border-radius="2"
|
|
||||||
:class="$style.edge"
|
|
||||||
/>
|
/>
|
||||||
<EdgeLabelRenderer>
|
<EdgeLabelRenderer>
|
||||||
<CanvasEdgeToolbar :class="edgeToolbarClasses" :style="edgeToolbarStyle" @delete="onDelete" />
|
<CanvasEdgeToolbar :class="edgeToolbarClasses" :style="edgeToolbarStyle" @delete="onDelete" />
|
||||||
|
@ -82,6 +107,10 @@ function onDelete() {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
.edge {
|
||||||
|
transition: stroke 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
.edgeToolbar {
|
.edgeToolbar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
import CanvasNodeRenderer from '@/components/canvas/elements/nodes/CanvasNodeRenderer.vue';
|
import CanvasNodeRenderer from '@/components/canvas/elements/nodes/CanvasNodeRenderer.vue';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { createCanvasNodeProvide } from '@/__tests__/data';
|
import { createCanvasNodeProvide } from '@/__tests__/data';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import { setActivePinia } from 'pinia';
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(CanvasNodeRenderer);
|
const renderComponent = createComponentRenderer(CanvasNodeRenderer);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const pinia = createTestingPinia();
|
||||||
|
setActivePinia(pinia);
|
||||||
|
});
|
||||||
|
|
||||||
describe('CanvasNodeRenderer', () => {
|
describe('CanvasNodeRenderer', () => {
|
||||||
it('should render default node correctly', async () => {
|
it('should render default node correctly', async () => {
|
||||||
const { getByTestId } = renderComponent({
|
const { getByTestId } = renderComponent({
|
||||||
|
|
|
@ -2,9 +2,16 @@ import CanvasNodeConfigurable from '@/components/canvas/elements/nodes/render-ty
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
import { createCanvasNodeProvide } from '@/__tests__/data';
|
import { createCanvasNodeProvide } from '@/__tests__/data';
|
||||||
|
import { setActivePinia } from 'pinia';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(CanvasNodeConfigurable);
|
const renderComponent = createComponentRenderer(CanvasNodeConfigurable);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const pinia = createTestingPinia();
|
||||||
|
setActivePinia(pinia);
|
||||||
|
});
|
||||||
|
|
||||||
describe('CanvasNodeConfigurable', () => {
|
describe('CanvasNodeConfigurable', () => {
|
||||||
it('should render node correctly', () => {
|
it('should render node correctly', () => {
|
||||||
const { getByText } = renderComponent({
|
const { getByText } = renderComponent({
|
||||||
|
|
|
@ -1,33 +1,40 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, useCssModule } from 'vue';
|
import { computed, useCssModule } from 'vue';
|
||||||
import { CanvasNodeKey, NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS } from '@/constants';
|
import { NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS } from '@/constants';
|
||||||
import { useNodeConnections } from '@/composables/useNodeConnections';
|
import { useNodeConnections } from '@/composables/useNodeConnections';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
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';
|
||||||
const node = inject(CanvasNodeKey);
|
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const label = computed(() => node?.label.value ?? '');
|
const {
|
||||||
|
label,
|
||||||
const inputs = computed(() => node?.data.value.inputs ?? []);
|
inputs,
|
||||||
const outputs = computed(() => node?.data.value.outputs ?? []);
|
outputs,
|
||||||
const connections = computed(() => node?.data.value.connections ?? { input: {}, output: {} });
|
connections,
|
||||||
|
isDisabled,
|
||||||
|
isSelected,
|
||||||
|
hasPinnedData,
|
||||||
|
hasRunData,
|
||||||
|
hasIssues,
|
||||||
|
} = useCanvasNode();
|
||||||
const { nonMainInputs, requiredNonMainInputs } = useNodeConnections({
|
const { nonMainInputs, requiredNonMainInputs } = useNodeConnections({
|
||||||
inputs,
|
inputs,
|
||||||
outputs,
|
outputs,
|
||||||
connections,
|
connections,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isDisabled = computed(() => node?.data.value.disabled ?? false);
|
|
||||||
|
|
||||||
const classes = computed(() => {
|
const classes = computed(() => {
|
||||||
return {
|
return {
|
||||||
[$style.node]: true,
|
[$style.node]: true,
|
||||||
[$style.selected]: node?.selected.value,
|
[$style.selected]: isSelected.value,
|
||||||
[$style.disabled]: isDisabled.value,
|
[$style.disabled]: isDisabled.value,
|
||||||
|
[$style.success]: hasRunData.value,
|
||||||
|
[$style.error]: hasIssues.value,
|
||||||
|
[$style.pinned]: hasPinnedData.value,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -54,6 +61,7 @@ const styles = computed(() => {
|
||||||
<template>
|
<template>
|
||||||
<div :class="classes" :style="styles" data-test-id="canvas-node-configurable">
|
<div :class="classes" :style="styles" data-test-id="canvas-node-configurable">
|
||||||
<slot />
|
<slot />
|
||||||
|
<CanvasNodeStatusIcons :class="$style.statusIcons" />
|
||||||
<CanvasNodeDisabledStrikeThrough v-if="isDisabled" />
|
<CanvasNodeDisabledStrikeThrough v-if="isDisabled" />
|
||||||
<div :class="$style.label">
|
<div :class="$style.label">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
|
@ -80,6 +88,31 @@ const styles = computed(() => {
|
||||||
background: var(--canvas-node--background, var(--color-node-background));
|
background: var(--canvas-node--background, var(--color-node-background));
|
||||||
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);
|
||||||
|
|
||||||
|
&.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 {
|
.label {
|
||||||
|
@ -93,11 +126,9 @@ const styles = computed(() => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected {
|
.statusIcons {
|
||||||
box-shadow: 0 0 0 4px var(--color-canvas-selected);
|
position: absolute;
|
||||||
}
|
top: calc(var(--canvas-node--height) - 24px);
|
||||||
|
right: var(--spacing-xs);
|
||||||
.disabled {
|
|
||||||
border-color: var(--color-canvas-node-disabled-border, var(--color-foreground-base));
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
import CanvasNodeConfiguration from '@/components/canvas/elements/nodes/render-types/CanvasNodeConfiguration.vue';
|
import CanvasNodeConfiguration from '@/components/canvas/elements/nodes/render-types/CanvasNodeConfiguration.vue';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { createCanvasNodeProvide } from '@/__tests__/data';
|
import { createCanvasNodeProvide } from '@/__tests__/data';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import { setActivePinia } from 'pinia';
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(CanvasNodeConfiguration);
|
const renderComponent = createComponentRenderer(CanvasNodeConfiguration);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const pinia = createTestingPinia();
|
||||||
|
setActivePinia(pinia);
|
||||||
|
});
|
||||||
|
|
||||||
describe('CanvasNodeConfiguration', () => {
|
describe('CanvasNodeConfiguration', () => {
|
||||||
it('should render node correctly', () => {
|
it('should render node correctly', () => {
|
||||||
const { getByText } = renderComponent({
|
const { getByText } = renderComponent({
|
||||||
|
|
|
@ -1,22 +1,20 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, useCssModule } from 'vue';
|
import { computed, useCssModule } from 'vue';
|
||||||
import { CanvasNodeKey } from '@/constants';
|
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import CanvasNodeStatusIcons from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue';
|
||||||
const node = inject(CanvasNodeKey);
|
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const label = computed(() => node?.label.value ?? '');
|
const { label, isDisabled, isSelected, hasIssues } = useCanvasNode();
|
||||||
|
|
||||||
const isDisabled = computed(() => node?.data.value.disabled ?? false);
|
|
||||||
|
|
||||||
const classes = computed(() => {
|
const classes = computed(() => {
|
||||||
return {
|
return {
|
||||||
[$style.node]: true,
|
[$style.node]: true,
|
||||||
[$style.selected]: node?.selected.value,
|
[$style.selected]: isSelected.value,
|
||||||
[$style.disabled]: isDisabled.value,
|
[$style.disabled]: isDisabled.value,
|
||||||
|
[$style.error]: hasIssues.value,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -24,6 +22,7 @@ const classes = computed(() => {
|
||||||
<template>
|
<template>
|
||||||
<div :class="classes" data-test-id="canvas-node-configuration">
|
<div :class="classes" data-test-id="canvas-node-configuration">
|
||||||
<slot />
|
<slot />
|
||||||
|
<CanvasNodeStatusIcons :class="$style.statusIcons" />
|
||||||
<div v-if="label" :class="$style.label">
|
<div v-if="label" :class="$style.label">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
<div v-if="isDisabled">({{ i18n.baseText('node.disabled') }})</div>
|
<div v-if="isDisabled">({{ i18n.baseText('node.disabled') }})</div>
|
||||||
|
@ -44,6 +43,23 @@ const classes = computed(() => {
|
||||||
background: var(--canvas-node--background, var(--node-type-supplemental-background));
|
background: var(--canvas-node--background, var(--node-type-supplemental-background));
|
||||||
border: 2px solid var(--canvas-node--border-color, var(--color-foreground-dark));
|
border: 2px solid var(--canvas-node--border-color, var(--color-foreground-dark));
|
||||||
border-radius: 50%;
|
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 {
|
.label {
|
||||||
|
@ -56,11 +72,8 @@ const classes = computed(() => {
|
||||||
margin-top: var(--spacing-2xs);
|
margin-top: var(--spacing-2xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected {
|
.statusIcons {
|
||||||
box-shadow: 0 0 0 4px var(--color-canvas-selected);
|
position: absolute;
|
||||||
}
|
top: calc(var(--canvas-node--height) - 24px);
|
||||||
|
|
||||||
.disabled {
|
|
||||||
border-color: var(--color-canvas-node-disabled-border, var(--color-foreground-base));
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,9 +2,16 @@ import CanvasNodeDefault from '@/components/canvas/elements/nodes/render-types/C
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
import { createCanvasNodeProvide } from '@/__tests__/data';
|
import { createCanvasNodeProvide } from '@/__tests__/data';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import { setActivePinia } from 'pinia';
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(CanvasNodeDefault);
|
const renderComponent = createComponentRenderer(CanvasNodeDefault);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const pinia = createTestingPinia();
|
||||||
|
setActivePinia(pinia);
|
||||||
|
});
|
||||||
|
|
||||||
describe('CanvasNodeDefault', () => {
|
describe('CanvasNodeDefault', () => {
|
||||||
it('should render node correctly', () => {
|
it('should render node correctly', () => {
|
||||||
const { getByText } = renderComponent({
|
const { getByText } = renderComponent({
|
||||||
|
|
|
@ -1,33 +1,39 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, useCssModule } from 'vue';
|
import { computed, useCssModule } from 'vue';
|
||||||
import { useNodeConnections } from '@/composables/useNodeConnections';
|
import { useNodeConnections } from '@/composables/useNodeConnections';
|
||||||
import { CanvasNodeKey } from '@/constants';
|
|
||||||
import { useI18n } from '@/composables/useI18n';
|
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';
|
||||||
const node = inject(CanvasNodeKey);
|
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const label = computed(() => node?.label.value ?? '');
|
const {
|
||||||
|
label,
|
||||||
const inputs = computed(() => node?.data.value.inputs ?? []);
|
inputs,
|
||||||
const outputs = computed(() => node?.data.value.outputs ?? []);
|
outputs,
|
||||||
const connections = computed(() => node?.data.value.connections ?? { input: {}, output: {} });
|
connections,
|
||||||
|
isDisabled,
|
||||||
|
isSelected,
|
||||||
|
hasPinnedData,
|
||||||
|
hasRunData,
|
||||||
|
hasIssues,
|
||||||
|
} = useCanvasNode();
|
||||||
const { mainOutputs } = useNodeConnections({
|
const { mainOutputs } = useNodeConnections({
|
||||||
inputs,
|
inputs,
|
||||||
outputs,
|
outputs,
|
||||||
connections,
|
connections,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isDisabled = computed(() => node?.data.value.disabled ?? false);
|
|
||||||
|
|
||||||
const classes = computed(() => {
|
const classes = computed(() => {
|
||||||
return {
|
return {
|
||||||
[$style.node]: true,
|
[$style.node]: true,
|
||||||
[$style.selected]: node?.selected.value,
|
[$style.selected]: isSelected.value,
|
||||||
[$style.disabled]: isDisabled.value,
|
[$style.disabled]: isDisabled.value,
|
||||||
|
[$style.success]: hasRunData.value,
|
||||||
|
[$style.error]: hasIssues.value,
|
||||||
|
[$style.pinned]: hasPinnedData.value,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -39,8 +45,9 @@ const styles = computed(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="node" :class="classes" :style="styles" data-test-id="canvas-node-default">
|
<div :class="classes" :style="styles" data-test-id="canvas-node-default">
|
||||||
<slot />
|
<slot />
|
||||||
|
<CanvasNodeStatusIcons :class="$style.statusIcons" />
|
||||||
<CanvasNodeDisabledStrikeThrough v-if="isDisabled" />
|
<CanvasNodeDisabledStrikeThrough v-if="isDisabled" />
|
||||||
<div v-if="label" :class="$style.label">
|
<div v-if="label" :class="$style.label">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
|
@ -62,6 +69,31 @@ const styles = computed(() => {
|
||||||
background: var(--canvas-node--background, var(--color-node-background));
|
background: var(--canvas-node--background, var(--color-node-background));
|
||||||
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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
border-color: var(--color-canvas-node-success-border-color, var(--color-success));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
border-color: var(--color-canvas-node-error-border-color, var(--color-danger));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pinned {
|
||||||
|
border-color: var(--color-canvas-node-pinned-border-color, var(--color-node-pinned-border));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
border-color: var(--color-canvas-node-disabled-border-color, var(--color-foreground-base));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
|
@ -74,11 +106,9 @@ const styles = computed(() => {
|
||||||
margin-top: var(--spacing-2xs);
|
margin-top: var(--spacing-2xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected {
|
.statusIcons {
|
||||||
box-shadow: 0 0 0 4px var(--color-canvas-selected);
|
position: absolute;
|
||||||
}
|
top: calc(var(--canvas-node--height) - 24px);
|
||||||
|
right: var(--spacing-xs);
|
||||||
.disabled {
|
|
||||||
border-color: var(--color-canvas-node-disabled-border, var(--color-foreground-base));
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, inject, useCssModule } from 'vue';
|
import { computed, useCssModule } from 'vue';
|
||||||
import { CanvasNodeKey } from '@/constants';
|
|
||||||
import { useNodeConnections } from '@/composables/useNodeConnections';
|
import { useNodeConnections } from '@/composables/useNodeConnections';
|
||||||
|
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
const node = inject(CanvasNodeKey);
|
|
||||||
|
|
||||||
const inputs = computed(() => node?.data.value.inputs ?? []);
|
const { inputs, outputs, connections } = useCanvasNode();
|
||||||
const outputs = computed(() => node?.data.value.outputs ?? []);
|
|
||||||
const connections = computed(() => node?.data.value.connections ?? { input: {}, output: {} });
|
|
||||||
const { mainInputConnections, mainOutputConnections } = useNodeConnections({
|
const { mainInputConnections, mainOutputConnections } = useNodeConnections({
|
||||||
inputs,
|
inputs,
|
||||||
outputs,
|
outputs,
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import TitledList from '@/components/TitledList.vue';
|
||||||
|
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||||
|
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||||
|
|
||||||
|
const nodeHelpers = useNodeHelpers();
|
||||||
|
|
||||||
|
const {
|
||||||
|
pinnedDataCount,
|
||||||
|
hasPinnedData,
|
||||||
|
issues,
|
||||||
|
hasIssues,
|
||||||
|
executionStatus,
|
||||||
|
executionWaiting,
|
||||||
|
hasRunData,
|
||||||
|
runDataCount,
|
||||||
|
} = useCanvasNode();
|
||||||
|
|
||||||
|
const hideNodeIssues = computed(() => false); // @TODO Implement this
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="$style.canvasNodeStatusIcons">
|
||||||
|
<div v-if="hasIssues && !hideNodeIssues" :class="$style.issues" data-test-id="node-issues">
|
||||||
|
<N8nTooltip :show-after="500" placement="bottom">
|
||||||
|
<template #content>
|
||||||
|
<TitledList :title="`${$locale.baseText('node.issues')}:`" :items="issues" />
|
||||||
|
</template>
|
||||||
|
<FontAwesomeIcon icon="exclamation-triangle" />
|
||||||
|
</N8nTooltip>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="executionWaiting || executionStatus === 'waiting'" class="waiting">
|
||||||
|
<N8nTooltip placement="bottom">
|
||||||
|
<template #content>
|
||||||
|
<div v-text="executionWaiting"></div>
|
||||||
|
</template>
|
||||||
|
<FontAwesomeIcon icon="clock" />
|
||||||
|
</N8nTooltip>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
v-else-if="hasPinnedData && !nodeHelpers.isProductionExecutionPreview.value"
|
||||||
|
:class="$style.pinnedData"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon="thumbtack" />
|
||||||
|
<span v-if="pinnedDataCount > 1" class="items-count"> {{ pinnedDataCount }}</span>
|
||||||
|
</span>
|
||||||
|
<span v-else-if="executionStatus === 'unknown'">
|
||||||
|
<!-- Do nothing, unknown means the node never executed -->
|
||||||
|
</span>
|
||||||
|
<span v-else-if="hasRunData" :class="$style.runData">
|
||||||
|
<FontAwesomeIcon icon="check" />
|
||||||
|
<span v-if="runDataCount > 1" :class="$style.itemsCount"> {{ runDataCount }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.canvasNodeStatusIcons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runData {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pinnedData {
|
||||||
|
color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.issues {
|
||||||
|
color: var(--color-danger);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemsCount {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -21,6 +21,7 @@ import { useRouter } from 'vue-router';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||||
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
|
|
||||||
vi.mock('vue-router', async () => {
|
vi.mock('vue-router', async () => {
|
||||||
const actual = await import('vue-router');
|
const actual = await import('vue-router');
|
||||||
|
@ -39,11 +40,12 @@ describe('useCanvasOperations', () => {
|
||||||
let nodeTypesStore: ReturnType<typeof useNodeTypesStore>;
|
let nodeTypesStore: ReturnType<typeof useNodeTypesStore>;
|
||||||
let credentialsStore: ReturnType<typeof useCredentialsStore>;
|
let credentialsStore: ReturnType<typeof useCredentialsStore>;
|
||||||
let canvasOperations: ReturnType<typeof useCanvasOperations>;
|
let canvasOperations: ReturnType<typeof useCanvasOperations>;
|
||||||
|
let workflowHelpers: ReturnType<typeof useWorkflowHelpers>;
|
||||||
|
|
||||||
const lastClickPosition = ref<XYPosition>([450, 450]);
|
const lastClickPosition = ref<XYPosition>([450, 450]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
setActivePinia(pinia);
|
setActivePinia(pinia);
|
||||||
|
|
||||||
|
@ -53,15 +55,17 @@ describe('useCanvasOperations', () => {
|
||||||
historyStore = useHistoryStore();
|
historyStore = useHistoryStore();
|
||||||
nodeTypesStore = useNodeTypesStore();
|
nodeTypesStore = useNodeTypesStore();
|
||||||
credentialsStore = useCredentialsStore();
|
credentialsStore = useCredentialsStore();
|
||||||
|
workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
|
||||||
const workflowId = 'test';
|
const workflowId = 'test';
|
||||||
workflowsStore.workflowsById[workflowId] = mock<IWorkflowDb>({
|
const workflow = mock<IWorkflowDb>({
|
||||||
id: workflowId,
|
id: workflowId,
|
||||||
nodes: [],
|
nodes: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
usedCredentials: [],
|
usedCredentials: [],
|
||||||
});
|
});
|
||||||
workflowsStore.initializeEditableWorkflow(workflowId);
|
workflowsStore.workflowsById[workflowId] = workflow;
|
||||||
|
await workflowHelpers.initState(workflow, true);
|
||||||
|
|
||||||
canvasOperations = useCanvasOperations({ router, lastClickPosition });
|
canvasOperations = useCanvasOperations({ router, lastClickPosition });
|
||||||
});
|
});
|
||||||
|
@ -506,13 +510,13 @@ describe('useCanvasOperations', () => {
|
||||||
connection: [
|
connection: [
|
||||||
{
|
{
|
||||||
index: 0,
|
index: 0,
|
||||||
node: 'Node B',
|
node: 'Node A',
|
||||||
type: 'main',
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: 0,
|
index: 0,
|
||||||
node: 'spy',
|
node: 'Node B',
|
||||||
type: 'main',
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -567,6 +571,8 @@ describe('useCanvasOperations', () => {
|
||||||
name: 'Node B',
|
name: 'Node B',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.spyOn(workflowsStore, 'getNodeById').mockReturnValueOnce(nodeA).mockReturnValueOnce(nodeB);
|
||||||
|
|
||||||
const connection: Connection = {
|
const connection: Connection = {
|
||||||
source: nodeA.id,
|
source: nodeA.id,
|
||||||
sourceHandle: `outputs/${NodeConnectionType.Main}/0`,
|
sourceHandle: `outputs/${NodeConnectionType.Main}/0`,
|
||||||
|
@ -574,7 +580,14 @@ describe('useCanvasOperations', () => {
|
||||||
targetHandle: `inputs/${NodeConnectionType.Main}/0`,
|
targetHandle: `inputs/${NodeConnectionType.Main}/0`,
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.spyOn(workflowsStore, 'getNodeById').mockReturnValueOnce(nodeA).mockReturnValueOnce(nodeB);
|
const nodeTypeDescription = mockNodeTypeDescription({
|
||||||
|
name: 'node',
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeTypesStore.setNodeTypes([nodeTypeDescription]);
|
||||||
|
canvasOperations.editableWorkflowObject.value.nodes[nodeA.name] = nodeA;
|
||||||
|
canvasOperations.editableWorkflowObject.value.nodes[nodeB.name] = nodeB;
|
||||||
|
|
||||||
canvasOperations.createConnection(connection);
|
canvasOperations.createConnection(connection);
|
||||||
|
|
||||||
|
@ -588,6 +601,189 @@ describe('useCanvasOperations', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isConnectionAllowed', () => {
|
||||||
|
it('should return false if source and target nodes are the same', () => {
|
||||||
|
const node = mockNode({ id: '1', type: 'testType', name: 'Test Node' });
|
||||||
|
expect(canvasOperations.isConnectionAllowed(node, node, NodeConnectionType.Main)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if target node type does not have inputs', () => {
|
||||||
|
const sourceNode = mockNode({
|
||||||
|
id: '1',
|
||||||
|
type: 'sourceType',
|
||||||
|
name: 'Source Node',
|
||||||
|
});
|
||||||
|
const targetNode = mockNode({
|
||||||
|
id: '2',
|
||||||
|
type: 'targetType',
|
||||||
|
name: 'Target Node',
|
||||||
|
});
|
||||||
|
const nodeTypeDescription = mockNodeTypeDescription({
|
||||||
|
name: 'targetType',
|
||||||
|
inputs: [],
|
||||||
|
});
|
||||||
|
nodeTypesStore.setNodeTypes([nodeTypeDescription]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
canvasOperations.isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if target node does not exist in the workflow', () => {
|
||||||
|
const sourceNode = mockNode({
|
||||||
|
id: '1',
|
||||||
|
type: 'sourceType',
|
||||||
|
name: 'Source Node',
|
||||||
|
});
|
||||||
|
const targetNode = mockNode({
|
||||||
|
id: '2',
|
||||||
|
type: 'targetType',
|
||||||
|
name: 'Target Node',
|
||||||
|
});
|
||||||
|
const nodeTypeDescription = mockNodeTypeDescription({
|
||||||
|
name: 'targetType',
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
});
|
||||||
|
nodeTypesStore.setNodeTypes([nodeTypeDescription]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
canvasOperations.isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if input type does not match connection type', () => {
|
||||||
|
const sourceNode = mockNode({
|
||||||
|
id: '1',
|
||||||
|
type: 'sourceType',
|
||||||
|
name: 'Source Node',
|
||||||
|
});
|
||||||
|
|
||||||
|
const targetNode = mockNode({
|
||||||
|
id: '2',
|
||||||
|
type: 'targetType',
|
||||||
|
name: 'Target Node',
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodeTypeDescription = mockNodeTypeDescription({
|
||||||
|
name: 'targetType',
|
||||||
|
inputs: [NodeConnectionType.AiTool],
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeTypesStore.setNodeTypes([nodeTypeDescription]);
|
||||||
|
canvasOperations.editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
|
||||||
|
canvasOperations.editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
canvasOperations.isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if source node type is not allowed by target node input filter', () => {
|
||||||
|
const sourceNode = mockNode({
|
||||||
|
id: '1',
|
||||||
|
type: 'sourceType',
|
||||||
|
name: 'Source Node',
|
||||||
|
typeVersion: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const targetNode = mockNode({
|
||||||
|
id: '2',
|
||||||
|
type: 'targetType',
|
||||||
|
name: 'Target Node',
|
||||||
|
typeVersion: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodeTypeDescription = mockNodeTypeDescription({
|
||||||
|
name: 'targetType',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
filter: {
|
||||||
|
nodes: ['allowedType'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeTypesStore.setNodeTypes([nodeTypeDescription]);
|
||||||
|
canvasOperations.editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
|
||||||
|
canvasOperations.editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
canvasOperations.isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if all conditions including filter are met', () => {
|
||||||
|
const sourceNode = mockNode({
|
||||||
|
id: '1',
|
||||||
|
type: 'sourceType',
|
||||||
|
name: 'Source Node',
|
||||||
|
typeVersion: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const targetNode = mockNode({
|
||||||
|
id: '2',
|
||||||
|
type: 'targetType',
|
||||||
|
name: 'Target Node',
|
||||||
|
typeVersion: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodeTypeDescription = mockNodeTypeDescription({
|
||||||
|
name: 'targetType',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
filter: {
|
||||||
|
nodes: ['sourceType'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeTypesStore.setNodeTypes([nodeTypeDescription]);
|
||||||
|
canvasOperations.editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
|
||||||
|
canvasOperations.editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
canvasOperations.isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if all conditions are met and no filter is set', () => {
|
||||||
|
const sourceNode = mockNode({
|
||||||
|
id: '1',
|
||||||
|
type: 'sourceType',
|
||||||
|
name: 'Source Node',
|
||||||
|
typeVersion: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const targetNode = mockNode({
|
||||||
|
id: '2',
|
||||||
|
type: 'targetType',
|
||||||
|
name: 'Target Node',
|
||||||
|
typeVersion: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodeTypeDescription = mockNodeTypeDescription({
|
||||||
|
name: 'targetType',
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeTypesStore.setNodeTypes([nodeTypeDescription]);
|
||||||
|
canvasOperations.editableWorkflowObject.value.nodes[sourceNode.name] = sourceNode;
|
||||||
|
canvasOperations.editableWorkflowObject.value.nodes[targetNode.name] = targetNode;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
canvasOperations.isConnectionAllowed(sourceNode, targetNode, NodeConnectionType.Main),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('deleteConnection', () => {
|
describe('deleteConnection', () => {
|
||||||
it('should not delete a connection if source node does not exist', () => {
|
it('should not delete a connection if source node does not exist', () => {
|
||||||
const removeConnectionSpy = vi
|
const removeConnectionSpy = vi
|
||||||
|
|
|
@ -7,24 +7,27 @@ import { mock } from 'vitest-mock-extended';
|
||||||
|
|
||||||
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
||||||
import type { IWorkflowDb } from '@/Interface';
|
import type { IWorkflowDb } from '@/Interface';
|
||||||
import { createTestWorkflowObject, mockNode, mockNodes } from '@/__tests__/mocks';
|
import {
|
||||||
import { MANUAL_TRIGGER_NODE_TYPE } from '@/constants';
|
createTestWorkflowObject,
|
||||||
|
mockNode,
|
||||||
vi.mock('@/stores/nodeTypes.store', () => ({
|
mockNodes,
|
||||||
useNodeTypesStore: vi.fn(() => ({
|
mockNodeTypeDescription,
|
||||||
getNodeType: vi.fn(() => ({
|
} from '@/__tests__/mocks';
|
||||||
name: 'test',
|
import { MANUAL_TRIGGER_NODE_TYPE, SET_NODE_TYPE } from '@/constants';
|
||||||
description: 'Test Node Description',
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
})),
|
|
||||||
isTriggerNode: vi.fn(),
|
|
||||||
isConfigNode: vi.fn(),
|
|
||||||
isConfigurableNode: vi.fn(),
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
setActivePinia(pinia);
|
setActivePinia(pinia);
|
||||||
|
|
||||||
|
useNodeTypesStore().setNodeTypes([
|
||||||
|
mockNodeTypeDescription({
|
||||||
|
name: MANUAL_TRIGGER_NODE_TYPE,
|
||||||
|
}),
|
||||||
|
mockNodeTypeDescription({
|
||||||
|
name: SET_NODE_TYPE,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -75,13 +78,41 @@ describe('useCanvasMapping', () => {
|
||||||
type: manualTriggerNode.type,
|
type: manualTriggerNode.type,
|
||||||
typeVersion: expect.anything(),
|
typeVersion: expect.anything(),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
inputs: [],
|
execution: {
|
||||||
outputs: [],
|
status: 'new',
|
||||||
|
waiting: undefined,
|
||||||
|
},
|
||||||
|
issues: {
|
||||||
|
items: [],
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
pinnedData: {
|
||||||
|
count: 0,
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
runData: {
|
||||||
|
count: 0,
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
label: undefined,
|
||||||
|
type: 'main',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
label: undefined,
|
||||||
|
type: 'main',
|
||||||
|
},
|
||||||
|
],
|
||||||
connections: {
|
connections: {
|
||||||
input: {},
|
input: {},
|
||||||
output: {},
|
output: {},
|
||||||
},
|
},
|
||||||
renderType: 'default',
|
renderType: 'trigger',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -173,6 +204,7 @@ describe('useCanvasMapping', () => {
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
|
status: undefined,
|
||||||
target: {
|
target: {
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
|
@ -219,6 +251,7 @@ describe('useCanvasMapping', () => {
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.AiTool,
|
type: NodeConnectionType.AiTool,
|
||||||
},
|
},
|
||||||
|
status: undefined,
|
||||||
target: {
|
target: {
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.AiTool,
|
type: NodeConnectionType.AiTool,
|
||||||
|
@ -239,6 +272,7 @@ describe('useCanvasMapping', () => {
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.AiDocument,
|
type: NodeConnectionType.AiDocument,
|
||||||
},
|
},
|
||||||
|
status: undefined,
|
||||||
target: {
|
target: {
|
||||||
index: 1,
|
index: 1,
|
||||||
type: NodeConnectionType.AiDocument,
|
type: NodeConnectionType.AiDocument,
|
|
@ -1,9 +1,16 @@
|
||||||
|
/**
|
||||||
|
* Canvas V2 Only
|
||||||
|
* @TODO Remove this notice when Canvas V2 is the only one in use
|
||||||
|
*/
|
||||||
|
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import type {
|
import type {
|
||||||
CanvasConnection,
|
CanvasConnection,
|
||||||
|
CanvasConnectionData,
|
||||||
CanvasConnectionPort,
|
CanvasConnectionPort,
|
||||||
CanvasElement,
|
CanvasElement,
|
||||||
CanvasElementData,
|
CanvasElementData,
|
||||||
|
@ -12,9 +19,17 @@ import {
|
||||||
mapLegacyConnectionsToCanvasConnections,
|
mapLegacyConnectionsToCanvasConnections,
|
||||||
mapLegacyEndpointsToCanvasConnectionPort,
|
mapLegacyEndpointsToCanvasConnectionPort,
|
||||||
} from '@/utils/canvasUtilsV2';
|
} from '@/utils/canvasUtilsV2';
|
||||||
import type { Workflow } from 'n8n-workflow';
|
import type {
|
||||||
|
ExecutionStatus,
|
||||||
|
ExecutionSummary,
|
||||||
|
INodeExecutionData,
|
||||||
|
ITaskData,
|
||||||
|
Workflow,
|
||||||
|
} from 'n8n-workflow';
|
||||||
import { NodeHelpers } from 'n8n-workflow';
|
import { NodeHelpers } from 'n8n-workflow';
|
||||||
import type { IWorkflowDb } from '@/Interface';
|
import type { IWorkflowDb } from '@/Interface';
|
||||||
|
import { WAIT_TIME_UNLIMITED } from '@/constants';
|
||||||
|
import { sanitizeHtml } from '@/utils/htmlUtils';
|
||||||
|
|
||||||
export function useCanvasMapping({
|
export function useCanvasMapping({
|
||||||
workflow,
|
workflow,
|
||||||
|
@ -23,7 +38,8 @@ export function useCanvasMapping({
|
||||||
workflow: Ref<IWorkflowDb>;
|
workflow: Ref<IWorkflowDb>;
|
||||||
workflowObject: Ref<Workflow>;
|
workflowObject: Ref<Workflow>;
|
||||||
}) {
|
}) {
|
||||||
const locale = useI18n();
|
const i18n = useI18n();
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
|
||||||
const renderTypeByNodeType = computed(
|
const renderTypeByNodeType = computed(
|
||||||
|
@ -87,6 +103,97 @@ export function useCanvasMapping({
|
||||||
}, {}),
|
}, {}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const nodePinnedDataById = computed(() =>
|
||||||
|
workflow.value.nodes.reduce<Record<string, INodeExecutionData[] | undefined>>((acc, node) => {
|
||||||
|
acc[node.id] = workflowsStore.pinDataByNodeName(node.name);
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const nodeExecutionStatusById = computed(() =>
|
||||||
|
workflow.value.nodes.reduce<Record<string, ExecutionStatus>>((acc, node) => {
|
||||||
|
acc[node.id] =
|
||||||
|
workflowsStore.getWorkflowRunData?.[node.name]?.filter(Boolean)[0].executionStatus ?? 'new';
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const nodeExecutionRunDataById = computed(() =>
|
||||||
|
workflow.value.nodes.reduce<Record<string, ITaskData[] | null>>((acc, node) => {
|
||||||
|
acc[node.id] = workflowsStore.getWorkflowResultDataByNodeName(node.name);
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const nodeIssuesById = computed(() =>
|
||||||
|
workflow.value.nodes.reduce<Record<string, string[]>>((acc, node) => {
|
||||||
|
const issues: string[] = [];
|
||||||
|
const nodeExecutionRunData = workflowsStore.getWorkflowRunData?.[node.name];
|
||||||
|
if (nodeExecutionRunData) {
|
||||||
|
nodeExecutionRunData.forEach((executionRunData) => {
|
||||||
|
if (executionRunData?.error) {
|
||||||
|
const { message, description } = executionRunData.error;
|
||||||
|
const issue = `${message}${description ? ` (${description})` : ''}`;
|
||||||
|
issues.push(sanitizeHtml(issue));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node?.issues !== undefined) {
|
||||||
|
issues.push(...NodeHelpers.nodeIssuesToString(node.issues, node));
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[node.id] = issues;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const nodeHasIssuesById = computed(() =>
|
||||||
|
workflow.value.nodes.reduce<Record<string, boolean>>((acc, node) => {
|
||||||
|
if (['crashed', 'error'].includes(nodeExecutionStatusById.value[node.id])) {
|
||||||
|
acc[node.id] = true;
|
||||||
|
} else if (nodePinnedDataById.value[node.id]) {
|
||||||
|
acc[node.id] = false;
|
||||||
|
} else {
|
||||||
|
acc[node.id] = Object.keys(node?.issues ?? {}).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const nodeExecutionWaitingById = computed(() =>
|
||||||
|
workflow.value.nodes.reduce<Record<string, string | undefined>>((acc, node) => {
|
||||||
|
const isExecutionSummary = (execution: object): execution is ExecutionSummary =>
|
||||||
|
'waitTill' in execution;
|
||||||
|
|
||||||
|
const workflowExecution = workflowsStore.getWorkflowExecution;
|
||||||
|
const lastNodeExecuted = workflowExecution?.data?.resultData?.lastNodeExecuted;
|
||||||
|
|
||||||
|
if (workflowExecution && lastNodeExecuted && isExecutionSummary(workflowExecution)) {
|
||||||
|
if (node.name === workflowExecution.data?.resultData?.lastNodeExecuted) {
|
||||||
|
const waitDate = new Date(workflowExecution.waitTill as Date);
|
||||||
|
|
||||||
|
if (waitDate.toISOString() === WAIT_TIME_UNLIMITED) {
|
||||||
|
acc[node.id] = i18n.baseText(
|
||||||
|
'node.theNodeIsWaitingIndefinitelyForAnIncomingWebhookCall',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[node.id] = i18n.baseText('node.nodeIsWaitingTill', {
|
||||||
|
interpolate: {
|
||||||
|
date: waitDate.toLocaleDateString(),
|
||||||
|
time: waitDate.toLocaleTimeString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
);
|
||||||
|
|
||||||
const elements = computed<CanvasElement[]>(() => [
|
const elements = computed<CanvasElement[]>(() => [
|
||||||
...workflow.value.nodes.map<CanvasElement>((node) => {
|
...workflow.value.nodes.map<CanvasElement>((node) => {
|
||||||
const inputConnections = workflowObject.value.connectionsByDestinationNode[node.name] ?? {};
|
const inputConnections = workflowObject.value.connectionsByDestinationNode[node.name] ?? {};
|
||||||
|
@ -103,6 +210,22 @@ export function useCanvasMapping({
|
||||||
input: inputConnections,
|
input: inputConnections,
|
||||||
output: outputConnections,
|
output: outputConnections,
|
||||||
},
|
},
|
||||||
|
issues: {
|
||||||
|
items: nodeIssuesById.value[node.id],
|
||||||
|
visible: nodeHasIssuesById.value[node.id],
|
||||||
|
},
|
||||||
|
pinnedData: {
|
||||||
|
count: nodePinnedDataById.value[node.id]?.length ?? 0,
|
||||||
|
visible: !!nodePinnedDataById.value[node.id],
|
||||||
|
},
|
||||||
|
execution: {
|
||||||
|
status: nodeExecutionStatusById.value[node.id],
|
||||||
|
waiting: nodeExecutionWaitingById.value[node.id],
|
||||||
|
},
|
||||||
|
runData: {
|
||||||
|
count: nodeExecutionRunDataById.value[node.id]?.length ?? 0,
|
||||||
|
visible: !!nodeExecutionRunDataById.value[node.id],
|
||||||
|
},
|
||||||
renderType: renderTypeByNodeType.value[node.type] ?? 'default',
|
renderType: renderTypeByNodeType.value[node.type] ?? 'default',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -125,26 +248,63 @@ export function useCanvasMapping({
|
||||||
return mappedConnections.map((connection) => {
|
return mappedConnections.map((connection) => {
|
||||||
const type = getConnectionType(connection);
|
const type = getConnectionType(connection);
|
||||||
const label = getConnectionLabel(connection);
|
const label = getConnectionLabel(connection);
|
||||||
|
const data = getConnectionData(connection);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...connection,
|
...connection,
|
||||||
|
data,
|
||||||
type,
|
type,
|
||||||
label,
|
label,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getConnectionData(connection: CanvasConnection): CanvasConnectionData {
|
||||||
|
const fromNode = workflow.value.nodes.find(
|
||||||
|
(node) => node.name === connection.data?.fromNodeName,
|
||||||
|
);
|
||||||
|
|
||||||
|
let status: CanvasConnectionData['status'];
|
||||||
|
if (fromNode) {
|
||||||
|
if (nodePinnedDataById.value[fromNode.id] && nodeExecutionRunDataById.value[fromNode.id]) {
|
||||||
|
status = 'pinned';
|
||||||
|
} else if (nodeHasIssuesById.value[fromNode.id]) {
|
||||||
|
status = 'error';
|
||||||
|
} else if (nodeExecutionRunDataById.value[fromNode.id]) {
|
||||||
|
status = 'success';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...(connection.data as CanvasConnectionData),
|
||||||
|
status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getConnectionType(_: CanvasConnection): string {
|
function getConnectionType(_: CanvasConnection): string {
|
||||||
return 'canvas-edge';
|
return 'canvas-edge';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConnectionLabel(connection: CanvasConnection): string {
|
function getConnectionLabel(connection: CanvasConnection): string {
|
||||||
const pinData = workflow.value.pinData?.[connection.data?.fromNodeName ?? ''];
|
const fromNode = workflow.value.nodes.find(
|
||||||
|
(node) => node.name === connection.data?.fromNodeName,
|
||||||
|
);
|
||||||
|
|
||||||
if (pinData?.length) {
|
if (!fromNode) {
|
||||||
return locale.baseText('ndv.output.items', {
|
return '';
|
||||||
adjustToNumber: pinData.length,
|
}
|
||||||
interpolate: { count: String(pinData.length) },
|
|
||||||
|
if (nodePinnedDataById.value[fromNode.id]) {
|
||||||
|
const pinnedDataCount = nodePinnedDataById.value[fromNode.id]?.length ?? 0;
|
||||||
|
return i18n.baseText('ndv.output.items', {
|
||||||
|
adjustToNumber: pinnedDataCount,
|
||||||
|
interpolate: { count: String(pinnedDataCount) },
|
||||||
|
});
|
||||||
|
} else if (nodeExecutionRunDataById.value[fromNode.id]) {
|
||||||
|
const runDataCount = nodeExecutionRunDataById.value[fromNode.id]?.length ?? 0;
|
||||||
|
return i18n.baseText('ndv.output.items', {
|
||||||
|
adjustToNumber: runDataCount,
|
||||||
|
interpolate: { count: String(runDataCount) },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
73
packages/editor-ui/src/composables/useCanvasNode.spec.ts
Normal file
73
packages/editor-ui/src/composables/useCanvasNode.spec.ts
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||||
|
import { inject, ref } from 'vue';
|
||||||
|
|
||||||
|
vi.mock('vue', async () => {
|
||||||
|
const actual = await vi.importActual('vue');
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
inject: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useCanvasNode', () => {
|
||||||
|
it('should return default values when node is not provided', () => {
|
||||||
|
const result = useCanvasNode();
|
||||||
|
|
||||||
|
expect(result.label.value).toBe('');
|
||||||
|
expect(result.inputs.value).toEqual([]);
|
||||||
|
expect(result.outputs.value).toEqual([]);
|
||||||
|
expect(result.connections.value).toEqual({ input: {}, output: {} });
|
||||||
|
expect(result.isDisabled.value).toBe(false);
|
||||||
|
expect(result.isSelected.value).toBeUndefined();
|
||||||
|
expect(result.pinnedDataCount.value).toBe(0);
|
||||||
|
expect(result.hasPinnedData.value).toBe(false);
|
||||||
|
expect(result.runDataCount.value).toBe(0);
|
||||||
|
expect(result.hasRunData.value).toBe(false);
|
||||||
|
expect(result.issues.value).toEqual([]);
|
||||||
|
expect(result.hasIssues.value).toBe(false);
|
||||||
|
expect(result.executionStatus.value).toBeUndefined();
|
||||||
|
expect(result.executionWaiting.value).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return node data when node is provided', () => {
|
||||||
|
const node = {
|
||||||
|
data: {
|
||||||
|
value: {
|
||||||
|
id: 'node1',
|
||||||
|
type: 'nodeType1',
|
||||||
|
typeVersion: 1,
|
||||||
|
disabled: true,
|
||||||
|
inputs: ['input1'],
|
||||||
|
outputs: ['output1'],
|
||||||
|
connections: { input: { '0': ['node2'] }, output: {} },
|
||||||
|
issues: { items: ['issue1'], visible: true },
|
||||||
|
execution: { status: 'running', waiting: false },
|
||||||
|
runData: { count: 1, visible: true },
|
||||||
|
pinnedData: { count: 1, visible: true },
|
||||||
|
renderType: 'default',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
label: ref('Node 1'),
|
||||||
|
selected: ref(true),
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(inject).mockReturnValue(node);
|
||||||
|
|
||||||
|
const result = useCanvasNode();
|
||||||
|
|
||||||
|
expect(result.label.value).toBe('Node 1');
|
||||||
|
expect(result.inputs.value).toEqual(['input1']);
|
||||||
|
expect(result.outputs.value).toEqual(['output1']);
|
||||||
|
expect(result.connections.value).toEqual({ input: { '0': ['node2'] }, output: {} });
|
||||||
|
expect(result.isDisabled.value).toBe(true);
|
||||||
|
expect(result.isSelected.value).toBe(true);
|
||||||
|
expect(result.pinnedDataCount.value).toBe(1);
|
||||||
|
expect(result.hasPinnedData.value).toBe(true);
|
||||||
|
expect(result.runDataCount.value).toBe(1);
|
||||||
|
expect(result.hasRunData.value).toBe(true);
|
||||||
|
expect(result.issues.value).toEqual(['issue1']);
|
||||||
|
expect(result.hasIssues.value).toBe(true);
|
||||||
|
expect(result.executionStatus.value).toBe('running');
|
||||||
|
expect(result.executionWaiting.value).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
69
packages/editor-ui/src/composables/useCanvasNode.ts
Normal file
69
packages/editor-ui/src/composables/useCanvasNode.ts
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
/**
|
||||||
|
* Canvas V2 Only
|
||||||
|
* @TODO Remove this notice when Canvas V2 is the only one in use
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CanvasNodeKey } from '@/constants';
|
||||||
|
import { computed, inject } from 'vue';
|
||||||
|
import type { CanvasElementData } from '@/types';
|
||||||
|
|
||||||
|
export function useCanvasNode() {
|
||||||
|
const node = inject(CanvasNodeKey);
|
||||||
|
const data = computed<CanvasElementData>(
|
||||||
|
() =>
|
||||||
|
node?.data.value ?? {
|
||||||
|
id: '',
|
||||||
|
type: '',
|
||||||
|
typeVersion: 1,
|
||||||
|
disabled: false,
|
||||||
|
inputs: [],
|
||||||
|
outputs: [],
|
||||||
|
connections: { input: {}, output: {} },
|
||||||
|
issues: { items: [], visible: false },
|
||||||
|
pinnedData: { count: 0, visible: false },
|
||||||
|
execution: {},
|
||||||
|
runData: { count: 0, visible: false },
|
||||||
|
renderType: 'default',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const label = computed(() => node?.label.value ?? '');
|
||||||
|
|
||||||
|
const inputs = computed(() => data.value.inputs);
|
||||||
|
const outputs = computed(() => data.value.outputs);
|
||||||
|
const connections = computed(() => data.value.connections);
|
||||||
|
|
||||||
|
const isDisabled = computed(() => data.value.disabled);
|
||||||
|
|
||||||
|
const isSelected = computed(() => node?.selected.value);
|
||||||
|
|
||||||
|
const pinnedDataCount = computed(() => data.value.pinnedData.count);
|
||||||
|
const hasPinnedData = computed(() => data.value.pinnedData.count > 0);
|
||||||
|
|
||||||
|
const issues = computed(() => data.value.issues.items ?? []);
|
||||||
|
const hasIssues = computed(() => data.value.issues.visible);
|
||||||
|
|
||||||
|
const executionStatus = computed(() => data.value.execution.status);
|
||||||
|
const executionWaiting = computed(() => data.value.execution.waiting);
|
||||||
|
|
||||||
|
const runDataCount = computed(() => data.value.runData.count);
|
||||||
|
const hasRunData = computed(() => data.value.runData.visible);
|
||||||
|
|
||||||
|
return {
|
||||||
|
node,
|
||||||
|
label,
|
||||||
|
inputs,
|
||||||
|
outputs,
|
||||||
|
connections,
|
||||||
|
isDisabled,
|
||||||
|
isSelected,
|
||||||
|
pinnedDataCount,
|
||||||
|
hasPinnedData,
|
||||||
|
runDataCount,
|
||||||
|
hasRunData,
|
||||||
|
issues,
|
||||||
|
hasIssues,
|
||||||
|
executionStatus,
|
||||||
|
executionWaiting,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,4 +1,10 @@
|
||||||
|
/**
|
||||||
|
* Canvas V2 Only
|
||||||
|
* @TODO Remove this notice when Canvas V2 is the only one in use
|
||||||
|
*/
|
||||||
|
|
||||||
import type { CanvasElement } from '@/types';
|
import type { CanvasElement } from '@/types';
|
||||||
|
import { CanvasConnectionMode } from '@/types';
|
||||||
import type {
|
import type {
|
||||||
AddedNodesAndConnections,
|
AddedNodesAndConnections,
|
||||||
INodeUi,
|
INodeUi,
|
||||||
|
@ -170,9 +176,9 @@ export function useCanvasOperations({
|
||||||
historyStore.startRecordingUndo();
|
historyStore.startRecordingUndo();
|
||||||
}
|
}
|
||||||
|
|
||||||
workflowsStore.removeNodeById(id);
|
|
||||||
workflowsStore.removeNodeConnectionsById(id);
|
workflowsStore.removeNodeConnectionsById(id);
|
||||||
workflowsStore.removeNodeExecutionDataById(id);
|
workflowsStore.removeNodeExecutionDataById(id);
|
||||||
|
workflowsStore.removeNodeById(id);
|
||||||
|
|
||||||
if (trackHistory) {
|
if (trackHistory) {
|
||||||
historyStore.pushCommandToUndo(new RemoveNodeCommand(node));
|
historyStore.pushCommandToUndo(new RemoveNodeCommand(node));
|
||||||
|
@ -215,7 +221,7 @@ export function useCanvasOperations({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ndvStore.activeNodeName = node.name;
|
setNodeActiveByName(node.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setNodeActiveByName(name: string) {
|
function setNodeActiveByName(name: string) {
|
||||||
|
@ -334,18 +340,32 @@ export function useCanvasOperations({
|
||||||
const outputIndex = lastSelectedNodeOutputIndex ?? 0;
|
const outputIndex = lastSelectedNodeOutputIndex ?? 0;
|
||||||
const targetEndpoint = lastSelectedNodeEndpointUuid ?? '';
|
const targetEndpoint = lastSelectedNodeEndpointUuid ?? '';
|
||||||
|
|
||||||
// Handle connection of scoped_endpoint types
|
// Create a connection between the last selected node and the new one
|
||||||
if (lastSelectedNode && !options.isAutoAdd) {
|
if (lastSelectedNode && !options.isAutoAdd) {
|
||||||
|
// If we have a specific endpoint to connect to
|
||||||
if (lastSelectedNodeEndpointUuid) {
|
if (lastSelectedNodeEndpointUuid) {
|
||||||
const { type: connectionType } = parseCanvasConnectionHandleString(
|
const { type: connectionType, mode } = parseCanvasConnectionHandleString(
|
||||||
lastSelectedNodeEndpointUuid,
|
lastSelectedNodeEndpointUuid,
|
||||||
);
|
);
|
||||||
if (isConnectionAllowed(lastSelectedNode, newNodeData, connectionType)) {
|
|
||||||
|
const newNodeId = newNodeData.id;
|
||||||
|
const newNodeHandle = `${CanvasConnectionMode.Input}/${connectionType}/0`;
|
||||||
|
const lasSelectedNodeId = lastSelectedNode.id;
|
||||||
|
const lastSelectedNodeHandle = targetEndpoint;
|
||||||
|
|
||||||
|
if (mode === CanvasConnectionMode.Input) {
|
||||||
createConnection({
|
createConnection({
|
||||||
source: lastSelectedNode.id,
|
source: newNodeId,
|
||||||
sourceHandle: targetEndpoint,
|
sourceHandle: newNodeHandle,
|
||||||
target: newNodeData.id,
|
target: lasSelectedNodeId,
|
||||||
targetHandle: `inputs/${connectionType}/0`,
|
targetHandle: lastSelectedNodeHandle,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
createConnection({
|
||||||
|
source: lasSelectedNodeId,
|
||||||
|
sourceHandle: lastSelectedNodeHandle,
|
||||||
|
target: newNodeId,
|
||||||
|
targetHandle: newNodeHandle,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -510,8 +530,6 @@ export function useCanvasOperations({
|
||||||
canvasStore.newNodeInsertPosition = null;
|
canvasStore.newNodeInsertPosition = null;
|
||||||
} else {
|
} else {
|
||||||
let yOffset = 0;
|
let yOffset = 0;
|
||||||
const workflow = workflowsStore.getCurrentWorkflow();
|
|
||||||
|
|
||||||
if (lastSelectedConnection) {
|
if (lastSelectedConnection) {
|
||||||
const sourceNodeType = nodeTypesStore.getNodeType(
|
const sourceNodeType = nodeTypesStore.getNodeType(
|
||||||
lastSelectedNode.type,
|
lastSelectedNode.type,
|
||||||
|
@ -526,7 +544,7 @@ export function useCanvasOperations({
|
||||||
];
|
];
|
||||||
|
|
||||||
const sourceNodeOutputs = NodeHelpers.getNodeOutputs(
|
const sourceNodeOutputs = NodeHelpers.getNodeOutputs(
|
||||||
workflow,
|
editableWorkflowObject.value,
|
||||||
lastSelectedNode,
|
lastSelectedNode,
|
||||||
sourceNodeType,
|
sourceNodeType,
|
||||||
);
|
);
|
||||||
|
@ -553,7 +571,11 @@ export function useCanvasOperations({
|
||||||
// outputs here is to calculate the position, it is fine to assume
|
// outputs here is to calculate the position, it is fine to assume
|
||||||
// that they have no outputs and are so treated as a regular node
|
// that they have no outputs and are so treated as a regular node
|
||||||
// with only "main" outputs.
|
// with only "main" outputs.
|
||||||
outputs = NodeHelpers.getNodeOutputs(workflow, newNodeData, nodeTypeDescription);
|
outputs = NodeHelpers.getNodeOutputs(
|
||||||
|
editableWorkflowObject.value,
|
||||||
|
newNodeData,
|
||||||
|
nodeTypeDescription,
|
||||||
|
);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
const outputTypes = NodeHelpers.getConnectionTypes(outputs);
|
const outputTypes = NodeHelpers.getConnectionTypes(outputs);
|
||||||
const lastSelectedNodeType = nodeTypesStore.getNodeType(
|
const lastSelectedNodeType = nodeTypesStore.getNodeType(
|
||||||
|
@ -566,13 +588,15 @@ export function useCanvasOperations({
|
||||||
outputTypes.length > 0 &&
|
outputTypes.length > 0 &&
|
||||||
outputTypes.every((outputName) => outputName !== NodeConnectionType.Main)
|
outputTypes.every((outputName) => outputName !== NodeConnectionType.Main)
|
||||||
) {
|
) {
|
||||||
const lastSelectedNodeWorkflow = workflow.getNode(lastSelectedNode.name);
|
const lastSelectedNodeWorkflow = editableWorkflowObject.value.getNode(
|
||||||
|
lastSelectedNode.name,
|
||||||
|
);
|
||||||
if (!lastSelectedNodeWorkflow || !lastSelectedNodeType) {
|
if (!lastSelectedNodeWorkflow || !lastSelectedNodeType) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastSelectedInputs = NodeHelpers.getNodeInputs(
|
const lastSelectedInputs = NodeHelpers.getNodeInputs(
|
||||||
workflow,
|
editableWorkflowObject.value,
|
||||||
lastSelectedNodeWorkflow,
|
lastSelectedNodeWorkflow,
|
||||||
lastSelectedNodeType,
|
lastSelectedNodeType,
|
||||||
);
|
);
|
||||||
|
@ -600,7 +624,7 @@ export function useCanvasOperations({
|
||||||
|
|
||||||
// Has only main outputs or no outputs at all
|
// Has only main outputs or no outputs at all
|
||||||
const inputs = NodeHelpers.getNodeInputs(
|
const inputs = NodeHelpers.getNodeInputs(
|
||||||
workflow,
|
editableWorkflowObject.value,
|
||||||
lastSelectedNode,
|
lastSelectedNode,
|
||||||
lastSelectedNodeType,
|
lastSelectedNodeType,
|
||||||
);
|
);
|
||||||
|
@ -683,10 +707,11 @@ export function useCanvasOperations({
|
||||||
{ trackHistory = false }: { trackHistory?: boolean },
|
{ trackHistory = false }: { trackHistory?: boolean },
|
||||||
) {
|
) {
|
||||||
const sourceNode = workflowsStore.nodesByName[sourceNodeName];
|
const sourceNode = workflowsStore.nodesByName[sourceNodeName];
|
||||||
|
const checkNodes = workflowHelpers.getConnectedNodes(
|
||||||
const workflow = workflowHelpers.getCurrentWorkflow();
|
'downstream',
|
||||||
|
editableWorkflowObject.value,
|
||||||
const checkNodes = workflowHelpers.getConnectedNodes('downstream', workflow, sourceNodeName);
|
sourceNodeName,
|
||||||
|
);
|
||||||
for (const nodeName of checkNodes) {
|
for (const nodeName of checkNodes) {
|
||||||
const node = workflowsStore.nodesByName[nodeName];
|
const node = workflowsStore.nodesByName[nodeName];
|
||||||
const oldPosition = node.position;
|
const oldPosition = node.position;
|
||||||
|
@ -784,6 +809,9 @@ export function useCanvasOperations({
|
||||||
connection: mappedConnection,
|
connection: mappedConnection,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
nodeHelpers.updateNodeInputIssues(sourceNode);
|
||||||
|
nodeHelpers.updateNodeInputIssues(targetNode);
|
||||||
|
|
||||||
uiStore.stateIsDirty = true;
|
uiStore.stateIsDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -829,29 +857,31 @@ export function useCanvasOperations({
|
||||||
function isConnectionAllowed(
|
function isConnectionAllowed(
|
||||||
sourceNode: INodeUi,
|
sourceNode: INodeUi,
|
||||||
targetNode: INodeUi,
|
targetNode: INodeUi,
|
||||||
targetNodeConnectionType: NodeConnectionType,
|
connectionType: NodeConnectionType,
|
||||||
): boolean {
|
): boolean {
|
||||||
const targetNodeType = nodeTypesStore.getNodeType(targetNode.type, targetNode.typeVersion);
|
if (sourceNode.id === targetNode.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetNodeType = nodeTypesStore.getNodeType(targetNode.type, targetNode.typeVersion);
|
||||||
if (targetNodeType?.inputs?.length) {
|
if (targetNodeType?.inputs?.length) {
|
||||||
const workflow = workflowsStore.getCurrentWorkflow();
|
const workflowNode = editableWorkflowObject.value.getNode(targetNode.name);
|
||||||
const workflowNode = workflow.getNode(targetNode.name);
|
|
||||||
if (!workflowNode) {
|
if (!workflowNode) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let inputs: Array<ConnectionTypes | INodeInputConfiguration> = [];
|
let inputs: Array<ConnectionTypes | INodeInputConfiguration> = [];
|
||||||
if (targetNodeType) {
|
if (targetNodeType) {
|
||||||
inputs = NodeHelpers.getNodeInputs(workflow, workflowNode, targetNodeType) || [];
|
inputs =
|
||||||
|
NodeHelpers.getNodeInputs(editableWorkflowObject.value, workflowNode, targetNodeType) ||
|
||||||
|
[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let targetHasConnectionTypeAsInput = false;
|
||||||
for (const input of inputs) {
|
for (const input of inputs) {
|
||||||
if (typeof input === 'string' || input.type !== targetNodeConnectionType || !input.filter) {
|
const inputType = typeof input === 'string' ? input : input.type;
|
||||||
// No filters defined or wrong connection type
|
if (inputType === connectionType) {
|
||||||
continue;
|
if (typeof input === 'object' && 'filter' in input && input.filter?.nodes.length) {
|
||||||
}
|
|
||||||
|
|
||||||
if (input.filter.nodes.length) {
|
|
||||||
if (!input.filter.nodes.includes(sourceNode.type)) {
|
if (!input.filter.nodes.includes(sourceNode.type)) {
|
||||||
// this.dropPrevented = true;
|
// this.dropPrevented = true;
|
||||||
toast.showToast({
|
toast.showToast({
|
||||||
|
@ -862,13 +892,19 @@ export function useCanvasOperations({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
targetHasConnectionTypeAsInput = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sourceNode.id !== targetNode.id;
|
return targetHasConnectionTypeAsInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addConnections(
|
async function addConnections(
|
||||||
|
@ -907,5 +943,6 @@ export function useCanvasOperations({
|
||||||
createConnection,
|
createConnection,
|
||||||
deleteConnection,
|
deleteConnection,
|
||||||
revertDeleteConnection,
|
revertDeleteConnection,
|
||||||
|
isConnectionAllowed,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -229,15 +229,13 @@ export function useNodeHelpers() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateNodesInputIssues() {
|
function updateNodeInputIssues(node: INodeUi): void {
|
||||||
const nodes = workflowsStore.allNodes;
|
|
||||||
const workflow = workflowsStore.getCurrentWorkflow();
|
|
||||||
|
|
||||||
for (const node of nodes) {
|
|
||||||
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
||||||
if (!nodeType) {
|
if (!nodeType) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const workflow = workflowsStore.getCurrentWorkflow();
|
||||||
const nodeInputIssues = getNodeInputIssues(workflow, node, nodeType);
|
const nodeInputIssues = getNodeInputIssues(workflow, node, nodeType);
|
||||||
|
|
||||||
workflowsStore.setNodeIssue({
|
workflowsStore.setNodeIssue({
|
||||||
|
@ -246,6 +244,13 @@ export function useNodeHelpers() {
|
||||||
value: nodeInputIssues?.input ? nodeInputIssues.input : null,
|
value: nodeInputIssues?.input ? nodeInputIssues.input : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateNodesInputIssues() {
|
||||||
|
const nodes = workflowsStore.allNodes;
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
updateNodeInputIssues(node);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateNodesExecutionIssues() {
|
function updateNodesExecutionIssues() {
|
||||||
|
@ -260,6 +265,14 @@ export function useNodeHelpers() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateNodesParameterIssues() {
|
||||||
|
const nodes = workflowsStore.allNodes;
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
updateNodeParameterIssues(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateNodeCredentialIssuesByName(name: string): void {
|
function updateNodeCredentialIssuesByName(name: string): void {
|
||||||
const node = workflowsStore.getNodeByName(name);
|
const node = workflowsStore.getNodeByName(name);
|
||||||
|
|
||||||
|
@ -1228,6 +1241,8 @@ export function useNodeHelpers() {
|
||||||
getNodeIssues,
|
getNodeIssues,
|
||||||
updateNodesInputIssues,
|
updateNodesInputIssues,
|
||||||
updateNodesExecutionIssues,
|
updateNodesExecutionIssues,
|
||||||
|
updateNodesParameterIssues,
|
||||||
|
updateNodeInputIssues,
|
||||||
updateNodeCredentialIssuesByName,
|
updateNodeCredentialIssuesByName,
|
||||||
updateNodeCredentialIssues,
|
updateNodeCredentialIssues,
|
||||||
updateNodeParameterIssuesByName,
|
updateNodeParameterIssuesByName,
|
||||||
|
|
|
@ -1050,8 +1050,12 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initState(workflowData: IWorkflowDb): Promise<void> {
|
async function initState(workflowData: IWorkflowDb, set = false): Promise<void> {
|
||||||
workflowsStore.addWorkflow(workflowData);
|
workflowsStore.addWorkflow(workflowData);
|
||||||
|
if (set) {
|
||||||
|
workflowsStore.setWorkflow(workflowData);
|
||||||
|
}
|
||||||
|
|
||||||
workflowsStore.setActive(workflowData.active || false);
|
workflowsStore.setActive(workflowData.active || false);
|
||||||
workflowsStore.setWorkflowId(workflowData.id);
|
workflowsStore.setWorkflowId(workflowData.id);
|
||||||
workflowsStore.setWorkflowName({
|
workflowsStore.setWorkflowName({
|
||||||
|
|
|
@ -451,6 +451,7 @@ export const enum VIEWS {
|
||||||
CREDENTIALS = 'CredentialsView',
|
CREDENTIALS = 'CredentialsView',
|
||||||
VARIABLES = 'VariablesView',
|
VARIABLES = 'VariablesView',
|
||||||
NEW_WORKFLOW = 'NodeViewNew',
|
NEW_WORKFLOW = 'NodeViewNew',
|
||||||
|
NEW_WORKFLOW_V2 = 'NodeViewNewV2',
|
||||||
WORKFLOW = 'NodeViewExisting',
|
WORKFLOW = 'NodeViewExisting',
|
||||||
WORKFLOW_V2 = 'NodeViewV2',
|
WORKFLOW_V2 = 'NodeViewV2',
|
||||||
DEMO = 'WorkflowDemo',
|
DEMO = 'WorkflowDemo',
|
||||||
|
@ -489,8 +490,9 @@ export const enum VIEWS {
|
||||||
export const EDITABLE_CANVAS_VIEWS = [
|
export const EDITABLE_CANVAS_VIEWS = [
|
||||||
VIEWS.WORKFLOW,
|
VIEWS.WORKFLOW,
|
||||||
VIEWS.NEW_WORKFLOW,
|
VIEWS.NEW_WORKFLOW,
|
||||||
VIEWS.EXECUTION_DEBUG,
|
|
||||||
VIEWS.WORKFLOW_V2,
|
VIEWS.WORKFLOW_V2,
|
||||||
|
VIEWS.NEW_WORKFLOW_V2,
|
||||||
|
VIEWS.EXECUTION_DEBUG,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const enum FAKE_DOOR_FEATURES {
|
export const enum FAKE_DOOR_FEATURES {
|
||||||
|
|
|
@ -70,6 +70,10 @@ function getTemplatesRedirect(defaultRedirect: VIEWS[keyof VIEWS]): { name: stri
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function nodeViewV2CustomMiddleware() {
|
||||||
|
return !!localStorage.getItem('features.NodeViewV2');
|
||||||
|
}
|
||||||
|
|
||||||
export const routes: RouteRecordRaw[] = [
|
export const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
|
@ -367,13 +371,29 @@ export const routes: RouteRecordRaw[] = [
|
||||||
sidebar: MainSidebar,
|
sidebar: MainSidebar,
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
|
nodeView: true,
|
||||||
|
keepWorkflowAlive: true,
|
||||||
middleware: ['authenticated', 'custom'],
|
middleware: ['authenticated', 'custom'],
|
||||||
middlewareOptions: {
|
middlewareOptions: {
|
||||||
custom: () => {
|
custom: nodeViewV2CustomMiddleware,
|
||||||
return !!localStorage.getItem('features.NodeViewV2');
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/workflow-v2/new',
|
||||||
|
name: VIEWS.NEW_WORKFLOW_V2,
|
||||||
|
components: {
|
||||||
|
default: NodeViewV2,
|
||||||
|
header: MainHeader,
|
||||||
|
sidebar: MainSidebar,
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
nodeView: true,
|
nodeView: true,
|
||||||
|
keepWorkflowAlive: true,
|
||||||
|
middleware: ['authenticated', 'custom'],
|
||||||
|
middlewareOptions: {
|
||||||
|
custom: nodeViewV2CustomMiddleware,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -31,7 +31,6 @@ import type {
|
||||||
WorkflowMetadata,
|
WorkflowMetadata,
|
||||||
IExecutionFlattedResponse,
|
IExecutionFlattedResponse,
|
||||||
IWorkflowTemplateNode,
|
IWorkflowTemplateNode,
|
||||||
ITag,
|
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import type {
|
import type {
|
||||||
|
@ -74,7 +73,6 @@ import { i18n } from '@/plugins/i18n';
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
import { useTagsStore } from '@/stores/tags.store';
|
|
||||||
|
|
||||||
const defaults: Omit<IWorkflowDb, 'id'> & { settings: NonNullable<IWorkflowDb['settings']> } = {
|
const defaults: Omit<IWorkflowDb, 'id'> & { settings: NonNullable<IWorkflowDb['settings']> } = {
|
||||||
name: '',
|
name: '',
|
||||||
|
@ -1524,31 +1522,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
clearNodeExecutionData(node.name);
|
clearNodeExecutionData(node.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeEditableWorkflow(id: string) {
|
|
||||||
const targetWorkflow = workflowsById.value[id];
|
|
||||||
const tags = (targetWorkflow?.tags ?? []) as ITag[];
|
|
||||||
const tagIds = tags.map((tag) => tag.id);
|
|
||||||
|
|
||||||
addWorkflow(targetWorkflow);
|
|
||||||
setWorkflow(targetWorkflow);
|
|
||||||
setActive(targetWorkflow.active || false);
|
|
||||||
setWorkflowId(targetWorkflow.id);
|
|
||||||
setWorkflowName({ newName: targetWorkflow.name, setStateDirty: false });
|
|
||||||
setWorkflowSettings(targetWorkflow.settings ?? {});
|
|
||||||
setWorkflowPinData(targetWorkflow.pinData ?? {});
|
|
||||||
setWorkflowVersionId(targetWorkflow.versionId);
|
|
||||||
setWorkflowMetadata(targetWorkflow.meta);
|
|
||||||
if (targetWorkflow.usedCredentials) {
|
|
||||||
setUsedCredentials(targetWorkflow.usedCredentials);
|
|
||||||
}
|
|
||||||
setWorkflowTagIds(tagIds || []);
|
|
||||||
|
|
||||||
if (tags.length > 0) {
|
|
||||||
const tagsStore = useTagsStore();
|
|
||||||
tagsStore.upsertTags(tags);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// End Canvas V2 Functions
|
// End Canvas V2 Functions
|
||||||
//
|
//
|
||||||
|
@ -1689,6 +1662,5 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
removeNodeExecutionDataById,
|
removeNodeExecutionDataById,
|
||||||
setNodes,
|
setNodes,
|
||||||
setConnections,
|
setConnections,
|
||||||
initializeEditableWorkflow,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,13 +6,13 @@
|
||||||
content: 'n8n supports all JavaScript functions, including those not listed.';
|
content: 'n8n supports all JavaScript functions, including those not listed.';
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-node-editor .ͼ2 .cm-tooltip-autocomplete > ul[role='listbox'] {
|
.code-node-editor .cm-editor .cm-tooltip-autocomplete > ul[role='listbox'] {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ͼ2 .cm-tooltip-autocomplete {
|
.cm-editor .cm-tooltip-autocomplete {
|
||||||
background-color: var(--color-background-xlight) !important;
|
background-color: var(--color-background-xlight) !important;
|
||||||
box-shadow: var(--box-shadow-light);
|
box-shadow: var(--box-shadow-light);
|
||||||
border: none;
|
border: none;
|
||||||
|
@ -94,9 +94,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ͼ2 .cm-tooltip.cm-completionInfo,
|
.cm-editor .cm-tooltip.cm-completionInfo,
|
||||||
.ͼ2 .cm-tooltip.cm-cursorInfo,
|
.cm-editor .cm-tooltip.cm-cursorInfo,
|
||||||
.ͼ2 .cm-tooltip-hover {
|
.cm-editor .cm-tooltip-hover {
|
||||||
// Add padding when infobox only contains text
|
// Add padding when infobox only contains text
|
||||||
&:not(:has(div)) {
|
&:not(:has(div)) {
|
||||||
padding: var(--spacing-xs);
|
padding: var(--spacing-xs);
|
||||||
|
@ -264,7 +264,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ͼ2 .cm-tooltip.cm-completionInfo {
|
.cm-editor .cm-tooltip.cm-completionInfo {
|
||||||
background-color: var(--color-background-xlight);
|
background-color: var(--color-background-xlight);
|
||||||
border: var(--border-base);
|
border: var(--border-base);
|
||||||
box-shadow: var(--box-shadow-light);
|
box-shadow: var(--box-shadow-light);
|
||||||
|
@ -305,8 +305,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ͼ2 .cm-tooltip.cm-cursorInfo,
|
.cm-editor .cm-tooltip.cm-cursorInfo,
|
||||||
.ͼ2 .cm-tooltip-hover {
|
.cm-editor .cm-tooltip-hover {
|
||||||
background-color: var(--color-infobox-background);
|
background-color: var(--color-infobox-background);
|
||||||
border: var(--border-base);
|
border: var(--border-base);
|
||||||
box-shadow: var(--box-shadow-light);
|
box-shadow: var(--box-shadow-light);
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
|
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
|
||||||
import type { ConnectionTypes, INodeConnections, INodeTypeDescription } from 'n8n-workflow';
|
import type {
|
||||||
|
ConnectionTypes,
|
||||||
|
ExecutionStatus,
|
||||||
|
INodeConnections,
|
||||||
|
INodeTypeDescription,
|
||||||
|
} from 'n8n-workflow';
|
||||||
import type { BrowserJsPlumbInstance } from '@jsplumb/browser-ui';
|
import type { BrowserJsPlumbInstance } from '@jsplumb/browser-ui';
|
||||||
import type { DefaultEdge, Node, NodeProps, Position } from '@vue-flow/core';
|
import type { DefaultEdge, Node, NodeProps, Position } from '@vue-flow/core';
|
||||||
import type { INodeUi } from '@/Interface';
|
import type { INodeUi } from '@/Interface';
|
||||||
|
@ -9,6 +14,16 @@ export type CanvasElementType = 'node' | 'note';
|
||||||
|
|
||||||
export type CanvasConnectionPortType = ConnectionTypes;
|
export type CanvasConnectionPortType = ConnectionTypes;
|
||||||
|
|
||||||
|
export const enum CanvasConnectionMode {
|
||||||
|
Input = 'inputs',
|
||||||
|
Output = 'outputs',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canvasConnectionModes = [
|
||||||
|
CanvasConnectionMode.Input,
|
||||||
|
CanvasConnectionMode.Output,
|
||||||
|
] as const;
|
||||||
|
|
||||||
export type CanvasConnectionPort = {
|
export type CanvasConnectionPort = {
|
||||||
type: CanvasConnectionPortType;
|
type: CanvasConnectionPortType;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
@ -32,6 +47,22 @@ export interface CanvasElementData {
|
||||||
input: INodeConnections;
|
input: INodeConnections;
|
||||||
output: INodeConnections;
|
output: INodeConnections;
|
||||||
};
|
};
|
||||||
|
issues: {
|
||||||
|
items: string[];
|
||||||
|
visible: boolean;
|
||||||
|
};
|
||||||
|
pinnedData: {
|
||||||
|
count: number;
|
||||||
|
visible: boolean;
|
||||||
|
};
|
||||||
|
execution: {
|
||||||
|
status?: ExecutionStatus;
|
||||||
|
waiting?: string;
|
||||||
|
};
|
||||||
|
runData: {
|
||||||
|
count: number;
|
||||||
|
visible: boolean;
|
||||||
|
};
|
||||||
renderType: 'default' | 'trigger' | 'configuration' | 'configurable';
|
renderType: 'default' | 'trigger' | 'configuration' | 'configurable';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +72,7 @@ export interface CanvasConnectionData {
|
||||||
source: CanvasConnectionPort;
|
source: CanvasConnectionPort;
|
||||||
target: CanvasConnectionPort;
|
target: CanvasConnectionPort;
|
||||||
fromNodeName?: string;
|
fromNodeName?: string;
|
||||||
|
status?: 'success' | 'error' | 'pinned';
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CanvasConnection = DefaultEdge<CanvasConnectionData>;
|
export type CanvasConnection = DefaultEdge<CanvasConnectionData>;
|
||||||
|
|
|
@ -428,10 +428,11 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
|
||||||
|
|
||||||
describe('parseCanvasConnectionHandleString', () => {
|
describe('parseCanvasConnectionHandleString', () => {
|
||||||
it('should parse valid handle string', () => {
|
it('should parse valid handle string', () => {
|
||||||
const handle = 'outputs/main/1';
|
const handle = 'inputs/main/1';
|
||||||
const result = parseCanvasConnectionHandleString(handle);
|
const result = parseCanvasConnectionHandleString(handle);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
|
mode: 'inputs',
|
||||||
type: 'main',
|
type: 'main',
|
||||||
index: 1,
|
index: 1,
|
||||||
});
|
});
|
||||||
|
@ -442,6 +443,7 @@ describe('parseCanvasConnectionHandleString', () => {
|
||||||
const result = parseCanvasConnectionHandleString(handle);
|
const result = parseCanvasConnectionHandleString(handle);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
|
mode: 'outputs',
|
||||||
type: 'main',
|
type: 'main',
|
||||||
index: 0,
|
index: 0,
|
||||||
});
|
});
|
||||||
|
@ -452,6 +454,7 @@ describe('parseCanvasConnectionHandleString', () => {
|
||||||
const result = parseCanvasConnectionHandleString(handle);
|
const result = parseCanvasConnectionHandleString(handle);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
|
mode: 'outputs',
|
||||||
type: 'main',
|
type: 'main',
|
||||||
index: 0,
|
index: 0,
|
||||||
});
|
});
|
||||||
|
@ -462,6 +465,7 @@ describe('parseCanvasConnectionHandleString', () => {
|
||||||
const result = parseCanvasConnectionHandleString(handle);
|
const result = parseCanvasConnectionHandleString(handle);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
|
mode: 'outputs',
|
||||||
type: 'main',
|
type: 'main',
|
||||||
index: 1,
|
index: 1,
|
||||||
});
|
});
|
||||||
|
@ -472,6 +476,7 @@ describe('parseCanvasConnectionHandleString', () => {
|
||||||
const result = parseCanvasConnectionHandleString(handle);
|
const result = parseCanvasConnectionHandleString(handle);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
|
mode: 'outputs',
|
||||||
type: 'main',
|
type: 'main',
|
||||||
index: 0,
|
index: 0,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import type { IConnection, IConnections, INodeTypeDescription } from 'n8n-workflow';
|
import type { IConnection, IConnections, INodeTypeDescription } from 'n8n-workflow';
|
||||||
import type { INodeUi } from '@/Interface';
|
import type { INodeUi } from '@/Interface';
|
||||||
import type { CanvasConnection, CanvasConnectionPortType, CanvasConnectionPort } from '@/types';
|
import type { CanvasConnection, CanvasConnectionPortType, CanvasConnectionPort } from '@/types';
|
||||||
|
import { CanvasConnectionMode } from '@/types';
|
||||||
import type { Connection } from '@vue-flow/core';
|
import type { Connection } from '@vue-flow/core';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { isValidNodeConnectionType } from '@/utils/typeGuards';
|
import { isValidCanvasConnectionMode, isValidNodeConnectionType } from '@/utils/typeGuards';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
|
||||||
export function mapLegacyConnectionsToCanvasConnections(
|
export function mapLegacyConnectionsToCanvasConnections(
|
||||||
|
@ -29,8 +30,8 @@ export function mapLegacyConnectionsToCanvasConnections(
|
||||||
id: `[${fromId}/${fromConnectionType}/${fromIndex}][${toId}/${toConnectionType}/${toIndex}]`,
|
id: `[${fromId}/${fromConnectionType}/${fromIndex}][${toId}/${toConnectionType}/${toIndex}]`,
|
||||||
source: fromId,
|
source: fromId,
|
||||||
target: toId,
|
target: toId,
|
||||||
sourceHandle: `outputs/${fromConnectionType}/${fromIndex}`,
|
sourceHandle: `${CanvasConnectionMode.Output}/${fromConnectionType}/${fromIndex}`,
|
||||||
targetHandle: `inputs/${toConnectionType}/${toIndex}`,
|
targetHandle: `${CanvasConnectionMode.Input}/${toConnectionType}/${toIndex}`,
|
||||||
data: {
|
data: {
|
||||||
fromNodeName,
|
fromNodeName,
|
||||||
source: {
|
source: {
|
||||||
|
@ -53,9 +54,10 @@ export function mapLegacyConnectionsToCanvasConnections(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseCanvasConnectionHandleString(handle: string | null | undefined) {
|
export function parseCanvasConnectionHandleString(handle: string | null | undefined) {
|
||||||
const [, type, index] = (handle ?? '').split('/');
|
const [mode, type, index] = (handle ?? '').split('/');
|
||||||
|
|
||||||
const resolvedType = isValidNodeConnectionType(type) ? type : NodeConnectionType.Main;
|
const resolvedType = isValidNodeConnectionType(type) ? type : NodeConnectionType.Main;
|
||||||
|
const resolvedMode = isValidCanvasConnectionMode(mode) ? mode : CanvasConnectionMode.Output;
|
||||||
|
|
||||||
let resolvedIndex = parseInt(index, 10);
|
let resolvedIndex = parseInt(index, 10);
|
||||||
if (isNaN(resolvedIndex)) {
|
if (isNaN(resolvedIndex)) {
|
||||||
|
@ -63,6 +65,7 @@ export function parseCanvasConnectionHandleString(handle: string | null | undefi
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
mode: resolvedMode,
|
||||||
type: resolvedType,
|
type: resolvedType,
|
||||||
index: resolvedIndex,
|
index: resolvedIndex,
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,6 +9,8 @@ import type { IExecutionResponse, ICredentialsResponse, NewCredentialsModal } fr
|
||||||
import type { jsPlumbDOMElement } from '@jsplumb/browser-ui';
|
import type { jsPlumbDOMElement } from '@jsplumb/browser-ui';
|
||||||
import type { Connection } from '@jsplumb/core';
|
import type { Connection } from '@jsplumb/core';
|
||||||
import type { RouteLocationRaw } from 'vue-router';
|
import type { RouteLocationRaw } from 'vue-router';
|
||||||
|
import type { CanvasConnectionMode } from '@/types';
|
||||||
|
import { canvasConnectionModes } from '@/types';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Type guards used in editor-ui project
|
Type guards used in editor-ui project
|
||||||
|
@ -69,6 +71,10 @@ export function isValidNodeConnectionType(
|
||||||
return nodeConnectionTypes.includes(connectionType as NodeConnectionType);
|
return nodeConnectionTypes.includes(connectionType as NodeConnectionType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isValidCanvasConnectionMode(mode: string): mode is CanvasConnectionMode {
|
||||||
|
return canvasConnectionModes.includes(mode as CanvasConnectionMode);
|
||||||
|
}
|
||||||
|
|
||||||
export function isTriggerPanelObject(
|
export function isTriggerPanelObject(
|
||||||
triggerPanel: INodeTypeDescription['triggerPanel'],
|
triggerPanel: INodeTypeDescription['triggerPanel'],
|
||||||
): triggerPanel is TriggerPanelDefinition {
|
): triggerPanel is TriggerPanelDefinition {
|
||||||
|
|
|
@ -1,5 +1,14 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, defineAsyncComponent, onBeforeUnmount, onMounted, ref, useCssModule } from 'vue';
|
import {
|
||||||
|
computed,
|
||||||
|
defineAsyncComponent,
|
||||||
|
nextTick,
|
||||||
|
onBeforeMount,
|
||||||
|
onBeforeUnmount,
|
||||||
|
onMounted,
|
||||||
|
ref,
|
||||||
|
useCssModule,
|
||||||
|
} from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { 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';
|
||||||
|
@ -13,6 +22,7 @@ import type {
|
||||||
INodeUi,
|
INodeUi,
|
||||||
IUpdateInformation,
|
IUpdateInformation,
|
||||||
IWorkflowDataUpdate,
|
IWorkflowDataUpdate,
|
||||||
|
IWorkflowDb,
|
||||||
ToggleNodeCreatorOptions,
|
ToggleNodeCreatorOptions,
|
||||||
XYPosition,
|
XYPosition,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
|
@ -21,15 +31,19 @@ import type { CanvasElement } from '@/types';
|
||||||
import {
|
import {
|
||||||
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT,
|
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT,
|
||||||
EnterpriseEditionFeature,
|
EnterpriseEditionFeature,
|
||||||
MODAL_CANCEL,
|
|
||||||
MODAL_CONFIRM,
|
MODAL_CONFIRM,
|
||||||
NEW_WORKFLOW_ID,
|
|
||||||
VIEWS,
|
VIEWS,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
||||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
import type { NodeConnectionType, ExecutionSummary, IConnection } from 'n8n-workflow';
|
import { TelemetryHelpers } from 'n8n-workflow';
|
||||||
|
import type {
|
||||||
|
NodeConnectionType,
|
||||||
|
ExecutionSummary,
|
||||||
|
IConnection,
|
||||||
|
IWorkflowBase,
|
||||||
|
} from 'n8n-workflow';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||||
|
@ -48,6 +62,14 @@ import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { useHistoryStore } from '@/stores/history.store';
|
import { useHistoryStore } from '@/stores/history.store';
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
import { usePostHog } from '@/stores/posthog.store';
|
import { usePostHog } from '@/stores/posthog.store';
|
||||||
|
import useWorkflowsEEStore from '@/stores/workflows.ee.store';
|
||||||
|
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||||
|
import { useExecutionDebugging } from '@/composables/useExecutionDebugging';
|
||||||
|
import type { ProjectSharingData } from '@/types/projects.types';
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
import { sourceControlEventBus } from '@/event-bus/source-control';
|
||||||
|
import { useTagsStore } from '@/stores/tags.store';
|
||||||
|
import { usePushConnectionStore } from '@/stores/pushConnection.store';
|
||||||
|
|
||||||
const NodeCreation = defineAsyncComponent(
|
const NodeCreation = defineAsyncComponent(
|
||||||
async () => await import('@/components/Node/NodeCreation.vue'),
|
async () => await import('@/components/Node/NodeCreation.vue'),
|
||||||
|
@ -67,10 +89,13 @@ const toast = useToast();
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const titleChange = useTitleChange();
|
const titleChange = useTitleChange();
|
||||||
const workflowHelpers = useWorkflowHelpers({ router });
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
const nodeHelpers = useNodeHelpers();
|
||||||
|
const posthog = usePostHog();
|
||||||
|
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
const workflowsEEStore = useWorkflowsEEStore();
|
||||||
const sourceControlStore = useSourceControlStore();
|
const sourceControlStore = useSourceControlStore();
|
||||||
const nodeCreatorStore = useNodeCreatorStore();
|
const nodeCreatorStore = useNodeCreatorStore();
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
|
@ -83,6 +108,9 @@ const canvasStore = useCanvasStore();
|
||||||
const npsSurveyStore = useNpsSurveyStore();
|
const npsSurveyStore = useNpsSurveyStore();
|
||||||
const historyStore = useHistoryStore();
|
const historyStore = useHistoryStore();
|
||||||
const projectsStore = useProjectsStore();
|
const projectsStore = useProjectsStore();
|
||||||
|
const usersStore = useUsersStore();
|
||||||
|
const tagsStore = useTagsStore();
|
||||||
|
const pushConnectionStore = usePushConnectionStore();
|
||||||
|
|
||||||
const lastClickPosition = ref<XYPosition>([450, 450]);
|
const lastClickPosition = ref<XYPosition>([450, 450]);
|
||||||
|
|
||||||
|
@ -105,6 +133,7 @@ const {
|
||||||
editableWorkflow,
|
editableWorkflow,
|
||||||
editableWorkflowObject,
|
editableWorkflowObject,
|
||||||
} = useCanvasOperations({ router, lastClickPosition });
|
} = useCanvasOperations({ router, lastClickPosition });
|
||||||
|
const { applyExecutionData } = useExecutionDebugging();
|
||||||
|
|
||||||
const isLoading = ref(true);
|
const isLoading = ref(true);
|
||||||
const isBlankRedirect = ref(false);
|
const isBlankRedirect = ref(false);
|
||||||
|
@ -120,6 +149,7 @@ const hideNodeIssues = ref(false);
|
||||||
const workflowId = computed<string>(() => route.params.workflowId as string);
|
const workflowId = computed<string>(() => route.params.workflowId as string);
|
||||||
const workflow = computed(() => workflowsStore.workflowsById[workflowId.value]);
|
const workflow = computed(() => workflowsStore.workflowsById[workflowId.value]);
|
||||||
|
|
||||||
|
const isNewWorkflowRoute = computed(() => route.name === VIEWS.NEW_WORKFLOW_V2);
|
||||||
const isDemoRoute = computed(() => route.name === VIEWS.DEMO);
|
const isDemoRoute = computed(() => route.name === VIEWS.DEMO);
|
||||||
const isReadOnlyRoute = computed(() => route?.meta?.readOnlyCanvas === true);
|
const isReadOnlyRoute = computed(() => route?.meta?.readOnlyCanvas === true);
|
||||||
const isReadOnlyEnvironment = computed(() => {
|
const isReadOnlyEnvironment = computed(() => {
|
||||||
|
@ -132,60 +162,63 @@ const isReadOnlyEnvironment = computed(() => {
|
||||||
|
|
||||||
async function initializeData() {
|
async function initializeData() {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
canvasStore.startLoading();
|
||||||
|
|
||||||
resetWorkspace();
|
resetWorkspace();
|
||||||
titleChange.titleReset();
|
titleChange.titleReset();
|
||||||
|
|
||||||
const loadPromises: Array<Promise<unknown>> = [
|
const loadPromises = (() => {
|
||||||
nodeTypesStore.getNodeTypes(),
|
if (settingsStore.isPreviewMode && isDemoRoute.value) return [];
|
||||||
workflowsStore.fetchWorkflow(workflowId.value),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!settingsStore.isPreviewMode && !isDemoRoute.value) {
|
const promises: Array<Promise<unknown>> = [
|
||||||
loadPromises.push(
|
|
||||||
workflowsStore.fetchActiveWorkflows(),
|
workflowsStore.fetchActiveWorkflows(),
|
||||||
credentialsStore.fetchAllCredentials(),
|
credentialsStore.fetchAllCredentials(),
|
||||||
credentialsStore.fetchCredentialTypes(true),
|
credentialsStore.fetchCredentialTypes(true),
|
||||||
);
|
];
|
||||||
|
|
||||||
if (settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Variables)) {
|
if (settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Variables)) {
|
||||||
loadPromises.push(environmentsStore.fetchAllVariables());
|
promises.push(environmentsStore.fetchAllVariables());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.ExternalSecrets)) {
|
if (settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.ExternalSecrets)) {
|
||||||
loadPromises.push(externalSecretsStore.fetchAllSecrets());
|
promises.push(externalSecretsStore.fetchAllSecrets());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (nodeTypesStore.allNodeTypes.length === 0) {
|
||||||
await Promise.all(loadPromises);
|
promises.push(nodeTypesStore.getNodeTypes());
|
||||||
} catch (error) {
|
|
||||||
return toast.showError(
|
|
||||||
error,
|
|
||||||
i18n.baseText('nodeView.showError.mounted1.title'),
|
|
||||||
i18n.baseText('nodeView.showError.mounted1.message') + ':',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void externalHooks.run('workflow.open', {
|
return promises;
|
||||||
workflowId: workflowsStore.workflow.id,
|
})();
|
||||||
workflowName: workflowsStore.workflow.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedExecution = executionsStore.activeExecution;
|
|
||||||
if (selectedExecution?.workflowId !== workflowsStore.workflow.id) {
|
|
||||||
executionsStore.activeExecution = null;
|
|
||||||
workflowsStore.currentWorkflowExecutions = [];
|
|
||||||
} else {
|
|
||||||
executionsStore.activeExecution = selectedExecution;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @TODO Implement this
|
// @TODO Implement this
|
||||||
// this.clipboard.onPaste.value = this.onClipboardPasteEvent;
|
// this.clipboard.onPaste.value = this.onClipboardPasteEvent;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(loadPromises);
|
||||||
|
} catch (error) {
|
||||||
|
toast.showError(
|
||||||
|
error,
|
||||||
|
i18n.baseText('nodeView.showError.mounted1.title'),
|
||||||
|
i18n.baseText('nodeView.showError.mounted1.message') + ':',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
canvasStore.stopLoading();
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
void usersStore.showPersonalizationSurvey();
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// @TODO: This currently breaks since front-end hooks are still not updated to work with pinia store
|
||||||
|
void externalHooks.run('nodeView.mount').catch(() => {});
|
||||||
|
|
||||||
|
// @TODO maybe we can find a better way to handle this
|
||||||
|
canvasStore.isDemo = isDemoRoute.value;
|
||||||
|
}
|
||||||
|
|
||||||
async function initializeView() {
|
async function initializeView() {
|
||||||
// In case the workflow got saved we do not have to run init
|
// In case the workflow got saved we do not have to run init
|
||||||
// as only the route changed but all the needed data is already loaded
|
// as only the route changed but all the needed data is already loaded
|
||||||
|
@ -205,28 +238,6 @@ async function initializeView() {
|
||||||
// const templateId = route.params.id;
|
// const templateId = route.params.id;
|
||||||
// await openWorkflowTemplate(templateId.toString());
|
// await openWorkflowTemplate(templateId.toString());
|
||||||
} else {
|
} else {
|
||||||
if (uiStore.stateIsDirty && !isReadOnlyEnvironment.value) {
|
|
||||||
const confirmModal = await message.confirm(
|
|
||||||
i18n.baseText('generic.unsavedWork.confirmMessage.message'),
|
|
||||||
{
|
|
||||||
title: i18n.baseText('generic.unsavedWork.confirmMessage.headline'),
|
|
||||||
type: 'warning',
|
|
||||||
confirmButtonText: i18n.baseText('generic.unsavedWork.confirmMessage.confirmButtonText'),
|
|
||||||
cancelButtonText: i18n.baseText('generic.unsavedWork.confirmMessage.cancelButtonText'),
|
|
||||||
showClose: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (confirmModal === MODAL_CONFIRM) {
|
|
||||||
const saved = await workflowHelpers.saveCurrentWorkflow();
|
|
||||||
if (saved) {
|
|
||||||
await npsSurveyStore.fetchPromptsData();
|
|
||||||
}
|
|
||||||
} else if (confirmModal === MODAL_CANCEL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get workflow id
|
// Get workflow id
|
||||||
let workflowIdParam: string | null = null;
|
let workflowIdParam: string | null = null;
|
||||||
if (route.params.workflowId) {
|
if (route.params.workflowId) {
|
||||||
|
@ -236,7 +247,7 @@ async function initializeView() {
|
||||||
historyStore.reset();
|
historyStore.reset();
|
||||||
|
|
||||||
// If there is no workflow id, treat it as a new workflow
|
// If there is no workflow id, treat it as a new workflow
|
||||||
if (!workflowIdParam || workflowIdParam === NEW_WORKFLOW_ID) {
|
if (!workflowIdParam || isNewWorkflowRoute.value) {
|
||||||
if (route.meta?.nodeView === true) {
|
if (route.meta?.nodeView === true) {
|
||||||
await initializeViewForNewWorkflow();
|
await initializeViewForNewWorkflow();
|
||||||
}
|
}
|
||||||
|
@ -248,24 +259,25 @@ async function initializeView() {
|
||||||
await workflowsStore.fetchWorkflow(workflowIdParam);
|
await workflowsStore.fetchWorkflow(workflowIdParam);
|
||||||
|
|
||||||
titleChange.titleSet(workflow.value.name, 'IDLE');
|
titleChange.titleSet(workflow.value.name, 'IDLE');
|
||||||
// @TODO Implement this
|
await openWorkflow(workflow.value);
|
||||||
// await openWorkflow(workflow);
|
await checkAndInitDebugMode();
|
||||||
// await checkAndInitDebugMode();
|
|
||||||
|
|
||||||
workflowsStore.initializeEditableWorkflow(workflowIdParam);
|
|
||||||
await projectsStore.setProjectNavActiveIdByWorkflowHomeProject(workflow.value.homeProject);
|
|
||||||
|
|
||||||
trackOpenWorkflowFromOnboardingTemplate();
|
trackOpenWorkflowFromOnboardingTemplate();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.showError(error, i18n.baseText('openWorkflow.workflowNotFoundError'));
|
toast.showError(error, i18n.baseText('openWorkflow.workflowNotFoundError'));
|
||||||
|
|
||||||
void router.push({
|
void router.push({
|
||||||
name: VIEWS.NEW_WORKFLOW,
|
name: VIEWS.NEW_WORKFLOW_V2,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nodeHelpers.updateNodesInputIssues();
|
||||||
|
nodeHelpers.updateNodesCredentialsIssues();
|
||||||
|
nodeHelpers.updateNodesParameterIssues();
|
||||||
|
|
||||||
await loadCredentials();
|
await loadCredentials();
|
||||||
|
|
||||||
uiStore.nodeViewInitialized = true;
|
uiStore.nodeViewInitialized = true;
|
||||||
|
|
||||||
// Once view is initialized, pick up all toast notifications
|
// Once view is initialized, pick up all toast notifications
|
||||||
|
@ -284,53 +296,113 @@ async function initializeViewForNewWorkflow() {
|
||||||
uiStore.nodeViewInitialized = true;
|
uiStore.nodeViewInitialized = true;
|
||||||
executionsStore.activeExecution = null;
|
executionsStore.activeExecution = null;
|
||||||
|
|
||||||
// @TODO Implement this
|
makeNewWorkflowShareable();
|
||||||
// canvasStore.setZoomLevel(1, [0, 0]);
|
await runAutoAddManualTriggerExperiment();
|
||||||
// canvasStore.zoomToFit();
|
}
|
||||||
|
|
||||||
// @TODO Implement this
|
/**
|
||||||
// this.makeNewWorkflowShareable();
|
* Pre-populate the canvas with the manual trigger node
|
||||||
|
* if the experiment is enabled and the user is in the variant group
|
||||||
// Pre-populate the canvas with the manual trigger node if the experiment is enabled and the user is in the variant group
|
*/
|
||||||
const { getVariant } = usePostHog();
|
async function runAutoAddManualTriggerExperiment() {
|
||||||
if (
|
if (
|
||||||
getVariant(CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT.name) ===
|
posthog.getVariant(CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT.name) !==
|
||||||
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT.variant
|
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT.variant
|
||||||
) {
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const manualTriggerNode = canvasStore.getAutoAddManualTriggerNode();
|
const manualTriggerNode = canvasStore.getAutoAddManualTriggerNode();
|
||||||
if (manualTriggerNode) {
|
if (manualTriggerNode) {
|
||||||
await addNodes([manualTriggerNode]);
|
await addNodes([manualTriggerNode]);
|
||||||
uiStore.lastSelectedNode = manualTriggerNode.name;
|
uiStore.lastSelectedNode = manualTriggerNode.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error @TODO Add binding on route leave
|
||||||
|
async function promptSaveOnBeforeRouteLeave() {
|
||||||
|
if (uiStore.stateIsDirty && !isReadOnlyEnvironment.value) {
|
||||||
|
const confirmModal = await message.confirm(
|
||||||
|
i18n.baseText('generic.unsavedWork.confirmMessage.message'),
|
||||||
|
{
|
||||||
|
title: i18n.baseText('generic.unsavedWork.confirmMessage.headline'),
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: i18n.baseText('generic.unsavedWork.confirmMessage.confirmButtonText'),
|
||||||
|
cancelButtonText: i18n.baseText('generic.unsavedWork.confirmMessage.cancelButtonText'),
|
||||||
|
showClose: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmModal === MODAL_CONFIRM) {
|
||||||
|
const saved = await workflowHelpers.saveCurrentWorkflow();
|
||||||
|
if (saved) {
|
||||||
|
await npsSurveyStore.fetchPromptsData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetWorkspace() {
|
function resetWorkspace() {
|
||||||
workflowsStore.resetWorkflow();
|
|
||||||
|
|
||||||
onToggleNodeCreator({ createNodeActive: false });
|
onToggleNodeCreator({ createNodeActive: false });
|
||||||
nodeCreatorStore.setShowScrim(false);
|
nodeCreatorStore.setShowScrim(false);
|
||||||
|
|
||||||
// @TODO Implement this
|
|
||||||
// Reset nodes
|
|
||||||
// this.unbindEndpointEventListeners();
|
|
||||||
// this.deleteEveryEndpoint();
|
|
||||||
|
|
||||||
// Make sure that if there is a waiting test-webhook that it gets removed
|
// Make sure that if there is a waiting test-webhook that it gets removed
|
||||||
if (isExecutionWaitingForWebhook.value) {
|
if (isExecutionWaitingForWebhook.value) {
|
||||||
try {
|
try {
|
||||||
void workflowsStore.removeTestWebhook(workflowsStore.workflowId);
|
void workflowsStore.removeTestWebhook(workflowsStore.workflowId);
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
}
|
}
|
||||||
|
workflowsStore.resetWorkflow();
|
||||||
workflowsStore.resetState();
|
workflowsStore.resetState();
|
||||||
uiStore.removeActiveAction('workflowRunning');
|
|
||||||
|
|
||||||
|
uiStore.removeActiveAction('workflowRunning');
|
||||||
uiStore.resetSelectedNodes();
|
uiStore.resetSelectedNodes();
|
||||||
uiStore.nodeViewOffsetPosition = [0, 0]; // @TODO Not sure if needed
|
uiStore.nodeViewOffsetPosition = [0, 0]; // @TODO Not sure if needed
|
||||||
|
|
||||||
// this.credentialsUpdated = false;
|
// this.credentialsUpdated = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Workflow
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function openWorkflow(data: IWorkflowDb) {
|
||||||
|
const selectedExecution = executionsStore.activeExecution;
|
||||||
|
|
||||||
|
resetWorkspace();
|
||||||
|
|
||||||
|
await workflowHelpers.initState(data, true);
|
||||||
|
|
||||||
|
if (data.sharedWithProjects) {
|
||||||
|
workflowsEEStore.setWorkflowSharedWith({
|
||||||
|
workflowId: data.id,
|
||||||
|
sharedWithProjects: data.sharedWithProjects,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.usedCredentials) {
|
||||||
|
workflowsStore.setUsedCredentials(data.usedCredentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nodeHelpers.credentialsUpdated.value) {
|
||||||
|
uiStore.stateIsDirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void externalHooks.run('workflow.open', {
|
||||||
|
workflowId: data.id,
|
||||||
|
workflowName: data.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selectedExecution?.workflowId !== data.id) {
|
||||||
|
executionsStore.activeExecution = null;
|
||||||
|
workflowsStore.currentWorkflowExecutions = [];
|
||||||
|
} else {
|
||||||
|
executionsStore.activeExecution = selectedExecution;
|
||||||
|
}
|
||||||
|
|
||||||
|
await projectsStore.setProjectNavActiveIdByWorkflowHomeProject(workflow.value.homeProject);
|
||||||
|
}
|
||||||
|
|
||||||
function trackOpenWorkflowFromOnboardingTemplate() {
|
function trackOpenWorkflowFromOnboardingTemplate() {
|
||||||
if (workflow.value.meta?.onboardingId) {
|
if (workflow.value.meta?.onboardingId) {
|
||||||
telemetry.track(
|
telemetry.track(
|
||||||
|
@ -345,6 +417,15 @@ function trackOpenWorkflowFromOnboardingTemplate() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeNewWorkflowShareable() {
|
||||||
|
const { currentProject, personalProject } = projectsStore;
|
||||||
|
const homeProject = currentProject ?? personalProject ?? {};
|
||||||
|
const scopes = currentProject?.scopes ?? personalProject?.scopes ?? [];
|
||||||
|
|
||||||
|
workflowsStore.workflow.homeProject = homeProject as ProjectSharingData;
|
||||||
|
workflowsStore.workflow.scopes = scopes;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nodes
|
* Nodes
|
||||||
*/
|
*/
|
||||||
|
@ -479,27 +560,32 @@ function onToggleNodeCreator(options: ToggleNodeCreatorOptions) {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async function onRunWorkflow() {
|
async function onRunWorkflow() {
|
||||||
|
trackRunWorkflow();
|
||||||
|
|
||||||
await runWorkflow({});
|
await runWorkflow({});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trackRunWorkflow() {
|
||||||
|
void workflowHelpers.getWorkflowDataToSave().then((workflowData) => {
|
||||||
|
const telemetryPayload = {
|
||||||
|
workflow_id: workflowId.value,
|
||||||
|
node_graph_string: JSON.stringify(
|
||||||
|
TelemetryHelpers.generateNodesGraph(
|
||||||
|
workflowData as IWorkflowBase,
|
||||||
|
workflowHelpers.getNodeTypes(),
|
||||||
|
{ isCloudDeployment: settingsStore.isCloudDeployment },
|
||||||
|
).nodeGraph,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
telemetry.track('User clicked execute workflow button', telemetryPayload);
|
||||||
|
void externalHooks.run('nodeView.onRunWorkflow', telemetryPayload);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function openExecution(_executionId: string) {
|
async function openExecution(_executionId: string) {
|
||||||
// @TODO
|
// @TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Unload
|
|
||||||
*/
|
|
||||||
|
|
||||||
function addUnloadEventBindings() {
|
|
||||||
// window.addEventListener('beforeunload', this.onBeforeUnload);
|
|
||||||
// window.addEventListener('unload', this.onUnload);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeUnloadEventBindings() {
|
|
||||||
// window.removeEventListener('beforeunload', this.onBeforeUnload);
|
|
||||||
// window.removeEventListener('unload', this.onUnload);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keboard
|
* Keboard
|
||||||
*/
|
*/
|
||||||
|
@ -538,6 +624,38 @@ function removeUndoRedoEventBindings() {
|
||||||
// historyBus.off('enableNodeToggle', onRevertEnableToggle);
|
// historyBus.off('enableNodeToggle', onRevertEnableToggle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Source control
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function onSourceControlPull() {
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
environmentsStore.fetchAllVariables(),
|
||||||
|
tagsStore.fetchAll(),
|
||||||
|
loadCredentials(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (workflowId.value !== null && !uiStore.stateIsDirty) {
|
||||||
|
const workflowData = await workflowsStore.fetchWorkflow(workflowId.value);
|
||||||
|
if (workflowData) {
|
||||||
|
titleChange.titleSet(workflowData.name, 'IDLE');
|
||||||
|
await openWorkflow(workflowData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSourceControlEventBindings() {
|
||||||
|
sourceControlEventBus.on('pull', onSourceControlPull);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSourceControlEventBindings() {
|
||||||
|
sourceControlEventBus.off('pull', onSourceControlPull);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Post message events
|
* Post message events
|
||||||
*/
|
*/
|
||||||
|
@ -647,6 +765,34 @@ function checkIfEditingIsAllowed(): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkIfRouteIsAllowed() {
|
||||||
|
if (
|
||||||
|
isReadOnlyEnvironment.value &&
|
||||||
|
[VIEWS.NEW_WORKFLOW, VIEWS.TEMPLATE_IMPORT].find((view) => view === route.name)
|
||||||
|
) {
|
||||||
|
void nextTick(async () => {
|
||||||
|
resetWorkspace();
|
||||||
|
uiStore.stateIsDirty = false;
|
||||||
|
|
||||||
|
await router.replace({ name: VIEWS.HOMEPAGE });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug mode
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function checkAndInitDebugMode() {
|
||||||
|
if (route.name === VIEWS.EXECUTION_DEBUG) {
|
||||||
|
titleChange.titleSet(workflowsStore.workflowName, 'DEBUG');
|
||||||
|
if (!workflowsStore.isInDebugMode) {
|
||||||
|
await applyExecutionData(route.params.executionId as string);
|
||||||
|
workflowsStore.isInDebugMode = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mouse events
|
* Mouse events
|
||||||
*/
|
*/
|
||||||
|
@ -656,25 +802,66 @@ function onClickPane(position: CanvasElement['position']) {
|
||||||
canvasStore.newNodeInsertPosition = [position.x, position.y];
|
canvasStore.newNodeInsertPosition = [position.x, position.y];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Actions
|
||||||
|
*/
|
||||||
|
|
||||||
|
function registerCustomActions() {
|
||||||
|
// @TODO Implement these
|
||||||
|
// this.registerCustomAction({
|
||||||
|
// key: 'openNodeDetail',
|
||||||
|
// action: ({ node }: { node: string }) => {
|
||||||
|
// this.nodeSelectedByName(node, true);
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// this.registerCustomAction({
|
||||||
|
// key: 'openSelectiveNodeCreator',
|
||||||
|
// action: this.openSelectiveNodeCreator,
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// this.registerCustomAction({
|
||||||
|
// key: 'showNodeCreator',
|
||||||
|
// action: () => {
|
||||||
|
// this.ndvStore.activeNodeName = null;
|
||||||
|
//
|
||||||
|
// void this.$nextTick(() => {
|
||||||
|
// this.showTriggerCreator(NODE_CREATOR_OPEN_SOURCES.TAB);
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lifecycle
|
* Lifecycle
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
if (!isDemoRoute.value) {
|
||||||
|
pushConnectionStore.pushConnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await initializeData();
|
void initializeData().then(() => {
|
||||||
await initializeView();
|
void initializeView();
|
||||||
|
|
||||||
|
checkIfRouteIsAllowed();
|
||||||
|
});
|
||||||
|
|
||||||
addUndoRedoEventBindings();
|
addUndoRedoEventBindings();
|
||||||
addPostMessageEventBindings();
|
addPostMessageEventBindings();
|
||||||
addKeyboardEventBindings();
|
addKeyboardEventBindings();
|
||||||
addUnloadEventBindings();
|
addSourceControlEventBindings();
|
||||||
|
|
||||||
|
registerCustomActions();
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
removeUnloadEventBindings();
|
|
||||||
removeKeyboardEventBindings();
|
removeKeyboardEventBindings();
|
||||||
removePostMessageEventBindings();
|
removePostMessageEventBindings();
|
||||||
removeUndoRedoEventBindings();
|
removeUndoRedoEventBindings();
|
||||||
|
removeSourceControlEventBindings();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1185,7 +1185,7 @@ importers:
|
||||||
version: 10.5.0(vue@3.4.21(typescript@5.5.2))
|
version: 10.5.0(vue@3.4.21(typescript@5.5.2))
|
||||||
axios:
|
axios:
|
||||||
specifier: 1.6.7
|
specifier: 1.6.7
|
||||||
version: 1.6.7(debug@3.2.7)
|
version: 1.6.7
|
||||||
chart.js:
|
chart.js:
|
||||||
specifier: ^4.4.0
|
specifier: ^4.4.0
|
||||||
version: 4.4.0
|
version: 4.4.0
|
||||||
|
@ -16118,7 +16118,7 @@ snapshots:
|
||||||
'@antfu/install-pkg': 0.1.1
|
'@antfu/install-pkg': 0.1.1
|
||||||
'@antfu/utils': 0.7.6
|
'@antfu/utils': 0.7.6
|
||||||
'@iconify/types': 2.0.0
|
'@iconify/types': 2.0.0
|
||||||
debug: 4.3.4(supports-color@8.1.1)
|
debug: 4.3.4
|
||||||
kolorist: 1.8.0
|
kolorist: 1.8.0
|
||||||
local-pkg: 0.4.3
|
local-pkg: 0.4.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
@ -19962,7 +19962,7 @@ snapshots:
|
||||||
|
|
||||||
agent-base@6.0.2:
|
agent-base@6.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.3.4(supports-color@8.1.1)
|
debug: 4.3.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
@ -20266,6 +20266,14 @@ snapshots:
|
||||||
'@babel/runtime': 7.23.6
|
'@babel/runtime': 7.23.6
|
||||||
is-retry-allowed: 2.2.0
|
is-retry-allowed: 2.2.0
|
||||||
|
|
||||||
|
axios@1.6.7:
|
||||||
|
dependencies:
|
||||||
|
follow-redirects: 1.15.6
|
||||||
|
form-data: 4.0.0
|
||||||
|
proxy-from-env: 1.1.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- debug
|
||||||
|
|
||||||
axios@1.6.7(debug@3.2.7):
|
axios@1.6.7(debug@3.2.7):
|
||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects: 1.15.6(debug@3.2.7)
|
follow-redirects: 1.15.6(debug@3.2.7)
|
||||||
|
@ -21292,6 +21300,10 @@ snapshots:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
supports-color: 8.1.1
|
supports-color: 8.1.1
|
||||||
|
|
||||||
|
debug@4.3.4:
|
||||||
|
dependencies:
|
||||||
|
ms: 2.1.2
|
||||||
|
|
||||||
debug@4.3.4(supports-color@8.1.1):
|
debug@4.3.4(supports-color@8.1.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.2
|
ms: 2.1.2
|
||||||
|
@ -22466,6 +22478,8 @@ snapshots:
|
||||||
|
|
||||||
fn.name@1.1.0: {}
|
fn.name@1.1.0: {}
|
||||||
|
|
||||||
|
follow-redirects@1.15.6: {}
|
||||||
|
|
||||||
follow-redirects@1.15.6(debug@3.2.7):
|
follow-redirects@1.15.6(debug@3.2.7):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
debug: 3.2.7(supports-color@5.5.0)
|
debug: 3.2.7(supports-color@5.5.0)
|
||||||
|
@ -23078,7 +23092,7 @@ snapshots:
|
||||||
https-proxy-agent@5.0.1:
|
https-proxy-agent@5.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 6.0.2
|
agent-base: 6.0.2
|
||||||
debug: 4.3.4(supports-color@8.1.1)
|
debug: 4.3.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
@ -27814,7 +27828,7 @@ snapshots:
|
||||||
'@antfu/install-pkg': 0.1.1
|
'@antfu/install-pkg': 0.1.1
|
||||||
'@antfu/utils': 0.7.6
|
'@antfu/utils': 0.7.6
|
||||||
'@iconify/utils': 2.1.11
|
'@iconify/utils': 2.1.11
|
||||||
debug: 4.3.4(supports-color@8.1.1)
|
debug: 4.3.4
|
||||||
kolorist: 1.8.0
|
kolorist: 1.8.0
|
||||||
local-pkg: 0.5.0
|
local-pkg: 0.5.0
|
||||||
unplugin: 1.5.1
|
unplugin: 1.5.1
|
||||||
|
|
Loading…
Reference in a new issue