mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 21:07:28 -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>) {
|
function translate(...args: Parameters<typeof i18n.baseText>) {
|
||||||
return i18n.baseText(...args);
|
return i18n.baseText(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memoize the translation function so we don't have to re-translate the same string
|
// Memoize the translation function so we don't have to re-translate the same string
|
||||||
// multiple times when generating the actions
|
// multiple times when generating the actions
|
||||||
const cachedBaseText = memoize(translate, (...args) => JSON.stringify(args));
|
const cachedBaseText = memoize(translate, (...args) => JSON.stringify(args));
|
||||||
|
@ -159,12 +160,26 @@ function resourceCategories(nodeTypeDescription: INodeTypeDescription): ActionTy
|
||||||
const isSingleResource = options.length === 1;
|
const isSingleResource = options.length === 1;
|
||||||
|
|
||||||
// Match operations for the resource by checking if displayOptions matches or contains the resource name
|
// Match operations for the resource by checking if displayOptions matches or contains the resource name
|
||||||
const operations = nodeTypeDescription.properties.find(
|
const operations = nodeTypeDescription.properties.find((operation) => {
|
||||||
(operation) =>
|
const isOperation = operation.name === 'operation';
|
||||||
operation.name === 'operation' &&
|
const isMatchingResource =
|
||||||
(operation.displayOptions?.show?.resource?.includes(resourceOption.value) ||
|
operation.displayOptions?.show?.resource?.includes(resourceOption.value) ||
|
||||||
isSingleResource),
|
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;
|
if (!operations?.options) return;
|
||||||
|
|
||||||
|
|
|
@ -1116,6 +1116,8 @@ export interface IDisplayOptions {
|
||||||
[key: string]: NodeParameterValue[] | undefined;
|
[key: string]: NodeParameterValue[] | undefined;
|
||||||
};
|
};
|
||||||
show?: {
|
show?: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
'@version'?: number[];
|
||||||
[key: string]: NodeParameterValue[] | undefined;
|
[key: string]: NodeParameterValue[] | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue