mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
fix(editor): Use display option's @Version specifier (#7351)
Nodes can have properties that have a displayOption which specifies a version for which node versions that property applies to. We should take this into account when forming the action types for a Node in the NodeList. For example Notion node has 2 version which have different Page operations.
This commit is contained in:
parent
aa1bf95136
commit
afbf0c3d5e
|
@ -0,0 +1,390 @@
|
|||
import type { INodeProperties, INodeTypeDescription } from 'n8n-workflow';
|
||||
import { useActionsGenerator } from '../composables/useActionsGeneration';
|
||||
|
||||
describe('useActionsGenerator', () => {
|
||||
const { generateMergedNodesAndActions } = useActionsGenerator();
|
||||
const NODE_NAME = 'n8n-nodes-base.test';
|
||||
const baseV2NodeWoProps: INodeTypeDescription = {
|
||||
name: NODE_NAME,
|
||||
displayName: 'Test',
|
||||
description: 'Test Node',
|
||||
defaultVersion: 2,
|
||||
version: 2,
|
||||
group: ['output'],
|
||||
defaults: {
|
||||
name: 'Test',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [],
|
||||
};
|
||||
|
||||
describe('App actions for resource category', () => {
|
||||
const resourcePropertyWithUser: INodeProperties = {
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'User',
|
||||
value: 'user',
|
||||
},
|
||||
],
|
||||
default: 'user',
|
||||
};
|
||||
const resourcePropertyWithUserAndPage: INodeProperties = {
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'User',
|
||||
value: 'user',
|
||||
},
|
||||
{
|
||||
name: 'Page',
|
||||
value: 'page',
|
||||
},
|
||||
],
|
||||
default: 'user',
|
||||
};
|
||||
|
||||
it('returns single action for single resource & single operation without resource filter', () => {
|
||||
const node: INodeTypeDescription = {
|
||||
...baseV2NodeWoProps,
|
||||
properties: [
|
||||
resourcePropertyWithUser,
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {},
|
||||
options: [
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get description',
|
||||
},
|
||||
],
|
||||
default: 'get',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { actions } = generateMergedNodesAndActions([node]);
|
||||
expect(actions).toEqual({
|
||||
[NODE_NAME]: [
|
||||
expect.objectContaining({
|
||||
actionKey: 'get',
|
||||
description: 'Get description',
|
||||
displayName: 'User Get',
|
||||
codex: {
|
||||
label: 'User Actions',
|
||||
categories: ['Actions'],
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns single action for single resource & single operation with matching resource filter', () => {
|
||||
const node: INodeTypeDescription = {
|
||||
...baseV2NodeWoProps,
|
||||
properties: [
|
||||
resourcePropertyWithUser,
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['user'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get description',
|
||||
},
|
||||
],
|
||||
default: 'get',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { actions } = generateMergedNodesAndActions([node]);
|
||||
expect(actions).toEqual({
|
||||
[NODE_NAME]: [
|
||||
expect.objectContaining({
|
||||
actionKey: 'get',
|
||||
description: 'Get description',
|
||||
displayName: 'User Get',
|
||||
codex: {
|
||||
label: 'User Actions',
|
||||
categories: ['Actions'],
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns nothing for multiple resources & single operation without resource filter', () => {
|
||||
const node: INodeTypeDescription = {
|
||||
...baseV2NodeWoProps,
|
||||
properties: [
|
||||
resourcePropertyWithUserAndPage,
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {},
|
||||
options: [
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get description',
|
||||
},
|
||||
],
|
||||
default: 'get',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { actions } = generateMergedNodesAndActions([node]);
|
||||
expect(actions).toEqual({
|
||||
[NODE_NAME]: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns single action for multiple resources & single operation with resource filter', () => {
|
||||
const node: INodeTypeDescription = {
|
||||
...baseV2NodeWoProps,
|
||||
properties: [
|
||||
resourcePropertyWithUserAndPage,
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['user'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get description',
|
||||
},
|
||||
],
|
||||
default: 'get',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { actions } = generateMergedNodesAndActions([node]);
|
||||
expect(actions).toEqual({
|
||||
[NODE_NAME]: [
|
||||
expect.objectContaining({
|
||||
actionKey: 'get',
|
||||
description: 'Get description',
|
||||
displayName: 'User Get',
|
||||
codex: {
|
||||
label: 'User Actions',
|
||||
categories: ['Actions'],
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns multiple actions for multiple resources & multiple operations with resource filters', () => {
|
||||
const node: INodeTypeDescription = {
|
||||
...baseV2NodeWoProps,
|
||||
properties: [
|
||||
resourcePropertyWithUserAndPage,
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['user'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get description',
|
||||
},
|
||||
],
|
||||
default: 'get',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['page'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get description',
|
||||
},
|
||||
],
|
||||
default: 'get',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { actions } = generateMergedNodesAndActions([node]);
|
||||
expect(actions).toEqual({
|
||||
[NODE_NAME]: [
|
||||
expect.objectContaining({
|
||||
actionKey: 'get',
|
||||
description: 'Get description',
|
||||
displayName: 'User Get',
|
||||
codex: {
|
||||
label: 'User Actions',
|
||||
categories: ['Actions'],
|
||||
},
|
||||
}),
|
||||
expect.objectContaining({
|
||||
actionKey: 'get',
|
||||
description: 'Get description',
|
||||
displayName: 'Page Get',
|
||||
codex: {
|
||||
label: 'Page Actions',
|
||||
categories: ['Actions'],
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns correct action for single resource & multiple operations with different versions', () => {
|
||||
const node: INodeTypeDescription = {
|
||||
...baseV2NodeWoProps,
|
||||
properties: [
|
||||
resourcePropertyWithUser,
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'@version': [1],
|
||||
resource: ['user'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Get Version 1',
|
||||
value: 'getv1',
|
||||
description: 'Get version 1',
|
||||
},
|
||||
],
|
||||
default: 'getv1',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'@version': [2],
|
||||
resource: ['user'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Get Version 2',
|
||||
value: 'getv2',
|
||||
description: 'Get version 2',
|
||||
},
|
||||
],
|
||||
default: 'getv2',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { actions } = generateMergedNodesAndActions([node]);
|
||||
expect(actions).toEqual({
|
||||
[NODE_NAME]: [
|
||||
expect.objectContaining({
|
||||
actionKey: 'getv2',
|
||||
description: 'Get version 2',
|
||||
displayName: 'User Get Version 2',
|
||||
codex: {
|
||||
label: 'User Actions',
|
||||
categories: ['Actions'],
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns correct action for single resource & single operation with multiple versions', () => {
|
||||
const node: INodeTypeDescription = {
|
||||
...baseV2NodeWoProps,
|
||||
properties: [
|
||||
resourcePropertyWithUser,
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'@version': [1, 2],
|
||||
resource: ['user'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Get Version 2',
|
||||
value: 'getv2',
|
||||
description: 'Get version 2',
|
||||
},
|
||||
],
|
||||
default: 'getv2',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { actions } = generateMergedNodesAndActions([node]);
|
||||
expect(actions).toEqual({
|
||||
[NODE_NAME]: [
|
||||
expect.objectContaining({
|
||||
actionKey: 'getv2',
|
||||
description: 'Get version 2',
|
||||
displayName: 'User Get Version 2',
|
||||
codex: {
|
||||
label: 'User Actions',
|
||||
categories: ['Actions'],
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -15,6 +15,7 @@ const PLACEHOLDER_RECOMMENDED_ACTION_KEY = 'placeholder_recommended';
|
|||
function translate(...args: Parameters<typeof i18n.baseText>) {
|
||||
return i18n.baseText(...args);
|
||||
}
|
||||
|
||||
// Memoize the translation function so we don't have to re-translate the same string
|
||||
// multiple times when generating the actions
|
||||
const cachedBaseText = memoize(translate, (...args) => JSON.stringify(args));
|
||||
|
@ -159,12 +160,26 @@ function resourceCategories(nodeTypeDescription: INodeTypeDescription): ActionTy
|
|||
const isSingleResource = options.length === 1;
|
||||
|
||||
// Match operations for the resource by checking if displayOptions matches or contains the resource name
|
||||
const operations = nodeTypeDescription.properties.find(
|
||||
(operation) =>
|
||||
operation.name === 'operation' &&
|
||||
(operation.displayOptions?.show?.resource?.includes(resourceOption.value) ||
|
||||
isSingleResource),
|
||||
);
|
||||
const operations = nodeTypeDescription.properties.find((operation) => {
|
||||
const isOperation = operation.name === 'operation';
|
||||
const isMatchingResource =
|
||||
operation.displayOptions?.show?.resource?.includes(resourceOption.value) ||
|
||||
isSingleResource;
|
||||
|
||||
// If the operation doesn't have a version defined, it should be
|
||||
// available for all versions. Otherwise, make sure the node type
|
||||
// version matches the operation version
|
||||
const operationVersions = operation.displayOptions?.show?.['@version'];
|
||||
const nodeTypeVersions = Array.isArray(nodeTypeDescription.version)
|
||||
? nodeTypeDescription.version
|
||||
: [nodeTypeDescription.version];
|
||||
|
||||
const isMatchingVersion = operationVersions
|
||||
? operationVersions.some((version) => nodeTypeVersions.includes(version))
|
||||
: true;
|
||||
|
||||
return isOperation && isMatchingResource && isMatchingVersion;
|
||||
});
|
||||
|
||||
if (!operations?.options) return;
|
||||
|
||||
|
|
|
@ -1116,6 +1116,8 @@ export interface IDisplayOptions {
|
|||
[key: string]: NodeParameterValue[] | undefined;
|
||||
};
|
||||
show?: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'@version'?: number[];
|
||||
[key: string]: NodeParameterValue[] | undefined;
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue