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,
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<FromAIOverride | null>(
makeOverrideValue(
props,
node.value && nodeTypesStore.getNodeType(node.value.type, node.value.typeVersion),
),
);
const fromAIOverride = ref<FromAIOverride | null>(makeOverrideValue(props, node.value));
const canBeContentOverride = computed(() => {
// The resourceLocator handles overrides separately

View file

@ -295,7 +295,7 @@ const fromAIOverride = ref<FromAIOverride | null>(
value: props.modelValue?.value ?? '',
...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 {
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<INodeUi>({
type: type.name,
typeVersion: type.version as number,
} as never);
}
describe('makeOverrideValue', () => {
test.each<[string, ...Parameters<typeof makeOverrideValue>]>([
['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();

View file

@ -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<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 (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,