From 6ef8d34f969ddb9e80b82dc50b38698249089af2 Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Mon, 3 Mar 2025 09:00:45 +0100 Subject: [PATCH] fix(editor): Hide fromAI button in old workflow tool (#13552) --- .../src/components/ParameterInputFull.vue | 9 +--- .../ResourceLocator/ResourceLocator.vue | 2 +- .../src/utils/fromAIOverrideUtils.test.ts | 48 +++++++++++++++---- .../src/utils/fromAIOverrideUtils.ts | 26 +++++++--- 4 files changed, 60 insertions(+), 25 deletions(-) diff --git a/packages/frontend/editor-ui/src/components/ParameterInputFull.vue b/packages/frontend/editor-ui/src/components/ParameterInputFull.vue index b0a6f8f9b6..62cb8b3f26 100644 --- a/packages/frontend/editor-ui/src/components/ParameterInputFull.vue +++ b/packages/frontend/editor-ui/src/components/ParameterInputFull.vue @@ -27,7 +27,6 @@ import { makeOverrideValue, updateFromAIOverrideValues, } from '../utils/fromAIOverrideUtils'; -import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useTelemetry } from '@/composables/useTelemetry'; type Props = { @@ -69,16 +68,10 @@ const menuExpanded = ref(false); const forceShowExpression = ref(false); const ndvStore = useNDVStore(); -const nodeTypesStore = useNodeTypesStore(); const telemetry = useTelemetry(); const node = computed(() => ndvStore.activeNode); -const fromAIOverride = ref( - makeOverrideValue( - props, - node.value && nodeTypesStore.getNodeType(node.value.type, node.value.typeVersion), - ), -); +const fromAIOverride = ref(makeOverrideValue(props, node.value)); const canBeContentOverride = computed(() => { // The resourceLocator handles overrides separately diff --git a/packages/frontend/editor-ui/src/components/ResourceLocator/ResourceLocator.vue b/packages/frontend/editor-ui/src/components/ResourceLocator/ResourceLocator.vue index f287a26c84..649272a12a 100644 --- a/packages/frontend/editor-ui/src/components/ResourceLocator/ResourceLocator.vue +++ b/packages/frontend/editor-ui/src/components/ResourceLocator/ResourceLocator.vue @@ -295,7 +295,7 @@ const fromAIOverride = ref( value: props.modelValue?.value ?? '', ...props, }, - props.node && nodeTypesStore.getNodeType(props.node.type, props.node.typeVersion), + props.node, ), ); diff --git a/packages/frontend/editor-ui/src/utils/fromAIOverrideUtils.test.ts b/packages/frontend/editor-ui/src/utils/fromAIOverrideUtils.test.ts index adbd70dee4..227b4732c3 100644 --- a/packages/frontend/editor-ui/src/utils/fromAIOverrideUtils.test.ts +++ b/packages/frontend/editor-ui/src/utils/fromAIOverrideUtils.test.ts @@ -1,3 +1,4 @@ +import type { INodeUi } from '@/Interface'; import type { FromAIOverride, OverrideContext } from './fromAIOverrideUtils'; import { buildValueFromOverride, @@ -8,6 +9,14 @@ import { } from './fromAIOverrideUtils'; import type { INodeTypeDescription, NodePropertyTypes } from 'n8n-workflow'; +const getNodeType = vi.fn(); + +vi.mock('@/stores/nodeTypes.store', () => ({ + useNodeTypesStore: vi.fn(() => ({ + getNodeType, + })), +})); + const DISPLAY_NAME = 'aDisplayName'; const PARAMETER_NAME = 'aName'; @@ -26,7 +35,7 @@ const makeContext = ( }); const MOCK_NODE_TYPE_MIXIN = { - version: 0, + version: 1, defaults: {}, inputs: [], outputs: [], @@ -74,22 +83,39 @@ const NON_AI_NODE_TYPE: INodeTypeDescription = { ...MOCK_NODE_TYPE_MIXIN, }; +function mockNodeFromType(type: INodeTypeDescription) { + return vi.mocked({ + type: type.name, + typeVersion: type.version as number, + } as never); +} + describe('makeOverrideValue', () => { test.each<[string, ...Parameters]>([ ['null nodeType', makeContext(''), null], - ['non-ai node type', makeContext(''), NON_AI_NODE_TYPE], - ['ai node type on denylist', makeContext(''), AI_DENYLIST_NODE_TYPE], - ['vector store type', makeContext(''), AI_VECTOR_STORE_NODE_TYPE], - ['denied parameter name', makeContext('', 'parameters.toolName'), AI_NODE_TYPE], - ['denied parameter type', makeContext('', undefined, 'credentialsSelect'), AI_NODE_TYPE], + ['non-ai node type', makeContext(''), mockNodeFromType(NON_AI_NODE_TYPE)], + ['ai node type on denylist', makeContext(''), mockNodeFromType(AI_DENYLIST_NODE_TYPE)], + ['vector store type', makeContext(''), mockNodeFromType(AI_VECTOR_STORE_NODE_TYPE)], + [ + 'denied parameter name', + makeContext('', 'parameters.toolName'), + mockNodeFromType(AI_NODE_TYPE), + ], + [ + 'denied parameter type', + makeContext('', undefined, 'credentialsSelect'), + mockNodeFromType(AI_NODE_TYPE), + ], ])('should not create an override for %s', (_name, context, nodeType) => { + getNodeType.mockReturnValue(nodeType); expect(makeOverrideValue(context, nodeType)).toBeNull(); }); it('should create an fromAI override', () => { + getNodeType.mockReturnValue(AI_NODE_TYPE); const result = makeOverrideValue( makeContext(`={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('${DISPLAY_NAME}') }}`), - AI_NODE_TYPE, + mockNodeFromType(AI_NODE_TYPE), ); expect(result).not.toBeNull(); @@ -97,12 +123,14 @@ describe('makeOverrideValue', () => { }); it('parses existing fromAI overrides', () => { + getNodeType.mockReturnValue(AI_NODE_TYPE); + const description = 'a description'; const result = makeOverrideValue( makeContext( `={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('${DISPLAY_NAME}', \`${description}\`) }}`, ), - AI_NODE_TYPE, + mockNodeFromType(AI_NODE_TYPE), ); expect(result).toBeDefined(); @@ -110,9 +138,11 @@ describe('makeOverrideValue', () => { }); it('parses an existing fromAI override with default values without adding extraPropValue entry', () => { + getNodeType.mockReturnValue(AI_NODE_TYPE); + const result = makeOverrideValue( makeContext("={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('aName', ``) }}"), - AI_NODE_TYPE, + mockNodeFromType(AI_NODE_TYPE), ); expect(result).toBeDefined(); diff --git a/packages/frontend/editor-ui/src/utils/fromAIOverrideUtils.ts b/packages/frontend/editor-ui/src/utils/fromAIOverrideUtils.ts index fc8348c84d..cea75a3956 100644 --- a/packages/frontend/editor-ui/src/utils/fromAIOverrideUtils.ts +++ b/packages/frontend/editor-ui/src/utils/fromAIOverrideUtils.ts @@ -1,11 +1,12 @@ import { extractFromAICalls, FROM_AI_AUTO_GENERATED_MARKER, - type INodeTypeDescription, type NodeParameterValueType, type NodePropertyTypes, } from 'n8n-workflow'; import { i18n } from '@/plugins/i18n'; +import type { INodeUi } from '@/Interface'; +import { useNodeTypesStore } from '@/stores/nodeTypes.store'; export type OverrideContext = { parameter: { @@ -44,7 +45,8 @@ function sanitizeFromAiParameterName(s: string) { return s; } -const NODE_DENYLIST = ['toolCode', 'toolHttpRequest']; +// nodeName | [nodeName, highestUnsupportedVersion] +const NODE_DENYLIST = ['toolCode', 'toolHttpRequest', ['toolWorkflow', 1.2]] as const; const PATH_DENYLIST = [ 'parameters.name', @@ -159,16 +161,26 @@ export function parseOverrides( return null; } +function isDeniedNode(nodeDenyData: string | readonly [string, number], node: INodeUi) { + if (typeof nodeDenyData === 'string') { + return node.type.endsWith(nodeDenyData); + } else { + const [name, version] = nodeDenyData; + return node.type.endsWith(name) && node.typeVersion <= version; + } +} + export function canBeContentOverride( props: Pick, - nodeType: INodeTypeDescription | null, + node: INodeUi, ) { - if (NODE_DENYLIST.some((x) => nodeType?.name?.endsWith(x) ?? false)) return false; + if (NODE_DENYLIST.some((x) => isDeniedNode(x, node))) return false; if (PATH_DENYLIST.includes(props.path)) return false; if (PROP_TYPE_DENYLIST.includes(props.parameter.type)) return false; + const nodeType = useNodeTypesStore().getNodeType(node.type, node?.typeVersion); const codex = nodeType?.codex; if ( !codex?.categories?.includes('AI') || @@ -182,11 +194,11 @@ export function canBeContentOverride( export function makeOverrideValue( context: OverrideContext, - nodeType: INodeTypeDescription | null | undefined, + node: INodeUi | null | undefined, ): FromAIOverride | null { - if (!nodeType) return null; + if (!node) return null; - if (canBeContentOverride(context, nodeType)) { + if (canBeContentOverride(context, node)) { const fromAiOverride: FromAIOverride = { type: 'fromAI', extraProps: fromAIExtraProps,