feat(editor): AI Floating Nodes (#8703)

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
This commit is contained in:
oleg 2024-02-23 13:34:32 +01:00 committed by GitHub
parent a29b41ec55
commit 41b191e055
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 575 additions and 75 deletions

View file

@ -1,7 +1,8 @@
import { v4 as uuid } from 'uuid';
import { getVisibleSelect } from '../utils';
import { MANUAL_TRIGGER_NODE_DISPLAY_NAME } from '../constants';
import { MANUAL_TRIGGER_NODE_DISPLAY_NAME, AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME } from '../constants';
import { NDV, WorkflowPage } from '../pages';
import { NodeCreator } from '../pages/features/node-creator';
const workflowPage = new WorkflowPage();
const ndv = new NDV();
@ -386,14 +387,12 @@ describe('NDV', () => {
) {
return cy.get(`[data-node-placement=${position}]`);
}
beforeEach(() => {
it('should traverse floating nodes with mouse', () => {
cy.createFixtureWorkflow('Floating_Nodes.json', `Floating Nodes`);
workflowPage.getters.canvasNodes().first().dblclick();
getFloatingNodeByPosition('inputMain').should('not.exist');
getFloatingNodeByPosition('outputMain').should('exist');
});
it('should traverse floating nodes with mouse', () => {
// Traverse 4 connected node forwards
Array.from(Array(4).keys()).forEach((i) => {
getFloatingNodeByPosition('outputMain').click({ force: true });
@ -411,19 +410,6 @@ describe('NDV', () => {
getFloatingNodeByPosition('outputMain').click({ force: true });
ndv.getters.nodeNameContainer().should('contain', 'Chain');
getFloatingNodeByPosition('inputSub').should('exist');
getFloatingNodeByPosition('inputSub').click({ force: true });
ndv.getters.nodeNameContainer().should('contain', 'Model');
getFloatingNodeByPosition('inputSub').should('not.exist');
getFloatingNodeByPosition('inputMain').should('not.exist');
getFloatingNodeByPosition('outputMain').should('not.exist');
getFloatingNodeByPosition('outputSub').should('exist');
ndv.actions.close();
workflowPage.getters.selectedNodes().should('have.length', 1);
workflowPage.getters.selectedNodes().first().should('contain', 'Model');
workflowPage.getters.selectedNodes().first().dblclick();
getFloatingNodeByPosition('outputSub').click({ force: true });
ndv.getters.nodeNameContainer().should('contain', 'Chain');
// Traverse 4 connected node backwards
Array.from(Array(4).keys()).forEach((i) => {
@ -448,7 +434,11 @@ describe('NDV', () => {
.should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME);
});
it('should traverse floating nodes with mouse', () => {
it('should traverse floating nodes with keyboard', () => {
cy.createFixtureWorkflow('Floating_Nodes.json', `Floating Nodes`);
workflowPage.getters.canvasNodes().first().dblclick();
getFloatingNodeByPosition('inputMain').should('not.exist');
getFloatingNodeByPosition('outputMain').should('exist');
// Traverse 4 connected node forwards
Array.from(Array(4).keys()).forEach((i) => {
cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowRight']);
@ -466,19 +456,6 @@ describe('NDV', () => {
cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowRight']);
ndv.getters.nodeNameContainer().should('contain', 'Chain');
getFloatingNodeByPosition('inputSub').should('exist');
cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowDown']);
ndv.getters.nodeNameContainer().should('contain', 'Model');
getFloatingNodeByPosition('inputSub').should('not.exist');
getFloatingNodeByPosition('inputMain').should('not.exist');
getFloatingNodeByPosition('outputMain').should('not.exist');
getFloatingNodeByPosition('outputSub').should('exist');
ndv.actions.close();
workflowPage.getters.selectedNodes().should('have.length', 1);
workflowPage.getters.selectedNodes().first().should('contain', 'Model');
workflowPage.getters.selectedNodes().first().dblclick();
cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowUp']);
ndv.getters.nodeNameContainer().should('contain', 'Chain');
// Traverse 4 connected node backwards
Array.from(Array(4).keys()).forEach((i) => {
@ -502,6 +479,47 @@ describe('NDV', () => {
.first()
.should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME);
});
it('should connect floating sub-nodes', () => {
const nodeCreator = new NodeCreator();
const connectionGroups = [
{
title: 'Language Models',
id: 'ai_languageModel'
},
{
title: 'Tools',
id: 'ai_tool'
},
]
workflowPage.actions.addInitialNodeToCanvas('AI Agent', { keepNdvOpen: true });
connectionGroups.forEach((group) => {
cy.getByTestId(`add-subnode-${group.id}`).should('exist');
cy.getByTestId(`add-subnode-${group.id}`).click();
cy.getByTestId('nodes-list-header').contains(group.title).should('exist');
nodeCreator.getters.getNthCreatorItem(1).click();
getFloatingNodeByPosition('outputSub').should('exist');
getFloatingNodeByPosition('outputSub').click({ force: true });
if (group.id === 'ai_languageModel') {
cy.getByTestId(`add-subnode-${group.id}`).should('not.exist');
} else {
cy.getByTestId(`add-subnode-${group.id}`).should('exist');
// Expand the subgroup
cy.getByTestId('subnode-connection-group-ai_tool').click();
cy.getByTestId(`add-subnode-${group.id}`).click();
nodeCreator.getters.getNthCreatorItem(1).click();
getFloatingNodeByPosition('outputSub').click({ force: true });
cy.getByTestId('subnode-connection-group-ai_tool').findChildByTestId('floating-subnode').should('have.length', 2);
}
});
// Since language model has no credentials set, it should show an error
cy.get('[class*=hasIssues]').should('have.length', 1);
})
});
it('should show node name and version in settings', () => {

View file

@ -3,7 +3,6 @@
<NDVFloatingNodes
v-if="activeNode"
:root-node="activeNode"
type="input"
@switchSelectedNode="onSwitchSelectedNode"
/>
<div v-if="!hideInputAndOutput" :class="$style.inputPanel" :style="inputPanelStyles">

View file

@ -46,12 +46,10 @@ import type { INodeTypeDescription } from 'n8n-workflow';
interface Props {
rootNode: INodeUi;
type: 'input' | 'sub-input' | 'sub-output' | 'output';
}
const enum FloatingNodePosition {
top = 'outputSub',
right = 'outputMain',
bottom = 'inputSub',
left = 'inputMain',
}
const props = defineProps<Props>();
@ -77,7 +75,6 @@ function onKeyDown(e: KeyboardEvent) {
const mapper = {
ArrowUp: FloatingNodePosition.top,
ArrowRight: FloatingNodePosition.right,
ArrowDown: FloatingNodePosition.bottom,
ArrowLeft: FloatingNodePosition.left,
};
/* eslint-enable @typescript-eslint/naming-convention */
@ -111,9 +108,6 @@ const connectedNodes = computed<
workflow.getChildNodes(rootName, 'ALL_NON_MAIN'),
),
[FloatingNodePosition.right]: getINodesFromNames(workflow.getChildNodes(rootName, 'main', 1)),
[FloatingNodePosition.bottom]: getINodesFromNames(
workflow.getParentNodes(rootName, 'ALL_NON_MAIN'),
),
[FloatingNodePosition.left]: getINodesFromNames(workflow.getParentNodes(rootName, 'main', 1)),
};
});
@ -121,13 +115,11 @@ const connectedNodes = computed<
const connectionGroups = [
FloatingNodePosition.top,
FloatingNodePosition.right,
FloatingNodePosition.bottom,
FloatingNodePosition.left,
];
const tooltipPositionMapper = {
[FloatingNodePosition.top]: 'bottom',
[FloatingNodePosition.right]: 'left',
[FloatingNodePosition.bottom]: 'top',
[FloatingNodePosition.left]: 'right',
};

View file

@ -0,0 +1,458 @@
<template>
<div :class="$style.container">
<div
:class="$style.connections"
:style="`--possible-connections: ${possibleConnections.length}`"
>
<div
v-for="connection in possibleConnections"
:key="connection.type"
:data-test-id="`subnode-connection-group-${connection.type}`"
>
<div :class="$style.connectionType">
<span
:class="{
[$style.connectionLabel]: true,
[$style.hasIssues]: hasInputIssues(connection.type),
}"
v-text="`${connection.displayName}${connection.required ? ' *' : ''}`"
/>
<div
v-on-click-outside="() => expandConnectionGroup(connection.type, false)"
:class="{
[$style.connectedNodesWrapper]: true,
[$style.connectedNodesWrapperExpanded]: expandedGroups.includes(connection.type),
}"
:style="`--nodes-length: ${connectedNodes[connection.type].length}`"
@click="expandConnectionGroup(connection.type, true)"
>
<div
v-if="
connectedNodes[connection.type].length >= 1 ? connection.maxConnections !== 1 : true
"
:class="{
[$style.plusButton]: true,
[$style.hasIssues]: hasInputIssues(connection.type),
}"
@click="onPlusClick(connection.type)"
>
<n8n-tooltip
placement="top"
:teleported="true"
:offset="10"
:show-after="300"
:disabled="
shouldShowConnectionTooltip(connection.type) &&
connectedNodes[connection.type].length >= 1
"
>
<template #content>
Add {{ connection.displayName }}
<template v-if="hasInputIssues(connection.type)">
<TitledList
:title="`${$locale.baseText('node.issues')}:`"
:items="nodeInputIssues[connection.type]"
/>
</template>
</template>
<n8n-icon-button
size="medium"
icon="plus"
type="tertiary"
:data-test-id="`add-subnode-${connection.type}`"
/>
</n8n-tooltip>
</div>
<div
v-if="connectedNodes[connection.type].length > 0"
:class="{
[$style.connectedNodes]: true,
[$style.connectedNodesMultiple]: connectedNodes[connection.type].length > 1,
}"
>
<div
v-for="(node, index) in connectedNodes[connection.type]"
:key="node.node.name"
:class="{ [$style.nodeWrapper]: true, [$style.hasIssues]: node.issues }"
data-test-id="floating-subnode"
:data-node-name="node.node.name"
:style="`--node-index: ${index}`"
>
<n8n-tooltip
:key="node.node.name"
placement="top"
:teleported="true"
:offset="10"
:show-after="300"
:disabled="shouldShowConnectionTooltip(connection.type)"
>
<template #content>
{{ node.node.name }}
<template v-if="node.issues">
<TitledList
:title="`${$locale.baseText('node.issues')}:`"
:items="node.issues"
/>
</template>
</template>
<div
:class="$style.connectedNode"
@click="onNodeClick(node.node.name, connection.type)"
>
<NodeIcon
:node-type="node.nodeType"
:node-name="node.node.name"
tooltip-position="top"
:size="20"
circle
/>
</div>
</n8n-tooltip>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { INodeUi } from '@/Interface';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { computed, ref, watch } from 'vue';
import { NodeHelpers } from 'n8n-workflow';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import NodeIcon from '@/components/NodeIcon.vue';
import TitledList from '@/components/TitledList.vue';
import type { ConnectionTypes, INodeInputConfiguration, INodeTypeDescription } from 'n8n-workflow';
import { useDebounce } from '@/composables/useDebounce';
interface Props {
rootNode: INodeUi;
}
const props = defineProps<Props>();
const workflowsStore = useWorkflowsStore();
const nodeTypesStore = useNodeTypesStore();
const nodeHelpers = useNodeHelpers();
const { debounce } = useDebounce();
const emit = defineEmits(['switchSelectedNode', 'openConnectionNodeCreator']);
interface NodeConfig {
node: INodeUi;
nodeType: INodeTypeDescription;
issues: string[];
}
const possibleConnections = ref<INodeInputConfiguration[]>([]);
const expandedGroups = ref<ConnectionTypes[]>([]);
const shouldShowNodeInputIssues = ref(false);
const nodeType = computed(() =>
nodeTypesStore.getNodeType(props.rootNode.type, props.rootNode.typeVersion),
);
const nodeData = computed(() => workflowsStore.getNodeByName(props.rootNode.name));
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
const nodeInputIssues = computed(() => {
const issues = nodeHelpers.getNodeIssues(nodeType.value, props.rootNode, workflow.value, [
'typeUnknown',
'parameters',
'credentials',
'execution',
]);
return issues?.input ?? {};
});
const connectedNodes = computed<Record<ConnectionTypes, NodeConfig[]>>(() => {
return possibleConnections.value.reduce(
(acc, connection) => {
const nodes = getINodesFromNames(
workflow.value.getParentNodes(props.rootNode.name, connection.type),
);
return { ...acc, [connection.type]: nodes };
},
{} as Record<ConnectionTypes, NodeConfig[]>,
);
});
function getConnectionConfig(connectionType: ConnectionTypes) {
return possibleConnections.value.find((c) => c.type === connectionType);
}
function isMultiConnection(connectionType: ConnectionTypes) {
const connectionConfig = getConnectionConfig(connectionType);
return connectionConfig?.maxConnections !== 1;
}
function shouldShowConnectionTooltip(connectionType: ConnectionTypes) {
return isMultiConnection(connectionType) && !expandedGroups.value.includes(connectionType);
}
function expandConnectionGroup(connectionType: ConnectionTypes, isExpanded: boolean) {
// If the connection is a single connection, we don't need to expand the group
if (!isMultiConnection(connectionType)) {
return;
}
if (isExpanded) {
expandedGroups.value = [...expandedGroups.value, connectionType];
} else {
expandedGroups.value = expandedGroups.value.filter((g) => g !== connectionType);
}
}
function getINodesFromNames(names: string[]): NodeConfig[] {
return names
.map((name) => {
const node = workflowsStore.getNodeByName(name);
if (node) {
const matchedNodeType = nodeTypesStore.getNodeType(node.type);
if (matchedNodeType) {
const issues = nodeHelpers.getNodeIssues(matchedNodeType, node, workflow.value);
const stringifiedIssues = issues ? NodeHelpers.nodeIssuesToString(issues, node) : '';
return { node, nodeType: matchedNodeType, issues: stringifiedIssues };
}
}
return null;
})
.filter((n): n is NodeConfig => n !== null);
}
function hasInputIssues(connectionType: ConnectionTypes) {
return (
shouldShowNodeInputIssues.value && (nodeInputIssues.value[connectionType] ?? []).length > 0
);
}
function isNodeInputConfiguration(
connectionConfig: ConnectionTypes | INodeInputConfiguration,
): connectionConfig is INodeInputConfiguration {
if (typeof connectionConfig === 'string') return false;
return 'type' in connectionConfig;
}
function getPossibleSubInputConnections(): INodeInputConfiguration[] {
if (!nodeType.value || !props.rootNode) return [];
const inputs = NodeHelpers.getNodeInputs(workflow.value, props.rootNode, nodeType.value);
const nonMainInputs = inputs.filter((input): input is INodeInputConfiguration => {
if (!isNodeInputConfiguration(input)) return false;
return input.type !== 'main';
});
return nonMainInputs;
}
function onNodeClick(nodeName: string, connectionType: ConnectionTypes) {
if (isMultiConnection(connectionType) && !expandedGroups.value.includes(connectionType)) {
expandConnectionGroup(connectionType, true);
return;
}
emit('switchSelectedNode', nodeName);
}
function onPlusClick(connectionType: ConnectionTypes) {
const connectionNodes = connectedNodes.value[connectionType];
if (
isMultiConnection(connectionType) &&
!expandedGroups.value.includes(connectionType) &&
connectionNodes.length >= 1
) {
expandConnectionGroup(connectionType, true);
return;
}
emit('openConnectionNodeCreator', props.rootNode.name, connectionType);
}
function showNodeInputsIssues() {
console.log('showNodeInputsIssues');
shouldShowNodeInputIssues.value = false;
// Reset animation
setTimeout(() => {
shouldShowNodeInputIssues.value = true;
}, 0);
}
watch(
nodeData,
debounce(
() =>
setTimeout(() => {
expandedGroups.value = [];
possibleConnections.value = getPossibleSubInputConnections();
}, 0),
{ debounceTime: 1000 },
),
{ immediate: true },
);
defineExpose({
showNodeInputsIssues,
});
</script>
<style lang="scss" module>
@keyframes horizontal-shake {
0% {
transform: translateX(0);
}
25% {
transform: translateX(5px);
}
50% {
transform: translateX(-5px);
}
75% {
transform: translateX(5px);
}
100% {
transform: translateX(0);
}
}
.container {
--node-size: 45px;
--plus-button-size: 30px;
--animation-duration: 150ms;
--collapsed-offset: 10px;
padding-top: calc(var(--node-size) + var(--spacing-3xs));
}
.connections {
// Make sure container has matching height if there's no connections
// since the plus button is absolutely positioned
min-height: calc(var(--node-size) + var(--spacing-m));
position: absolute;
bottom: calc((var(--node-size) / 2) * -1);
left: 0;
right: 0;
user-select: none;
justify-content: space-between;
display: grid;
grid-template-columns: repeat(var(--possible-connections), 1fr);
}
.connectionType {
display: flex;
flex-direction: column;
align-items: center;
transition: all calc((var(--animation-duration) - 50ms)) ease;
}
.connectionLabel {
margin-bottom: var(--spacing-2xs);
font-size: var(--font-size-2xs);
user-select: none;
text-wrap: nowrap;
&.hasIssues {
color: var(--color-danger);
}
}
.connectedNodesWrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
position: relative;
}
.plusButton {
transition: all var(--animation-duration) ease;
position: absolute;
top: var(--spacing-2xs);
&.hasIssues {
animation: horizontal-shake 500ms;
button {
--button-font-color: var(--color-danger);
--button-border-color: var(--color-danger);
}
}
&:not(:last-child) {
z-index: 1;
right: 100%;
margin-right: calc((var(--plus-button-size) * -1) * 0.9);
pointer-events: none;
.connectedNodesWrapperExpanded & {
// left: 100%;
margin-right: var(--spacing-2xs);
opacity: 1;
pointer-events: all;
}
}
}
.connectedNodesMultiple {
transition: all var(--animation-duration) ease;
}
.connectedNodesWrapperExpanded {
z-index: 1;
}
// Hide all other connection groups when one is expanded
.connections:has(.connectedNodesWrapperExpanded)
.connectionType:not(:has(.connectedNodesWrapperExpanded)) {
opacity: 0;
pointer-events: none;
visibility: hidden;
}
.connectedNode {
border: var(--border-base);
background-color: var(--color-canvas-node-background);
border-radius: 100%;
padding: var(--spacing-xs);
cursor: pointer;
pointer-events: all;
transition: all var(--animation-duration) ease;
position: relative;
display: flex;
justify-self: center;
align-self: center;
}
.connectedNodes {
display: flex;
justify-content: center;
margin-right: calc(
(var(--nodes-length) - 1) * (-1 * (var(--node-size) - var(--collapsed-offset)))
);
.connectedNodesWrapperExpanded & {
margin-right: 0;
// Negative margin to offset the absolutely positioned plus button
// when the nodes are expanded to center the nodes
margin-right: calc((var(--spacing-2xs) + var(--plus-button-size)) * -1);
}
}
.nodeWrapper {
transition: all var(--animation-duration) ease;
transform-origin: center;
z-index: 1;
.connectedNodesWrapperExpanded &:not(:first-child) {
margin-left: var(--spacing-2xs);
}
&.hasIssues {
.connectedNode {
border-width: calc(var(--border-width-base) * 2);
border-color: var(--color-danger);
}
}
&:not(:first-child) {
transform: translateX(
calc(var(--node-index) * (-1 * (var(--node-size) - var(--collapsed-offset))))
);
}
.connectedNodesWrapperExpanded & {
transform: translateX(0);
}
}
</style>

View file

@ -45,6 +45,7 @@
:has-double-width="activeNodeType?.parameterPane === 'wide'"
:node-type="activeNodeType"
@switchSelectedNode="onSwitchSelectedNode"
@openConnectionNodeCreator="onOpenConnectionNodeCreator"
@close="close"
@init="onPanelsInit"
@dragstart="onDragStart"
@ -117,6 +118,8 @@
@stopExecution="onStopExecution"
@redrawRequired="redrawRequired = true"
@activate="onWorkflowActivate"
@switchSelectedNode="onSwitchSelectedNode"
@openConnectionNodeCreator="onOpenConnectionNodeCreator"
/>
<a
v-if="featureRequestUrl"
@ -143,6 +146,7 @@ import type {
IRunData,
IRunExecutionData,
Workflow,
ConnectionTypes,
} from 'n8n-workflow';
import { jsonParse, NodeHelpers, NodeConnectionType } from 'n8n-workflow';
import type { IExecutionResponse, INodeUi, IUpdateInformation, TargetItem } from '@/Interface';
@ -664,9 +668,12 @@ export default defineComponent({
nodeTypeSelected(nodeTypeName: string) {
this.$emit('nodeTypeSelected', nodeTypeName);
},
async onSwitchSelectedNode(nodeTypeName: string) {
onSwitchSelectedNode(nodeTypeName: string) {
this.$emit('switchSelectedNode', nodeTypeName);
},
onOpenConnectionNodeCreator(nodeTypeName: string, connectionType: ConnectionTypes) {
this.$emit('openConnectionNodeCreator', nodeTypeName, connectionType);
},
async close() {
if (this.isDragging) {
return;
@ -780,8 +787,9 @@ export default defineComponent({
}
.data-display-wrapper {
height: calc(100% - var(--spacing-2xl));
height: calc(100% - var(--spacing-l)) !important;
margin-top: var(--spacing-xl) !important;
margin-bottom: var(--spacing-xl) !important;
width: 100%;
background: none;
border: none;

View file

@ -165,6 +165,13 @@
</div>
</div>
</div>
<NDVSubConnections
v-if="node"
ref="subConnections"
:root-node="node"
@switchSelectedNode="onSwitchSelectedNode"
@openConnectionNodeCreator="onOpenConnectionNodeCreator"
/>
<n8n-block-ui :show="blockUI" />
</div>
</template>
@ -178,6 +185,7 @@ import type {
INodeParameters,
INodeProperties,
NodeParameterValue,
ConnectionTypes,
} from 'n8n-workflow';
import { NodeHelpers, NodeConnectionType, deepCopy } from 'n8n-workflow';
import type {
@ -199,6 +207,7 @@ import ParameterInputList from '@/components/ParameterInputList.vue';
import NodeCredentials from '@/components/NodeCredentials.vue';
import NodeSettingsTabs from '@/components/NodeSettingsTabs.vue';
import NodeWebhooks from '@/components/NodeWebhooks.vue';
import NDVSubConnections from '@/components/NDVSubConnections.vue';
import { get, set, unset } from 'lodash-es';
import NodeExecuteButton from './NodeExecuteButton.vue';
@ -223,6 +232,7 @@ export default defineComponent({
ParameterInputList,
NodeSettingsTabs,
NodeWebhooks,
NDVSubConnections,
NodeExecuteButton,
},
setup() {
@ -467,6 +477,12 @@ export default defineComponent({
this.eventBus?.off('openSettings', this.openSettings);
},
methods: {
onSwitchSelectedNode(node: string) {
this.$emit('switchSelectedNode', node);
},
onOpenConnectionNodeCreator(node: string, connectionType: ConnectionTypes) {
this.$emit('openConnectionNodeCreator', node, connectionType);
},
populateHiddenIssuesSet() {
if (!this.node || !this.workflowsStore.isNodePristine(this.node.name)) return;
@ -612,6 +628,7 @@ export default defineComponent({
},
onNodeExecute() {
this.hiddenIssuesInputs = [];
(this.$refs.subConnections as InstanceType<typeof NDVSubConnections>)?.showNodeInputsIssues();
this.$emit('execute');
},
setValue(name: string, value: NodeParameterValue) {

View file

@ -94,6 +94,7 @@
:is-production-execution-preview="isProductionExecutionPreview"
@redrawNode="redrawNode"
@switchSelectedNode="onSwitchSelectedNode"
@openConnectionNodeCreator="onOpenConnectionNodeCreator"
@valueChanged="valueChanged"
@stopExecution="stopExecution"
@saveKeyboardShortcut="onSaveKeyboardShortcut"
@ -900,38 +901,7 @@ export default defineComponent({
this.registerCustomAction({
key: 'openSelectiveNodeCreator',
action: async ({
connectiontype,
node,
creatorview,
}: {
connectiontype: NodeConnectionType;
node: string;
creatorview?: string;
}) => {
const nodeName = node ?? this.ndvStore.activeNodeName;
const nodeData = nodeName ? this.workflowsStore.getNodeByName(nodeName) : null;
this.ndvStore.activeNodeName = null;
await this.redrawNode(node);
// Wait for UI to update
setTimeout(() => {
if (creatorview) {
this.onToggleNodeCreator({
createNodeActive: true,
nodeCreatorView: creatorview,
});
} else if (connectiontype && nodeData) {
this.insertNodeAfterSelected({
index: 0,
endpointUuid: `${nodeData.id}-input${connectiontype}0`,
eventSource: NODE_CREATOR_OPEN_SOURCES.NOTICE_ERROR_MESSAGE,
outputType: connectiontype,
sourceId: nodeData.id,
});
}
}, 0);
},
action: this.openSelectiveNodeCreator,
});
this.readOnlyEnvRouteCheck();
@ -1022,6 +992,38 @@ export default defineComponent({
sourceControlEventBus.off('pull', this.onSourceControlPull);
},
methods: {
async openSelectiveNodeCreator({
connectiontype,
node,
creatorview,
}: {
connectiontype: ConnectionTypes;
node: string;
creatorview?: string;
}) {
const nodeName = node ?? this.ndvStore.activeNodeName;
const nodeData = nodeName ? this.workflowsStore.getNodeByName(nodeName) : null;
this.ndvStore.activeNodeName = null;
await this.redrawNode(node);
// Wait for UI to update
setTimeout(() => {
if (creatorview) {
this.onToggleNodeCreator({
createNodeActive: true,
nodeCreatorView: creatorview,
});
} else if (connectiontype && nodeData) {
this.insertNodeAfterSelected({
index: 0,
endpointUuid: `${nodeData.id}-input${connectiontype}0`,
eventSource: NODE_CREATOR_OPEN_SOURCES.NOTICE_ERROR_MESSAGE,
outputType: connectiontype,
sourceId: nodeData.id,
});
}
});
},
editAllowedCheck(): boolean {
if (this.readOnlyNotification?.visible) {
return;
@ -3966,6 +3968,12 @@ export default defineComponent({
async onSwitchSelectedNode(nodeName: string) {
this.nodeSelectedByName(nodeName, true, true);
},
async onOpenConnectionNodeCreator(node: string, connectionType: ConnectionTypes) {
await this.openSelectiveNodeCreator({
connectiontype: connectionType,
node,
});
},
async redrawNode(nodeName: string) {
// TODO: Improve later
// For now we redraw the node by simply renaming it. Can for sure be