fix(editor): Hide fromAI button in old workflow tool (#13552)

This commit is contained in:
Charlie Kolb 2025-03-03 09:00:45 +01:00 committed by GitHub
parent 1c8c7e34f9
commit 6ef8d34f96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 60 additions and 25 deletions

View file

@ -27,7 +27,6 @@ import {
makeOverrideValue, makeOverrideValue,
updateFromAIOverrideValues, updateFromAIOverrideValues,
} from '../utils/fromAIOverrideUtils'; } from '../utils/fromAIOverrideUtils';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useTelemetry } from '@/composables/useTelemetry'; import { useTelemetry } from '@/composables/useTelemetry';
type Props = { type Props = {
@ -69,16 +68,10 @@ const menuExpanded = ref(false);
const forceShowExpression = ref(false); const forceShowExpression = ref(false);
const ndvStore = useNDVStore(); const ndvStore = useNDVStore();
const nodeTypesStore = useNodeTypesStore();
const telemetry = useTelemetry(); const telemetry = useTelemetry();
const node = computed(() => ndvStore.activeNode); const node = computed(() => ndvStore.activeNode);
const fromAIOverride = ref<FromAIOverride | null>( const fromAIOverride = ref<FromAIOverride | null>(makeOverrideValue(props, node.value));
makeOverrideValue(
props,
node.value && nodeTypesStore.getNodeType(node.value.type, node.value.typeVersion),
),
);
const canBeContentOverride = computed(() => { const canBeContentOverride = computed(() => {
// The resourceLocator handles overrides separately // The resourceLocator handles overrides separately

View file

@ -295,7 +295,7 @@ const fromAIOverride = ref<FromAIOverride | null>(
value: props.modelValue?.value ?? '', value: props.modelValue?.value ?? '',
...props, ...props,
}, },
props.node && nodeTypesStore.getNodeType(props.node.type, props.node.typeVersion), props.node,
), ),
); );

View file

@ -1,3 +1,4 @@
import type { INodeUi } from '@/Interface';
import type { FromAIOverride, OverrideContext } from './fromAIOverrideUtils'; import type { FromAIOverride, OverrideContext } from './fromAIOverrideUtils';
import { import {
buildValueFromOverride, buildValueFromOverride,
@ -8,6 +9,14 @@ import {
} from './fromAIOverrideUtils'; } from './fromAIOverrideUtils';
import type { INodeTypeDescription, NodePropertyTypes } from 'n8n-workflow'; 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 DISPLAY_NAME = 'aDisplayName';
const PARAMETER_NAME = 'aName'; const PARAMETER_NAME = 'aName';
@ -26,7 +35,7 @@ const makeContext = (
}); });
const MOCK_NODE_TYPE_MIXIN = { const MOCK_NODE_TYPE_MIXIN = {
version: 0, version: 1,
defaults: {}, defaults: {},
inputs: [], inputs: [],
outputs: [], outputs: [],
@ -74,22 +83,39 @@ const NON_AI_NODE_TYPE: INodeTypeDescription = {
...MOCK_NODE_TYPE_MIXIN, ...MOCK_NODE_TYPE_MIXIN,
}; };
function mockNodeFromType(type: INodeTypeDescription) {
return vi.mocked<INodeUi>({
type: type.name,
typeVersion: type.version as number,
} as never);
}
describe('makeOverrideValue', () => { describe('makeOverrideValue', () => {
test.each<[string, ...Parameters<typeof makeOverrideValue>]>([ test.each<[string, ...Parameters<typeof makeOverrideValue>]>([
['null nodeType', makeContext(''), null], ['null nodeType', makeContext(''), null],
['non-ai node type', makeContext(''), NON_AI_NODE_TYPE], ['non-ai node type', makeContext(''), mockNodeFromType(NON_AI_NODE_TYPE)],
['ai node type on denylist', makeContext(''), AI_DENYLIST_NODE_TYPE], ['ai node type on denylist', makeContext(''), mockNodeFromType(AI_DENYLIST_NODE_TYPE)],
['vector store type', makeContext(''), AI_VECTOR_STORE_NODE_TYPE], ['vector store type', makeContext(''), mockNodeFromType(AI_VECTOR_STORE_NODE_TYPE)],
['denied parameter name', makeContext('', 'parameters.toolName'), AI_NODE_TYPE], [
['denied parameter type', makeContext('', undefined, 'credentialsSelect'), AI_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) => { ])('should not create an override for %s', (_name, context, nodeType) => {
getNodeType.mockReturnValue(nodeType);
expect(makeOverrideValue(context, nodeType)).toBeNull(); expect(makeOverrideValue(context, nodeType)).toBeNull();
}); });
it('should create an fromAI override', () => { it('should create an fromAI override', () => {
getNodeType.mockReturnValue(AI_NODE_TYPE);
const result = makeOverrideValue( const result = makeOverrideValue(
makeContext(`={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('${DISPLAY_NAME}') }}`), makeContext(`={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('${DISPLAY_NAME}') }}`),
AI_NODE_TYPE, mockNodeFromType(AI_NODE_TYPE),
); );
expect(result).not.toBeNull(); expect(result).not.toBeNull();
@ -97,12 +123,14 @@ describe('makeOverrideValue', () => {
}); });
it('parses existing fromAI overrides', () => { it('parses existing fromAI overrides', () => {
getNodeType.mockReturnValue(AI_NODE_TYPE);
const description = 'a description'; const description = 'a description';
const result = makeOverrideValue( const result = makeOverrideValue(
makeContext( makeContext(
`={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('${DISPLAY_NAME}', \`${description}\`) }}`, `={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('${DISPLAY_NAME}', \`${description}\`) }}`,
), ),
AI_NODE_TYPE, mockNodeFromType(AI_NODE_TYPE),
); );
expect(result).toBeDefined(); expect(result).toBeDefined();
@ -110,9 +138,11 @@ describe('makeOverrideValue', () => {
}); });
it('parses an existing fromAI override with default values without adding extraPropValue entry', () => { it('parses an existing fromAI override with default values without adding extraPropValue entry', () => {
getNodeType.mockReturnValue(AI_NODE_TYPE);
const result = makeOverrideValue( const result = makeOverrideValue(
makeContext("={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('aName', ``) }}"), makeContext("={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('aName', ``) }}"),
AI_NODE_TYPE, mockNodeFromType(AI_NODE_TYPE),
); );
expect(result).toBeDefined(); expect(result).toBeDefined();

View file

@ -1,11 +1,12 @@
import { import {
extractFromAICalls, extractFromAICalls,
FROM_AI_AUTO_GENERATED_MARKER, FROM_AI_AUTO_GENERATED_MARKER,
type INodeTypeDescription,
type NodeParameterValueType, type NodeParameterValueType,
type NodePropertyTypes, type NodePropertyTypes,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { i18n } from '@/plugins/i18n'; import { i18n } from '@/plugins/i18n';
import type { INodeUi } from '@/Interface';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
export type OverrideContext = { export type OverrideContext = {
parameter: { parameter: {
@ -44,7 +45,8 @@ function sanitizeFromAiParameterName(s: string) {
return s; return s;
} }
const NODE_DENYLIST = ['toolCode', 'toolHttpRequest']; // nodeName | [nodeName, highestUnsupportedVersion]
const NODE_DENYLIST = ['toolCode', 'toolHttpRequest', ['toolWorkflow', 1.2]] as const;
const PATH_DENYLIST = [ const PATH_DENYLIST = [
'parameters.name', 'parameters.name',
@ -159,16 +161,26 @@ export function parseOverrides(
return null; 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( export function canBeContentOverride(
props: Pick<OverrideContext, 'path' | 'parameter'>, props: Pick<OverrideContext, 'path' | 'parameter'>,
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 (PATH_DENYLIST.includes(props.path)) return false;
if (PROP_TYPE_DENYLIST.includes(props.parameter.type)) return false; if (PROP_TYPE_DENYLIST.includes(props.parameter.type)) return false;
const nodeType = useNodeTypesStore().getNodeType(node.type, node?.typeVersion);
const codex = nodeType?.codex; const codex = nodeType?.codex;
if ( if (
!codex?.categories?.includes('AI') || !codex?.categories?.includes('AI') ||
@ -182,11 +194,11 @@ export function canBeContentOverride(
export function makeOverrideValue( export function makeOverrideValue(
context: OverrideContext, context: OverrideContext,
nodeType: INodeTypeDescription | null | undefined, node: INodeUi | null | undefined,
): FromAIOverride | null { ): FromAIOverride | null {
if (!nodeType) return null; if (!node) return null;
if (canBeContentOverride(context, nodeType)) { if (canBeContentOverride(context, node)) {
const fromAiOverride: FromAIOverride = { const fromAiOverride: FromAIOverride = {
type: 'fromAI', type: 'fromAI',
extraProps: fromAIExtraProps, extraProps: fromAIExtraProps,