mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-19 07:36:52 +00:00
feat(editor): Gate n8n Connect selector on node typeVersion (no-changelog) (#32565)
This commit is contained in:
@@ -19,4 +19,5 @@ export class AiGatewayConfigDto extends Z.class({
|
||||
credentialTypes: z.array(z.string()),
|
||||
providerConfig: z.record(z.object(aiGatewayProviderConfigEntryShape)),
|
||||
supportedActions: z.record(z.record(z.array(z.string()))).optional(),
|
||||
minNodeTypeVersion: z.record(z.number()).optional(),
|
||||
}) {}
|
||||
|
||||
@@ -30,6 +30,9 @@ export function useAiGateway() {
|
||||
operation: string,
|
||||
): boolean => aiGatewayStore.isActionSupported(nodeName, resource, operation);
|
||||
|
||||
const isNodeTypeVersionSupported = (nodeName: string, typeVersion: number): boolean =>
|
||||
aiGatewayStore.isNodeTypeVersionSupported(nodeName, typeVersion);
|
||||
|
||||
async function fetchConfig(): Promise<void> {
|
||||
if (!isEnabled.value) return;
|
||||
await aiGatewayStore.fetchConfig();
|
||||
@@ -48,6 +51,7 @@ export function useAiGateway() {
|
||||
fetchWallet,
|
||||
isCredentialTypeSupported,
|
||||
isActionSupported,
|
||||
isNodeTypeVersionSupported,
|
||||
saveAfterToggle,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -371,4 +371,64 @@ describe('aiGateway.store', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNodeTypeVersionSupported()', () => {
|
||||
const CONFIG_WITH_VERSION_REQ = {
|
||||
...MOCK_CONFIG,
|
||||
nodes: [...MOCK_CONFIG.nodes, 'some-package.SomeNode'],
|
||||
credentialTypes: [...MOCK_CONFIG.credentialTypes, 'someApi'],
|
||||
minNodeTypeVersion: { 'some-package.SomeNode': 1.1 },
|
||||
};
|
||||
|
||||
it('should return true when typeVersion meets the minimum', async () => {
|
||||
mockGetGatewayConfig.mockResolvedValue(CONFIG_WITH_VERSION_REQ);
|
||||
const store = useAiGatewayStore();
|
||||
await store.fetchConfig();
|
||||
|
||||
expect(store.isNodeTypeVersionSupported('some-package.SomeNode', 1.1)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when typeVersion exceeds the minimum', async () => {
|
||||
mockGetGatewayConfig.mockResolvedValue(CONFIG_WITH_VERSION_REQ);
|
||||
const store = useAiGatewayStore();
|
||||
await store.fetchConfig();
|
||||
|
||||
expect(store.isNodeTypeVersionSupported('some-package.SomeNode', 2)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when typeVersion is below the minimum', async () => {
|
||||
mockGetGatewayConfig.mockResolvedValue(CONFIG_WITH_VERSION_REQ);
|
||||
const store = useAiGatewayStore();
|
||||
await store.fetchConfig();
|
||||
|
||||
expect(store.isNodeTypeVersionSupported('some-package.SomeNode', 1.0)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when no minNodeTypeVersion entry exists for the node (no version gate)', async () => {
|
||||
mockGetGatewayConfig.mockResolvedValue(CONFIG_WITH_VERSION_REQ);
|
||||
const store = useAiGatewayStore();
|
||||
await store.fetchConfig();
|
||||
|
||||
expect(
|
||||
store.isNodeTypeVersionSupported('@n8n/n8n-nodes-langchain.lmChatGoogleGemini', 1),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for a node with no version requirement (even if unknown)', async () => {
|
||||
mockGetGatewayConfig.mockResolvedValue(CONFIG_WITH_VERSION_REQ);
|
||||
const store = useAiGatewayStore();
|
||||
await store.fetchConfig();
|
||||
|
||||
// No minNodeTypeVersion entry = no version gate; node support is a separate concern
|
||||
expect(store.isNodeTypeVersionSupported('unknown-package.UnknownNode', 2)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when config has not been loaded (no version gate defined)', () => {
|
||||
const store = useAiGatewayStore();
|
||||
|
||||
// config not loaded → no minNodeTypeVersion entry → no version gate → pass through
|
||||
// node support when config is unloaded is handled by isCredentialTypeSupported / isNodeSupported
|
||||
expect(store.isNodeTypeVersionSupported('some-package.SomeNode', 1.1)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,6 +104,12 @@ export const useAiGatewayStore = defineStore(STORES.AI_GATEWAY, () => {
|
||||
return ops.includes(operation);
|
||||
}
|
||||
|
||||
function isNodeTypeVersionSupported(nodeName: string, typeVersion: number): boolean {
|
||||
const minVersion = config.value?.minNodeTypeVersion?.[nodeName];
|
||||
if (minVersion === undefined) return true;
|
||||
return typeVersion >= minVersion;
|
||||
}
|
||||
|
||||
return {
|
||||
config,
|
||||
balance,
|
||||
@@ -116,6 +122,7 @@ export const useAiGatewayStore = defineStore(STORES.AI_GATEWAY, () => {
|
||||
fetchUsage,
|
||||
fetchMoreUsage,
|
||||
isNodeSupported,
|
||||
isNodeTypeVersionSupported,
|
||||
isCredentialTypeSupported,
|
||||
isActionSupported,
|
||||
};
|
||||
|
||||
+167
@@ -1110,6 +1110,7 @@ describe('NodeCredentials', () => {
|
||||
vi.mocked(useAiGateway).mockReturnValue({
|
||||
isEnabled: computed(() => true),
|
||||
isCredentialTypeSupported: vi.fn((credType: string) => credType === 'googlePalmApi'),
|
||||
isNodeTypeVersionSupported: vi.fn(() => true),
|
||||
isActionSupported: vi.fn(() => true),
|
||||
balance: computed(() => undefined),
|
||||
budget: computed(() => undefined),
|
||||
@@ -1183,6 +1184,7 @@ describe('NodeCredentials', () => {
|
||||
vi.mocked(useAiGateway).mockReturnValue({
|
||||
isEnabled: computed(() => true),
|
||||
isCredentialTypeSupported: vi.fn(() => false),
|
||||
isNodeTypeVersionSupported: vi.fn(() => true),
|
||||
isActionSupported: vi.fn(() => true),
|
||||
balance: computed(() => undefined),
|
||||
budget: computed(() => undefined),
|
||||
@@ -1211,6 +1213,7 @@ describe('NodeCredentials', () => {
|
||||
vi.mocked(useAiGateway).mockReturnValue({
|
||||
isEnabled: computed(() => false),
|
||||
isCredentialTypeSupported: vi.fn(() => false),
|
||||
isNodeTypeVersionSupported: vi.fn(() => true),
|
||||
isActionSupported: vi.fn(() => true),
|
||||
balance: computed(() => undefined),
|
||||
budget: computed(() => undefined),
|
||||
@@ -1264,6 +1267,170 @@ describe('NodeCredentials', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('minNodeTypeVersion gate', () => {
|
||||
const versionedNodeType: INodeTypeDescription = {
|
||||
displayName: 'Some Node',
|
||||
name: 'some-package.SomeNode',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description: '',
|
||||
defaults: { name: 'Some Node' },
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
credentials: [{ name: 'someApi', required: true }],
|
||||
properties: [],
|
||||
};
|
||||
|
||||
const someApiCredType: ICredentialType = {
|
||||
name: 'someApi',
|
||||
displayName: 'Some API',
|
||||
properties: [{ displayName: 'API Key', name: 'apiKey', type: 'string', default: '' }],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||
nodeTypesStore.setNodeTypes([versionedNodeType]);
|
||||
credentialsStore.state.credentialTypes = { someApi: someApiCredType };
|
||||
});
|
||||
|
||||
it('should hide AiGatewaySelector when typeVersion is below the minimum', () => {
|
||||
vi.mocked(useAiGateway).mockReturnValue({
|
||||
isEnabled: computed(() => true),
|
||||
isCredentialTypeSupported: vi.fn((credType: string) => credType === 'someApi'),
|
||||
isNodeTypeVersionSupported: vi.fn(() => false),
|
||||
isActionSupported: vi.fn(() => true),
|
||||
balance: computed(() => undefined),
|
||||
budget: computed(() => undefined),
|
||||
fetchConfig: vi.fn().mockResolvedValue(undefined),
|
||||
fetchWallet: vi.fn().mockResolvedValue(undefined),
|
||||
saveAfterToggle: vi.fn().mockResolvedValue(undefined),
|
||||
fetchError: computed(() => null),
|
||||
});
|
||||
|
||||
const node: INodeUi = {
|
||||
id: 'node-some',
|
||||
name: 'Some Node',
|
||||
type: 'some-package.SomeNode',
|
||||
typeVersion: 1.0,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
credentials: {},
|
||||
};
|
||||
ndvStore.activeNode = node;
|
||||
|
||||
renderComponent({
|
||||
props: { node, overrideCredType: 'someApi' },
|
||||
global: { stubs: { AiGatewaySelector: aiGatewayToggleStub } },
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('ai-gateway-toggle')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show AiGatewaySelector when typeVersion meets the minimum', () => {
|
||||
vi.mocked(useAiGateway).mockReturnValue({
|
||||
isEnabled: computed(() => true),
|
||||
isCredentialTypeSupported: vi.fn((credType: string) => credType === 'someApi'),
|
||||
isNodeTypeVersionSupported: vi.fn(() => true),
|
||||
isActionSupported: vi.fn(() => true),
|
||||
balance: computed(() => undefined),
|
||||
budget: computed(() => undefined),
|
||||
fetchConfig: vi.fn().mockResolvedValue(undefined),
|
||||
fetchWallet: vi.fn().mockResolvedValue(undefined),
|
||||
saveAfterToggle: vi.fn().mockResolvedValue(undefined),
|
||||
fetchError: computed(() => null),
|
||||
});
|
||||
|
||||
const node: INodeUi = {
|
||||
id: 'node-some',
|
||||
name: 'Some Node',
|
||||
type: 'some-package.SomeNode',
|
||||
typeVersion: 1.1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
credentials: {},
|
||||
};
|
||||
ndvStore.activeNode = node;
|
||||
|
||||
renderComponent({
|
||||
props: { node, overrideCredType: 'someApi' },
|
||||
global: { stubs: { AiGatewaySelector: aiGatewayToggleStub } },
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('ai-gateway-toggle')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should emit credentialSelected clearing __aiGatewayManaged when version gate fails on mount', () => {
|
||||
vi.mocked(useAiGateway).mockReturnValue({
|
||||
isEnabled: computed(() => true),
|
||||
isCredentialTypeSupported: vi.fn((credType: string) => credType === 'someApi'),
|
||||
isNodeTypeVersionSupported: vi.fn(() => false),
|
||||
isActionSupported: vi.fn(() => true),
|
||||
balance: computed(() => undefined),
|
||||
budget: computed(() => undefined),
|
||||
fetchConfig: vi.fn().mockResolvedValue(undefined),
|
||||
fetchWallet: vi.fn().mockResolvedValue(undefined),
|
||||
saveAfterToggle: vi.fn().mockResolvedValue(undefined),
|
||||
fetchError: computed(() => null),
|
||||
});
|
||||
|
||||
const node: INodeUi = {
|
||||
id: 'node-some',
|
||||
name: 'Some Node',
|
||||
type: 'some-package.SomeNode',
|
||||
typeVersion: 1.0,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
credentials: { someApi: { id: null, name: '', __aiGatewayManaged: true } },
|
||||
};
|
||||
ndvStore.activeNode = node;
|
||||
|
||||
const { emitted } = renderComponent({
|
||||
props: { node, overrideCredType: 'someApi' },
|
||||
global: { stubs: { AiGatewaySelector: aiGatewayToggleStub } },
|
||||
});
|
||||
|
||||
expect(emitted('credentialSelected')).toBeTruthy();
|
||||
const payload = ((emitted('credentialSelected')[0] as unknown[]) ?? [])[0] as {
|
||||
properties: { credentials: Record<string, unknown> };
|
||||
};
|
||||
// No available credentials in store → entry is deleted, not restored
|
||||
expect(payload.properties.credentials['someApi']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not emit credentialSelected on mount when version gate fails but no managed credential exists', () => {
|
||||
vi.mocked(useAiGateway).mockReturnValue({
|
||||
isEnabled: computed(() => true),
|
||||
isCredentialTypeSupported: vi.fn((credType: string) => credType === 'someApi'),
|
||||
isNodeTypeVersionSupported: vi.fn(() => false),
|
||||
isActionSupported: vi.fn(() => true),
|
||||
balance: computed(() => undefined),
|
||||
budget: computed(() => undefined),
|
||||
fetchConfig: vi.fn().mockResolvedValue(undefined),
|
||||
fetchWallet: vi.fn().mockResolvedValue(undefined),
|
||||
saveAfterToggle: vi.fn().mockResolvedValue(undefined),
|
||||
fetchError: computed(() => null),
|
||||
});
|
||||
|
||||
const node: INodeUi = {
|
||||
id: 'node-some',
|
||||
name: 'Some Node',
|
||||
type: 'some-package.SomeNode',
|
||||
typeVersion: 1.0,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
credentials: {},
|
||||
};
|
||||
ndvStore.activeNode = node;
|
||||
|
||||
const { emitted } = renderComponent({
|
||||
props: { node, overrideCredType: 'someApi' },
|
||||
global: { stubs: { AiGatewaySelector: aiGatewayToggleStub } },
|
||||
});
|
||||
|
||||
expect(emitted('credentialSelected')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit credentialSelected with __aiGatewayManaged:true when toggled ON', async () => {
|
||||
ndvStore.activeNode = googleAiNode;
|
||||
|
||||
|
||||
+17
-1
@@ -226,6 +226,18 @@ watch(
|
||||
credentialTypesNodeDescriptionDisplayed,
|
||||
(types) => {
|
||||
if (props.skipAutoSelect) return;
|
||||
|
||||
if (
|
||||
aiGateway.isEnabled.value &&
|
||||
!aiGateway.isNodeTypeVersionSupported(node.value.type, node.value.typeVersion)
|
||||
) {
|
||||
for (const { type } of types) {
|
||||
if (selected.value[type.name]?.__aiGatewayManaged) {
|
||||
onAiGatewaySelector(type.name, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (types.length === 0 || !isEmpty(selected.value)) return;
|
||||
|
||||
const allOptions = types.map((type) => type.options).flat();
|
||||
@@ -234,7 +246,10 @@ watch(
|
||||
// No credentials configured — auto-enable AI Gateway for supported types
|
||||
if (aiGateway.isEnabled.value) {
|
||||
for (const { type } of types) {
|
||||
if (aiGateway.isCredentialTypeSupported(type.name)) {
|
||||
if (
|
||||
aiGateway.isCredentialTypeSupported(type.name) &&
|
||||
aiGateway.isNodeTypeVersionSupported(node.value.type, node.value.typeVersion)
|
||||
) {
|
||||
onAiGatewaySelector(type.name, true, false);
|
||||
}
|
||||
}
|
||||
@@ -565,6 +580,7 @@ function isAiGatewayManagedCredentials(credentialType: string): boolean {
|
||||
|
||||
function showAiGatewaySelector(credentialType: string): boolean {
|
||||
if (!aiGateway.isEnabled.value) return false;
|
||||
if (!aiGateway.isNodeTypeVersionSupported(node.value.type, node.value.typeVersion)) return false;
|
||||
if (isAiGatewayManagedCredentials(credentialType)) return true;
|
||||
if (!aiGateway.isCredentialTypeSupported(credentialType)) return false;
|
||||
return true;
|
||||
|
||||
+4
@@ -1746,6 +1746,7 @@ describe('ParameterInputList', () => {
|
||||
vi.mocked(useAiGateway).mockReturnValue({
|
||||
isEnabled: { value: true } as never,
|
||||
isCredentialTypeSupported: vi.fn(() => true),
|
||||
isNodeTypeVersionSupported: vi.fn(() => true),
|
||||
isActionSupported: vi.fn(() => false),
|
||||
balance: { value: undefined } as never,
|
||||
budget: { value: undefined } as never,
|
||||
@@ -1779,6 +1780,7 @@ describe('ParameterInputList', () => {
|
||||
vi.mocked(useAiGateway).mockReturnValue({
|
||||
isEnabled: { value: true } as never,
|
||||
isCredentialTypeSupported: vi.fn(() => true),
|
||||
isNodeTypeVersionSupported: vi.fn(() => true),
|
||||
isActionSupported: vi.fn(() => true),
|
||||
balance: { value: undefined } as never,
|
||||
budget: { value: undefined } as never,
|
||||
@@ -1857,6 +1859,7 @@ describe('ParameterInputList', () => {
|
||||
vi.mocked(useAiGateway).mockReturnValue({
|
||||
isEnabled: { value: true } as never,
|
||||
isCredentialTypeSupported: vi.fn(() => true),
|
||||
isNodeTypeVersionSupported: vi.fn(() => true),
|
||||
isActionSupported: vi.fn(() => false),
|
||||
balance: { value: undefined } as never,
|
||||
budget: { value: undefined } as never,
|
||||
@@ -1889,6 +1892,7 @@ describe('ParameterInputList', () => {
|
||||
vi.mocked(useAiGateway).mockReturnValue({
|
||||
isEnabled: { value: true } as never,
|
||||
isCredentialTypeSupported: vi.fn(() => true),
|
||||
isNodeTypeVersionSupported: vi.fn(() => true),
|
||||
isActionSupported: vi.fn(() => true),
|
||||
balance: { value: undefined } as never,
|
||||
budget: { value: undefined } as never,
|
||||
|
||||
@@ -38,11 +38,28 @@ import {
|
||||
AI_CATEGORY_ROOT_NODES,
|
||||
AI_SUBCATEGORY,
|
||||
} from '@/app/constants';
|
||||
import { useAiGatewayStore } from '@/app/stores/aiGateway.store';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { useSettingsStore } from '@/app/stores/settings.store';
|
||||
|
||||
vi.mock('@/app/stores/settings.store', () => ({
|
||||
useSettingsStore: vi.fn(() => ({ settings: {}, isAskAiEnabled: true })),
|
||||
}));
|
||||
|
||||
vi.mock('@/app/stores/aiGateway.store', () => ({
|
||||
useAiGatewayStore: vi.fn(() => ({
|
||||
isNodeSupported: vi.fn(() => false),
|
||||
isNodeTypeVersionSupported: vi.fn(() => true),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/app/stores/nodeTypes.store', () => ({
|
||||
useNodeTypesStore: vi.fn(() => ({
|
||||
getNodeVersions: vi.fn(() => []),
|
||||
communityNodeType: vi.fn(() => null),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('NodeCreator - utils', () => {
|
||||
describe('groupItemsInSections', () => {
|
||||
it('should handle multiple sections (with "other" section)', () => {
|
||||
@@ -738,6 +755,61 @@ describe('NodeCreator - utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('finalizeItems - Free credits badge (minNodeTypeVersion gate)', () => {
|
||||
const makeGatewayNode = (name = 'gatewayNode') => mockNodeCreateElement(undefined, { name });
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(useSettingsStore).mockReturnValue({
|
||||
isAiGatewayEnabled: true,
|
||||
} as unknown as ReturnType<typeof useSettingsStore>);
|
||||
vi.mocked(useAiGatewayStore).mockReturnValue({
|
||||
isNodeSupported: vi.fn(() => true),
|
||||
isNodeTypeVersionSupported: vi.fn(() => true),
|
||||
} as unknown as ReturnType<typeof useAiGatewayStore>);
|
||||
vi.mocked(useNodeTypesStore).mockReturnValue({
|
||||
getNodeVersions: vi.fn(() => [1, 1.1]),
|
||||
} as unknown as ReturnType<typeof useNodeTypesStore>);
|
||||
});
|
||||
|
||||
it('should show Free credits badge when latest version meets the minimum', () => {
|
||||
const [result] = finalizeItems([makeGatewayNode()]) as NodeCreateElement[];
|
||||
expect(result.properties.tag).toEqual({ text: expect.any(String), pill: true });
|
||||
});
|
||||
|
||||
it('should suppress Free credits badge when latest version is below the minimum', () => {
|
||||
vi.mocked(useAiGatewayStore).mockReturnValue({
|
||||
isNodeSupported: vi.fn(() => true),
|
||||
isNodeTypeVersionSupported: vi.fn(() => false),
|
||||
} as unknown as ReturnType<typeof useAiGatewayStore>);
|
||||
|
||||
const [result] = finalizeItems([makeGatewayNode()]) as NodeCreateElement[];
|
||||
expect(result.properties.tag).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should suppress Free credits badge when gateway is disabled', () => {
|
||||
vi.mocked(useSettingsStore).mockReturnValue({
|
||||
isAiGatewayEnabled: false,
|
||||
} as unknown as ReturnType<typeof useSettingsStore>);
|
||||
|
||||
const [result] = finalizeItems([makeGatewayNode()]) as NodeCreateElement[];
|
||||
expect(result.properties.tag).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should pass the latest (max) version to the version check', () => {
|
||||
const isNodeTypeVersionSupported = vi.fn(() => true);
|
||||
vi.mocked(useNodeTypesStore).mockReturnValue({
|
||||
getNodeVersions: vi.fn(() => [1, 1.1, 2]),
|
||||
} as unknown as ReturnType<typeof useNodeTypesStore>);
|
||||
vi.mocked(useAiGatewayStore).mockReturnValue({
|
||||
isNodeSupported: vi.fn(() => true),
|
||||
isNodeTypeVersionSupported,
|
||||
} as unknown as ReturnType<typeof useAiGatewayStore>);
|
||||
|
||||
finalizeItems([makeGatewayNode('my-node')]);
|
||||
expect(isNodeTypeVersionSupported).toHaveBeenCalledWith('my-node', 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapToolSubcategoryIcon', () => {
|
||||
it('should return "globe" for AI_CATEGORY_OTHER_TOOLS', () => {
|
||||
expect(mapToolSubcategoryIcon(AI_CATEGORY_OTHER_TOOLS)).toBe('globe');
|
||||
|
||||
@@ -315,10 +315,14 @@ function applyNodeTags(element: INodeCreateElement): INodeCreateElement {
|
||||
useSettingsStore().isAiGatewayEnabled &&
|
||||
useAiGatewayStore().isNodeSupported(element.properties.name)
|
||||
) {
|
||||
element.properties.tag = {
|
||||
text: i18n.baseText('generic.freeCredits'),
|
||||
pill: true,
|
||||
};
|
||||
const versions = useNodeTypesStore().getNodeVersions(element.properties.name);
|
||||
const latestVersion = versions.length > 0 ? Math.max(...versions) : 1;
|
||||
if (useAiGatewayStore().isNodeTypeVersionSupported(element.properties.name, latestVersion)) {
|
||||
element.properties.tag = {
|
||||
text: i18n.baseText('generic.freeCredits'),
|
||||
pill: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return element;
|
||||
|
||||
Reference in New Issue
Block a user