From 73748e300ed4a7f57b9c01f72eba2fbaa24ceef9 Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Fri, 4 Apr 2025 09:22:13 +0200 Subject: [PATCH] feat(core): Add special `@tool` displayOption (#14318) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ --- packages/cli/src/credentials-helper.ts | 1 + .../node-execution-context.ts | 1 + .../utils/extract-value.ts | 8 +- .../utils/validate-value-against-schema.ts | 3 +- .../core/src/execution-engine/routing-node.ts | 12 +- .../src/execution-engine/workflow-execute.ts | 1 + .../CredentialEdit/CredentialEdit.vue | 1 + .../editor-ui/src/components/NodeSettings.vue | 6 +- .../src/components/ParameterInput.test.ts | 1 + .../src/components/ParameterInput.vue | 1 + .../src/components/ParameterInputList.vue | 8 +- .../ResourceMapper/ResourceMapper.vue | 1 + .../src/composables/useCanvasOperations.ts | 1 + .../src/composables/useNodeHelpers.ts | 15 +- .../src/composables/useWorkflowHelpers.ts | 1 + .../editor-ui/src/stores/workflows.store.ts | 1 + .../editor-ui/src/utils/nodeViewUtils.ts | 3 +- .../src/utils/nodes/nodeTransforms.ts | 12 +- packages/workflow/src/Interfaces.ts | 1 + packages/workflow/src/NodeHelpers.ts | 141 ++++-- packages/workflow/src/TelemetryHelpers.ts | 2 + packages/workflow/src/Workflow.ts | 1 + .../test/NodeHelpers.conditions.test.ts | 4 + packages/workflow/test/NodeHelpers.test.ts | 411 +++++++++++++++++- 24 files changed, 589 insertions(+), 48 deletions(-) diff --git a/packages/cli/src/credentials-helper.ts b/packages/cli/src/credentials-helper.ts index a4aaaae25a..4feff3956b 100644 --- a/packages/cli/src/credentials-helper.ts +++ b/packages/cli/src/credentials-helper.ts @@ -371,6 +371,7 @@ export class CredentialsHelper extends ICredentialsHelper { true, false, null, + null, ) as ICredentialDataDecryptedObject; if (decryptedDataOriginal.oauthTokenData !== undefined) { diff --git a/packages/core/src/execution-engine/node-execution-context/node-execution-context.ts b/packages/core/src/execution-engine/node-execution-context/node-execution-context.ts index 662e24d6b6..ae431c517e 100644 --- a/packages/core/src/execution-engine/node-execution-context/node-execution-context.ts +++ b/packages/core/src/execution-engine/node-execution-context/node-execution-context.ts @@ -219,6 +219,7 @@ export abstract class NodeExecutionContext implements Omit i.name === name && - NodeHelpers.displayParameterPath(nodeParameters, i, currentParamPath, node), + NodeHelpers.displayParameterPath( + nodeParameters, + i, + currentParamPath, + node, + nodeType.description, + ), ); }; diff --git a/packages/core/src/execution-engine/node-execution-context/utils/validate-value-against-schema.ts b/packages/core/src/execution-engine/node-execution-context/utils/validate-value-against-schema.ts index 73b5423f49..c683f2ddd2 100644 --- a/packages/core/src/execution-engine/node-execution-context/utils/validate-value-against-schema.ts +++ b/packages/core/src/execution-engine/node-execution-context/utils/validate-value-against-schema.ts @@ -160,7 +160,8 @@ export const validateValueAgainstSchema = ( const propertyDescription = nodeType.description.properties.find( (prop) => - parameterPath[0] === prop.name && NodeHelpers.displayParameter(node.parameters, prop, node), + parameterPath[0] === prop.name && + NodeHelpers.displayParameter(node.parameters, prop, node, nodeType.description), ); if (!propertyDescription) { diff --git a/packages/core/src/execution-engine/routing-node.ts b/packages/core/src/execution-engine/routing-node.ts index 22944ff5e6..83b2bfd862 100644 --- a/packages/core/src/execution-engine/routing-node.ts +++ b/packages/core/src/execution-engine/routing-node.ts @@ -829,8 +829,16 @@ export class RoutingNode { }; let basePath = path ? `${path}.` : ''; - const { node } = this.context; - if (!NodeHelpers.displayParameter(node.parameters, nodeProperties, node, node.parameters)) { + const { node, nodeType } = this.context; + if ( + !NodeHelpers.displayParameter( + node.parameters, + nodeProperties, + node, + nodeType.description, + node.parameters, + ) + ) { return undefined; } if (nodeProperties.routing) { diff --git a/packages/core/src/execution-engine/workflow-execute.ts b/packages/core/src/execution-engine/workflow-execute.ts index 244eb37ccf..1b371da141 100644 --- a/packages/core/src/execution-engine/workflow-execute.ts +++ b/packages/core/src/execution-engine/workflow-execute.ts @@ -999,6 +999,7 @@ export class WorkflowExecute { nodeIssues = NodeHelpers.getNodeParametersIssues( nodeType.description.properties, node, + nodeType.description, inputData.pinDataNodeNames, ); } diff --git a/packages/frontend/editor-ui/src/components/CredentialEdit/CredentialEdit.vue b/packages/frontend/editor-ui/src/components/CredentialEdit/CredentialEdit.vue index 5be36dfba9..eff526f020 100644 --- a/packages/frontend/editor-ui/src/components/CredentialEdit/CredentialEdit.vue +++ b/packages/frontend/editor-ui/src/components/CredentialEdit/CredentialEdit.vue @@ -663,6 +663,7 @@ async function saveCredential(): Promise { false, false, null, + null, ); assert(credentialTypeName.value); diff --git a/packages/frontend/editor-ui/src/components/NodeSettings.vue b/packages/frontend/editor-ui/src/components/NodeSettings.vue index f154d03618..c90594b54a 100644 --- a/packages/frontend/editor-ui/src/components/NodeSettings.vue +++ b/packages/frontend/editor-ui/src/components/NodeSettings.vue @@ -337,7 +337,7 @@ const removeMismatchedOptionValues = ( ); } - if (!hasValidOptions && displayParameter(nodeParameterValues, prop, node.value)) { + if (!hasValidOptions && displayParameter(nodeParameterValues, prop, node.value, nodeType)) { unset(nodeParameterValues as object, prop.name); } }); @@ -395,6 +395,7 @@ const valueChanged = (parameterData: IUpdateInformation) => { false, false, _node, + nodeType, ); const oldNodeParameters = Object.assign({}, nodeParameters); @@ -453,6 +454,7 @@ const valueChanged = (parameterData: IUpdateInformation) => { true, false, _node, + nodeType, ); for (const key of Object.keys(nodeParameters as object)) { @@ -487,6 +489,7 @@ const valueChanged = (parameterData: IUpdateInformation) => { false, false, _node, + nodeType, ); const oldNodeParameters = Object.assign({}, nodeParameters); @@ -535,6 +538,7 @@ const valueChanged = (parameterData: IUpdateInformation) => { true, false, _node, + nodeType, ); for (const key of Object.keys(nodeParameters as object)) { diff --git a/packages/frontend/editor-ui/src/components/ParameterInput.test.ts b/packages/frontend/editor-ui/src/components/ParameterInput.test.ts index 38dda6e214..64099a722a 100644 --- a/packages/frontend/editor-ui/src/components/ParameterInput.test.ts +++ b/packages/frontend/editor-ui/src/components/ParameterInput.test.ts @@ -81,6 +81,7 @@ describe('ParameterInput.vue', () => { }; mockNodeTypesState = { allNodeTypes: [], + getNodeType: vi.fn().mockReturnValue(null), }; createAppModals(); }); diff --git a/packages/frontend/editor-ui/src/components/ParameterInput.vue b/packages/frontend/editor-ui/src/components/ParameterInput.vue index 1066fc19d5..3c28c9acff 100644 --- a/packages/frontend/editor-ui/src/components/ParameterInput.vue +++ b/packages/frontend/editor-ui/src/components/ParameterInput.vue @@ -346,6 +346,7 @@ const getIssues = computed(() => { node.value.parameters, newPath.join('.'), node.value, + nodeTypesStore.getNodeType(node.value.type, node.value.typeVersion), ); if (props.parameter.type === 'credentialsSelect' && displayValue.value === '') { diff --git a/packages/frontend/editor-ui/src/components/ParameterInputList.vue b/packages/frontend/editor-ui/src/components/ParameterInputList.vue index 8c625b348d..2ed432fe26 100644 --- a/packages/frontend/editor-ui/src/components/ParameterInputList.vue +++ b/packages/frontend/editor-ui/src/components/ParameterInputList.vue @@ -466,7 +466,13 @@ function getParameterIssues(parameter: INodeProperties): string[] { if (!node.value || !showIssuesInLabelFor.includes(parameter.type)) { return []; } - const issues = NodeHelpers.getParameterIssues(parameter, node.value.parameters, '', node.value); + const issues = NodeHelpers.getParameterIssues( + parameter, + node.value.parameters, + '', + node.value, + nodeType.value, + ); return issues.parameters?.[parameter.name] ?? []; } diff --git a/packages/frontend/editor-ui/src/components/ResourceMapper/ResourceMapper.vue b/packages/frontend/editor-ui/src/components/ResourceMapper/ResourceMapper.vue index dc98bf0c19..e4f4caed3f 100644 --- a/packages/frontend/editor-ui/src/components/ResourceMapper/ResourceMapper.vue +++ b/packages/frontend/editor-ui/src/components/ResourceMapper/ResourceMapper.vue @@ -407,6 +407,7 @@ function updateNodeIssues(): void { const parameterIssues = NodeHelpers.getNodeParametersIssues( nodeType.value?.properties ?? [], props.node, + nodeType.value, ); if (parameterIssues) { ndvStore.updateNodeParameterIssues(parameterIssues); diff --git a/packages/frontend/editor-ui/src/composables/useCanvasOperations.ts b/packages/frontend/editor-ui/src/composables/useCanvasOperations.ts index 94a9ff1510..5b97311d4a 100644 --- a/packages/frontend/editor-ui/src/composables/useCanvasOperations.ts +++ b/packages/frontend/editor-ui/src/composables/useCanvasOperations.ts @@ -874,6 +874,7 @@ export function useCanvasOperations({ router }: { router: ReturnType { true, false, latestNode, + nodeType, ); if (latestNode) { diff --git a/packages/frontend/editor-ui/src/utils/nodeViewUtils.ts b/packages/frontend/editor-ui/src/utils/nodeViewUtils.ts index 0f31ff93d8..61d402ce8b 100644 --- a/packages/frontend/editor-ui/src/utils/nodeViewUtils.ts +++ b/packages/frontend/editor-ui/src/utils/nodeViewUtils.ts @@ -338,8 +338,7 @@ export function getGenericHints({ true, false, node, - undefined, - false, + nodeType, ); const assignments = diff --git a/packages/frontend/editor-ui/src/utils/nodes/nodeTransforms.ts b/packages/frontend/editor-ui/src/utils/nodes/nodeTransforms.ts index 664895ba8c..a49dc8e652 100644 --- a/packages/frontend/editor-ui/src/utils/nodes/nodeTransforms.ts +++ b/packages/frontend/editor-ui/src/utils/nodes/nodeTransforms.ts @@ -22,11 +22,17 @@ export function getNodeTypeDisplayableCredentials( // credentials can have conditional requirements that depend on // node parameters. const nodeParameters = - NodeHelpers.getNodeParameters(nodeType.properties, node.parameters, true, false, node) ?? - node.parameters; + NodeHelpers.getNodeParameters( + nodeType.properties, + node.parameters, + true, + false, + node, + nodeType, + ) ?? node.parameters; const displayableCredentials = nodeTypeCreds.filter((credentialTypeDescription) => { - return NodeHelpers.displayParameter(nodeParameters, credentialTypeDescription, node); + return NodeHelpers.displayParameter(nodeParameters, credentialTypeDescription, node, nodeType); }); return displayableCredentials; diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index cb85460d03..e246c9f48c 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -1376,6 +1376,7 @@ export interface IDisplayOptions { }; show?: { '@version'?: Array; + '@tool'?: [boolean]; [key: string]: Array | undefined; }; diff --git a/packages/workflow/src/NodeHelpers.ts b/packages/workflow/src/NodeHelpers.ts index 7886d9801b..770afd2aa6 100644 --- a/packages/workflow/src/NodeHelpers.ts +++ b/packages/workflow/src/NodeHelpers.ts @@ -392,6 +392,7 @@ const getPropertyValues = ( nodeValues: INodeParameters, propertyName: string, node: Pick | null, + nodeTypeDescription: INodeTypeDescription | null, nodeValuesRoot: INodeParameters, ) => { let value; @@ -400,6 +401,8 @@ const getPropertyValues = ( value = get(nodeValuesRoot, propertyName.slice(1)); } else if (propertyName === '@version') { value = node?.typeVersion || 0; + } else if (propertyName === '@tool') { + value = nodeTypeDescription?.name.endsWith('Tool') ?? false; } else { // Get the value from current level value = get(nodeValues, propertyName); @@ -487,6 +490,7 @@ export function displayParameter( nodeValues: INodeParameters, parameter: INodeProperties | INodeCredentialDescription, node: Pick | null, // Allow null as it does also get used by credentials and they do not have versioning yet + nodeTypeDescription: INodeTypeDescription | null, nodeValuesRoot?: INodeParameters, displayKey: 'displayOptions' | 'disabledOptions' = 'displayOptions', ) { @@ -501,7 +505,13 @@ export function displayParameter( if (show) { // All the defined rules have to match to display parameter for (const propertyName of Object.keys(show)) { - const values = getPropertyValues(nodeValues, propertyName, node, nodeValuesRoot); + const values = getPropertyValues( + nodeValues, + propertyName, + node, + nodeTypeDescription, + nodeValuesRoot, + ); if (values.some((v) => typeof v === 'string' && v.charAt(0) === '=')) { return true; @@ -516,7 +526,13 @@ export function displayParameter( if (hide) { // Any of the defined hide rules have to match to hide the parameter for (const propertyName of Object.keys(hide)) { - const values = getPropertyValues(nodeValues, propertyName, node, nodeValuesRoot); + const values = getPropertyValues( + nodeValues, + propertyName, + node, + nodeTypeDescription, + nodeValuesRoot, + ); if (values.length !== 0 && checkConditions(hide[propertyName]!, values)) { return false; @@ -541,6 +557,7 @@ export function displayParameterPath( parameter: INodeProperties | INodeCredentialDescription, path: string, node: Pick | null, + nodeTypeDescription: INodeTypeDescription | null, displayKey: 'displayOptions' | 'disabledOptions' = 'displayOptions', ) { let resolvedNodeValues = nodeValues; @@ -554,7 +571,14 @@ export function displayParameterPath( nodeValuesRoot = get(nodeValues, 'parameters') as INodeParameters; } - return displayParameter(resolvedNodeValues, parameter, node, nodeValuesRoot, displayKey); + return displayParameter( + resolvedNodeValues, + parameter, + node, + nodeTypeDescription, + nodeValuesRoot, + displayKey, + ); } /** @@ -698,6 +722,14 @@ export function getParameterResolveOrder( return executionOrder; } +export type GetNodeParametersOptions = { + onlySimpleTypes?: boolean; + dataIsResolved?: boolean; // If nodeValues are already fully resolved (so that all default values got added already) + nodeValuesRoot?: INodeParameters; + parentType?: string; + parameterDependencies?: IParameterDependencies; +}; + /** * Returns the node parameter values. Depending on the settings it either just returns the none * default values or it applies all the default values. @@ -706,9 +738,7 @@ export function getParameterResolveOrder( * @param {INodeParameters} nodeValues The node parameter data * @param {boolean} returnDefaults If default values get added or only none default values returned * @param {boolean} returnNoneDisplayed If also values which should not be displayed should be returned - * @param {boolean} [onlySimpleTypes=false] If only simple types should be resolved - * @param {boolean} [dataIsResolved=false] If nodeValues are already fully resolved (so that all default values got added already) - * @param {INodeParameters} [nodeValuesRoot] The root node-parameter-data + * @param {GetNodeParametersOptions} options Optional properties */ // eslint-disable-next-line complexity export function getNodeParameters( @@ -717,12 +747,11 @@ export function getNodeParameters( returnDefaults: boolean, returnNoneDisplayed: boolean, node: Pick | null, - onlySimpleTypes = false, - dataIsResolved = false, - nodeValuesRoot?: INodeParameters, - parentType?: string, - parameterDependencies?: IParameterDependencies, + nodeTypeDescription: INodeTypeDescription | null, + options?: GetNodeParametersOptions, ): INodeParameters | null { + let { nodeValuesRoot, parameterDependencies } = options ?? {}; + const { onlySimpleTypes = false, dataIsResolved = false, parentType } = options ?? {}; if (parameterDependencies === undefined) { parameterDependencies = getParameterDependencies(nodePropertiesArray); } @@ -752,11 +781,14 @@ export function getNodeParameters( true, true, node, - true, - true, - nodeValuesRoot, - parentType, - parameterDependencies, + nodeTypeDescription, + { + onlySimpleTypes: true, + dataIsResolved: true, + nodeValuesRoot, + parentType, + parameterDependencies, + }, ) as INodeParameters; } @@ -781,7 +813,13 @@ export function getNodeParameters( if ( !returnNoneDisplayed && - !displayParameter(nodeValuesDisplayCheck, nodeProperties, node, nodeValuesRoot) + !displayParameter( + nodeValuesDisplayCheck, + nodeProperties, + node, + nodeTypeDescription, + nodeValuesRoot, + ) ) { if (!returnNoneDisplayed || !returnDefaults) { continue; @@ -792,7 +830,15 @@ export function getNodeParameters( // Is a simple property so can be set as it is if (duplicateParameterNames.includes(nodeProperties.name)) { - if (!displayParameter(nodeValuesDisplayCheck, nodeProperties, node, nodeValuesRoot)) { + if ( + !displayParameter( + nodeValuesDisplayCheck, + nodeProperties, + node, + nodeTypeDescription, + nodeValuesRoot, + ) + ) { continue; } } @@ -871,10 +917,13 @@ export function getNodeParameters( returnDefaults, returnNoneDisplayed, node, - false, - false, - nodeValuesRoot, - nodeProperties.type, + nodeTypeDescription, + { + onlySimpleTypes: false, + dataIsResolved: false, + nodeValuesRoot, + parentType: nodeProperties.type, + }, ); if (tempNodeParameters !== null) { @@ -944,10 +993,13 @@ export function getNodeParameters( returnDefaults, returnNoneDisplayed, node, - false, - false, - nodeValuesRoot, - nodeProperties.type, + nodeTypeDescription, + { + onlySimpleTypes: false, + dataIsResolved: false, + nodeValuesRoot, + parentType: nodeProperties.type, + }, ); if (tempValue !== null) { tempArrayValue.push(tempValue); @@ -972,10 +1024,13 @@ export function getNodeParameters( returnDefaults, returnNoneDisplayed, node, - false, - false, - nodeValuesRoot, - nodeProperties.type, + nodeTypeDescription, + { + onlySimpleTypes: false, + dataIsResolved: false, + nodeValuesRoot, + parentType: nodeProperties.type, + }, ); if (tempValue !== null) { Object.assign(tempNodeParameters, tempValue); @@ -1236,6 +1291,7 @@ export function getNodeOutputs( export function getNodeParametersIssues( nodePropertiesArray: INodeProperties[], node: INode, + nodeTypeDescription: INodeTypeDescription | null, pinDataNodeNames?: string[], ): INodeIssues | null { const foundIssues: INodeIssues = {}; @@ -1247,7 +1303,13 @@ export function getNodeParametersIssues( } for (const nodeProperty of nodePropertiesArray) { - propertyIssues = getParameterIssues(nodeProperty, node.parameters, '', node); + propertyIssues = getParameterIssues( + nodeProperty, + node.parameters, + '', + node, + nodeTypeDescription, + ); mergeIssues(foundIssues, propertyIssues); } @@ -1456,9 +1518,16 @@ export function getParameterIssues( nodeValues: INodeParameters, path: string, node: INode, + nodeTypeDescription: INodeTypeDescription | null, ): INodeIssues { const foundIssues: INodeIssues = {}; - const isDisplayed = displayParameterPath(nodeValues, nodeProperties, path, node); + const isDisplayed = displayParameterPath( + nodeValues, + nodeProperties, + path, + node, + nodeTypeDescription, + ); if (nodeProperties.required === true) { if (isDisplayed) { const value = getParameterValueByPath(nodeValues, nodeProperties.name, path); @@ -1636,7 +1705,13 @@ export function getParameterIssues( let propertyIssues; for (const optionData of checkChildNodeProperties) { - propertyIssues = getParameterIssues(optionData.data, nodeValues, optionData.basePath, node); + propertyIssues = getParameterIssues( + optionData.data, + nodeValues, + optionData.basePath, + node, + nodeTypeDescription, + ); mergeIssues(foundIssues, propertyIssues); } diff --git a/packages/workflow/src/TelemetryHelpers.ts b/packages/workflow/src/TelemetryHelpers.ts index 672ea70c9b..5ec289c960 100644 --- a/packages/workflow/src/TelemetryHelpers.ts +++ b/packages/workflow/src/TelemetryHelpers.ts @@ -198,6 +198,7 @@ export function generateNodesGraph( true, false, stickyNote, + stickyType.description, ) ?? {}; } catch { // prevent node param resolution from failing graph generation @@ -376,6 +377,7 @@ export function generateNodesGraph( true, false, node, + nodeType.description, ); if (nodeParameters) { diff --git a/packages/workflow/src/Workflow.ts b/packages/workflow/src/Workflow.ts index a68b89c860..4b711e1fb2 100644 --- a/packages/workflow/src/Workflow.ts +++ b/packages/workflow/src/Workflow.ts @@ -112,6 +112,7 @@ export class Workflow { true, false, node, + nodeType.description, ); node.parameters = nodeParameters !== null ? nodeParameters : {}; } diff --git a/packages/workflow/test/NodeHelpers.conditions.test.ts b/packages/workflow/test/NodeHelpers.conditions.test.ts index 05355d4a2e..c7625a9153 100644 --- a/packages/workflow/test/NodeHelpers.conditions.test.ts +++ b/packages/workflow/test/NodeHelpers.conditions.test.ts @@ -2166,6 +2166,7 @@ describe('NodeHelpers', () => { false, false, null, + null, ); expect(result).toEqual(testData.output.noneDisplayedFalse.defaultsFalse); @@ -2176,6 +2177,7 @@ describe('NodeHelpers', () => { true, false, null, + null, ); expect(result).toEqual(testData.output.noneDisplayedFalse.defaultsTrue); @@ -2186,6 +2188,7 @@ describe('NodeHelpers', () => { false, true, null, + null, ); expect(result).toEqual(testData.output.noneDisplayedTrue.defaultsFalse); @@ -2196,6 +2199,7 @@ describe('NodeHelpers', () => { true, true, null, + null, ); expect(result).toEqual(testData.output.noneDisplayedTrue.defaultsTrue); }); diff --git a/packages/workflow/test/NodeHelpers.test.ts b/packages/workflow/test/NodeHelpers.test.ts index 128a7c3312..9716f5b1fc 100644 --- a/packages/workflow/test/NodeHelpers.test.ts +++ b/packages/workflow/test/NodeHelpers.test.ts @@ -16,6 +16,7 @@ import { getParameterIssues, isTriggerNode, isExecutable, + displayParameter, } from '@/NodeHelpers'; import type { Workflow } from '@/Workflow'; @@ -3420,6 +3421,7 @@ describe('NodeHelpers', () => { false, false, null, + null, ); expect(result).toEqual(testData.output.noneDisplayedFalse.defaultsFalse); @@ -3430,6 +3432,7 @@ describe('NodeHelpers', () => { true, false, null, + null, ); expect(result).toEqual(testData.output.noneDisplayedFalse.defaultsTrue); @@ -3440,6 +3443,7 @@ describe('NodeHelpers', () => { false, true, null, + null, ); expect(result).toEqual(testData.output.noneDisplayedTrue.defaultsFalse); @@ -3450,6 +3454,7 @@ describe('NodeHelpers', () => { true, true, null, + null, ); expect(result).toEqual(testData.output.noneDisplayedTrue.defaultsTrue); }); @@ -4193,6 +4198,7 @@ describe('NodeHelpers', () => { testData.input.nodeValues, testData.input.path, testData.input.node, + null, ); expect(result).toEqual(testData.output); }); @@ -4209,6 +4215,18 @@ describe('NodeHelpers', () => { parameters: {}, }; + const testNodeType: INodeTypeDescription = { + name: 'Test Node', + version: 0, + defaults: {}, + inputs: [], + outputs: [], + properties: [], + displayName: '', + group: [], + description: '', + }; + it('Should validate required dateTime parameters if empty string', () => { const nodeProperties: INodeProperties = { displayName: 'Date Time', @@ -4221,7 +4239,7 @@ describe('NodeHelpers', () => { testDateTime: '', }; - const result = getParameterIssues(nodeProperties, nodeValues, '', testNode); + const result = getParameterIssues(nodeProperties, nodeValues, '', testNode, null); expect(result).toEqual({ parameters: { @@ -4242,7 +4260,7 @@ describe('NodeHelpers', () => { testDateTime: undefined, }; - const result = getParameterIssues(nodeProperties, nodeValues, '', testNode); + const result = getParameterIssues(nodeProperties, nodeValues, '', testNode, testNodeType); expect(result).toEqual({ parameters: { @@ -4513,4 +4531,393 @@ describe('NodeHelpers', () => { }); } }); + describe('displayParameter', () => { + const testNode: INode = { + id: '12345', + name: 'Test Node', + typeVersion: 1, + type: 'n8n-nodes-base.testNode', + position: [1, 1], + parameters: {}, + }; + + const testNodeType: INodeTypeDescription = { + name: 'Test Node', + version: 0, + defaults: {}, + inputs: [], + outputs: [], + properties: [], + displayName: '', + group: [], + description: '', + }; + + const defaultTestInput = { + nodeValues: {}, + parameter: { + displayName: 'Test Parameter', + name: 'testParameter', + type: 'string', + default: '', + } as INodeProperties, + node: testNode, + nodeTypeDescription: testNodeType, + nodeValuesRoot: undefined as undefined | INodeParameters, + displayKey: 'displayOptions' as 'displayOptions' | 'disabledOptions', + }; + + const tests: Array<[string, typeof defaultTestInput, boolean]> = [ + ['Should return true if no displayOptions are defined', { ...defaultTestInput }, true], + [ + 'Should return true if displayOptions.show conditions are met', + { + ...defaultTestInput, + nodeValues: { condition: 'value1' }, + parameter: { + ...defaultTestInput.parameter, + displayOptions: { + show: { + condition: ['value1'], + }, + }, + }, + }, + true, + ], + [ + 'Should return false if displayOptions.show conditions are not met', + { + ...defaultTestInput, + nodeValues: { condition: 'value2' }, + parameter: { + ...defaultTestInput.parameter, + displayOptions: { + show: { + condition: ['value1'], + }, + }, + }, + }, + false, + ], + [ + 'Should return false if displayOptions.hide conditions are met', + { + ...defaultTestInput, + nodeValues: { condition: 'value1' }, + parameter: { + ...defaultTestInput.parameter, + displayOptions: { + hide: { + condition: ['value1'], + }, + }, + }, + }, + false, + ], + [ + 'Should return true if displayOptions.hide conditions are not met', + { + ...defaultTestInput, + nodeValues: { condition: 'value2' }, + parameter: { + ...defaultTestInput.parameter, + displayOptions: { + hide: { + condition: ['value1'], + }, + }, + }, + }, + true, + ], + [ + 'Should return true if displayOptions.show and hide conditions are both met', + { + ...defaultTestInput, + nodeValues: { condition: 'value1' }, + parameter: { + ...defaultTestInput.parameter, + displayOptions: { + show: { + condition: ['value1'], + }, + hide: { + condition: ['value1'], + }, + }, + }, + }, + false, // Hide takes precedence over show + ], + [ + 'Should return true if displayOptions.show conditions are met with multiple values', + { + ...defaultTestInput, + nodeValues: { condition: 'value2' }, + parameter: { + ...defaultTestInput.parameter, + displayOptions: { + show: { + condition: ['value1', 'value2'], + }, + }, + }, + }, + true, + ], + [ + 'Should return false if displayOptions.hide conditions are met with multiple values', + { + ...defaultTestInput, + nodeValues: { condition: 'value2' }, + parameter: { + ...defaultTestInput.parameter, + displayOptions: { + hide: { + condition: ['value1', 'value2'], + }, + }, + }, + }, + false, + ], + [ + 'Should return true if @tool is true in nodeTypeDescription of tool', + { + ...defaultTestInput, + nodeTypeDescription: { + ...testNodeType, + name: testNodeType.name + 'Tool', + }, + parameter: { + ...defaultTestInput.parameter, + displayOptions: { + show: { + '@tool': [true], + }, + }, + }, + }, + true, + ], + [ + 'Should return false if @tool is true in nodeTypeDescription of non-tool', + { + ...defaultTestInput, + parameter: { + ...defaultTestInput.parameter, + displayOptions: { + show: { + '@tool': [true], + }, + }, + }, + }, + false, + ], + [ + 'Should return true if @version condition is met', + { + ...defaultTestInput, + node: { + ...testNode, + typeVersion: 2, + }, + parameter: { + ...defaultTestInput.parameter, + displayOptions: { + show: { + '@version': [ + { + _cnd: { + gte: 2, + }, + }, + ], + }, + }, + }, + }, + true, + ], + [ + 'Should return false if @version condition is not met', + { + ...defaultTestInput, + node: { + ...testNode, + typeVersion: 1, + }, + parameter: { + ...defaultTestInput.parameter, + displayOptions: { + show: { + '@version': [ + { + _cnd: { + gte: 2, + }, + }, + ], + }, + }, + }, + }, + false, + ], + [ + 'Should return true if @tool and @version conditions are both met', + { + ...defaultTestInput, + node: { + ...testNode, + typeVersion: 2, + }, + nodeTypeDescription: { + ...testNodeType, + name: testNodeType.name + 'Tool', + }, + parameter: { + ...defaultTestInput.parameter, + displayOptions: { + show: { + '@tool': [true], + '@version': [ + { + _cnd: { + gte: 2, + }, + }, + ], + }, + }, + }, + }, + true, + ], + [ + 'Should return false if @tool is true but @version condition is not met', + { + ...defaultTestInput, + node: { + ...testNode, + typeVersion: 1, + }, + nodeTypeDescription: { + ...testNodeType, + name: testNodeType.name + 'Tool', + }, + parameter: { + ...defaultTestInput.parameter, + displayOptions: { + show: { + '@tool': [true], + '@version': [ + { + _cnd: { + gte: 2, + }, + }, + ], + }, + }, + }, + }, + false, + ], + [ + 'Should return true if no disabledOptions are defined', + { + ...defaultTestInput, + displayKey: 'disabledOptions', + }, + true, + ], + [ + 'Should return false if disabledOptions.hide conditions are met', + { + ...defaultTestInput, + nodeValues: { condition: 'value1' }, + parameter: { + ...defaultTestInput.parameter, + disabledOptions: { + hide: { + condition: ['value1'], + }, + }, + }, + displayKey: 'disabledOptions', + }, + false, + ], + [ + 'Should return true if disabledOptions.hide conditions are not met', + { + ...defaultTestInput, + nodeValues: { condition: 'value2' }, + parameter: { + ...defaultTestInput.parameter, + disabledOptions: { + hide: { + condition: ['value1'], + }, + }, + }, + displayKey: 'disabledOptions', + }, + true, + ], + [ + 'Should return true if nodeValuesRoot contains a matching value for displayOptions.show', + { + ...defaultTestInput, + nodeValues: {}, + nodeValuesRoot: { condition: 'value1' }, + parameter: { + ...defaultTestInput.parameter, + displayOptions: { + show: { + '/condition': ['value1'], + }, + }, + }, + }, + true, + ], + [ + 'Should return false if nodeValuesRoot does not contain a matching value for displayOptions.show', + { + ...defaultTestInput, + nodeValues: { condition: 'value1' }, + nodeValuesRoot: { anotherKey: 'value1' }, + parameter: { + ...defaultTestInput.parameter, + displayOptions: { + show: { + '/condition': ['value1'], + }, + }, + }, + }, + false, + ], + ]; + + for (const [description, input, expected] of tests) { + test(description, () => { + const result = displayParameter( + input.nodeValues, + input.parameter, + input.node, + input.nodeTypeDescription, + input.nodeValuesRoot, + input.displayKey, + ); + expect(result).toEqual(expected); + }); + } + }); });