fix(editor): Allow pinning of AI root nodes (#9060)

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
This commit is contained in:
oleg
2024-04-05 15:00:31 +02:00
committed by GitHub
parent caea27dbb5
commit 32df17104c
7 changed files with 127 additions and 121 deletions

View File

@@ -1,20 +1,15 @@
import type { XYPosition } from '@/Interface';
import {
NOT_DUPLICATABE_NODE_TYPES,
PIN_DATA_NODE_TYPES_DENYLIST,
STICKY_NODE_TYPE,
} from '@/constants';
import { NOT_DUPLICATABE_NODE_TYPES, STICKY_NODE_TYPE } from '@/constants';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import type { IActionDropdownItem } from 'n8n-design-system/src/components/N8nActionDropdown/ActionDropdown.vue';
import { NodeHelpers, NodeConnectionType } from 'n8n-workflow';
import type { INode, INodeTypeDescription } from 'n8n-workflow';
import { computed, ref, watch } from 'vue';
import { getMousePosition } from '../utils/nodeViewUtils';
import { useI18n } from './useI18n';
import { useDataSchema } from './useDataSchema';
import { usePinnedData } from './usePinnedData';
export type ContextMenuTarget =
| { source: 'canvas' }
@@ -47,7 +42,7 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
const nodeTypesStore = useNodeTypesStore();
const workflowsStore = useWorkflowsStore();
const sourceControlStore = useSourceControlStore();
const { getInputDataWithPinned } = useDataSchema();
const i18n = useI18n();
const isReadOnly = computed(
@@ -83,13 +78,6 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
return canAddNodeOfType(nodeType);
};
const canPinNode = (node: INode): boolean => {
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
const dataToPin = getInputDataWithPinned(node);
if (!nodeType || dataToPin.length === 0) return false;
return nodeType.outputs.length === 1 && !PIN_DATA_NODE_TYPES_DENYLIST.includes(node.type);
};
const hasPinData = (node: INode): boolean => {
return !!workflowsStore.pinDataByNodeName(node.name);
};
@@ -159,16 +147,6 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
...selectionActions,
];
} else {
const nonMainInputs = (node: INode) => {
const workflow = workflowsStore.getCurrentWorkflow();
const workflowNode = workflow.getNode(node.name);
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
const inputs = NodeHelpers.getNodeInputs(workflow, workflowNode!, nodeType!);
const inputNames = NodeHelpers.getConnectionTypes(inputs);
return !!inputNames.find((inputName) => inputName !== NodeConnectionType.Main);
};
const menuActions: IActionDropdownItem[] = [
!onlyStickies && {
id: 'toggle_activation',
@@ -184,7 +162,7 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
? i18n.baseText('contextMenu.unpin', i18nOptions)
: i18n.baseText('contextMenu.pin', i18nOptions),
shortcut: { keys: ['p'] },
disabled: nodes.some(nonMainInputs) || isReadOnly.value || !nodes.every(canPinNode),
disabled: isReadOnly.value || !nodes.every((n) => usePinnedData(n).canPinNode(true)),
},
{
id: 'copy',

View File

@@ -1,7 +1,7 @@
import { useToast } from '@/composables/useToast';
import { useI18n } from '@/composables/useI18n';
import type { INodeExecutionData, IPinData } from 'n8n-workflow';
import { jsonParse, jsonStringify } from 'n8n-workflow';
import { jsonParse, jsonStringify, NodeConnectionType, NodeHelpers } from 'n8n-workflow';
import {
MAX_EXPECTED_REQUEST_SIZE,
MAX_PINNED_DATA_SIZE,
@@ -18,6 +18,8 @@ import { computed, unref } from 'vue';
import { useRootStore } from '@/stores/n8nRoot.store';
import { storeToRefs } from 'pinia';
import { useNodeType } from '@/composables/useNodeType';
import { useDataSchema } from './useDataSchema';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
export type PinDataSource =
| 'pin-icon-click'
@@ -47,6 +49,7 @@ export function usePinnedData(
const i18n = useI18n();
const telemetry = useTelemetry();
const externalHooks = useExternalHooks();
const { getInputDataWithPinned } = useDataSchema();
const { pushRef } = storeToRefs(rootStore);
const { isSubNodeType, isMultipleOutputsNodeType } = useNodeType({
@@ -73,6 +76,26 @@ export function usePinnedData(
);
});
function canPinNode(checkDataEmpty = false) {
const targetNode = unref(node);
if (targetNode === null) return false;
const nodeType = useNodeTypesStore().getNodeType(targetNode.type, targetNode.typeVersion);
const dataToPin = getInputDataWithPinned(targetNode);
if (!nodeType || (checkDataEmpty && dataToPin.length === 0)) return false;
const workflow = workflowsStore.getCurrentWorkflow();
const outputs = NodeHelpers.getNodeOutputs(workflow, targetNode, nodeType);
const mainOutputs = outputs.filter((output) =>
typeof output === 'string'
? output === NodeConnectionType.Main
: output.type === NodeConnectionType.Main,
);
return mainOutputs.length === 1 && !PIN_DATA_NODE_TYPES_DENYLIST.includes(targetNode.type);
}
function isValidJSON(data: string): boolean {
try {
JSON.parse(data);
@@ -246,6 +269,7 @@ export function usePinnedData(
data,
hasData,
isValidNodeType,
canPinNode,
setData,
onSetDataSuccess,
onSetDataError,