mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 12:44:07 -08:00
fix: Load remote resources even if expressions in non requried parameters resolve (#6987)
Github issue / Community forum post (link here to close automatically):
This commit is contained in:
parent
8cd4db0ab7
commit
8a8d4e8bb3
|
@ -1,5 +1,5 @@
|
||||||
import { WorkflowPage, NDV, CredentialsModal } from '../pages';
|
import { WorkflowPage, NDV, CredentialsModal } from '../pages';
|
||||||
import { getVisibleSelect } from '../utils';
|
import { getPopper, getVisiblePopper, getVisibleSelect } from '../utils';
|
||||||
|
|
||||||
const workflowPage = new WorkflowPage();
|
const workflowPage = new WorkflowPage();
|
||||||
const ndv = new NDV();
|
const ndv = new NDV();
|
||||||
|
@ -51,4 +51,19 @@ describe('Resource Locator', () => {
|
||||||
ndv.actions.setRLCValue('documentId', '321');
|
ndv.actions.setRLCValue('documentId', '321');
|
||||||
ndv.getters.resourceLocatorInput('sheetName').should('have.value', '');
|
ndv.getters.resourceLocatorInput('sheetName').should('have.value', '');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// unlike RMC and remote options, RLC does not support loadOptionDependsOn
|
||||||
|
it('should retrieve list options when other params throw errors', () => {
|
||||||
|
workflowPage.actions.addInitialNodeToCanvas('E2e Test', {action: 'Resource Locator'});
|
||||||
|
|
||||||
|
ndv.getters.resourceLocatorInput('rlc').click();
|
||||||
|
getVisiblePopper().should('have.length', 1).findChildByTestId('rlc-item').should('have.length', 5);
|
||||||
|
|
||||||
|
ndv.actions.setInvalidExpression('fieldId');
|
||||||
|
|
||||||
|
ndv.getters.container().click(); // remove focus from input, hide expression preview
|
||||||
|
|
||||||
|
ndv.getters.resourceLocatorInput('rlc').click();
|
||||||
|
getVisiblePopper().should('have.length', 1).findChildByTestId('rlc-item').should('have.length', 5);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
32
cypress/e2e/28-resource-mapper.cy.ts
Normal file
32
cypress/e2e/28-resource-mapper.cy.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { WorkflowPage, NDV } from '../pages';
|
||||||
|
|
||||||
|
const workflowPage = new WorkflowPage();
|
||||||
|
const ndv = new NDV();
|
||||||
|
|
||||||
|
describe('Resource Mapper', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
workflowPage.actions.visit();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not retrieve list options when required params throw errors', () => {
|
||||||
|
workflowPage.actions.addInitialNodeToCanvas('E2e Test', {action: 'Resource Mapping Component'});
|
||||||
|
|
||||||
|
ndv.getters.resourceMapperFieldsContainer().should('be.visible').findChildByTestId('parameter-input').should('have.length', 2);
|
||||||
|
|
||||||
|
ndv.actions.setInvalidExpression('fieldId');
|
||||||
|
|
||||||
|
ndv.actions.refreshResourceMapperColumns();
|
||||||
|
ndv.getters.resourceMapperFieldsContainer().should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve list options when optional params throw errors', () => {
|
||||||
|
workflowPage.actions.addInitialNodeToCanvas('E2e Test', {action: 'Resource Mapping Component'});
|
||||||
|
|
||||||
|
ndv.getters.resourceMapperFieldsContainer().should('be.visible').findChildByTestId('parameter-input').should('have.length', 2);
|
||||||
|
|
||||||
|
ndv.actions.setInvalidExpression('otherField');
|
||||||
|
|
||||||
|
ndv.actions.refreshResourceMapperColumns();
|
||||||
|
ndv.getters.resourceMapperFieldsContainer().should('be.visible').findChildByTestId('parameter-input').should('have.length', 2);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,5 +1,6 @@
|
||||||
import { WorkflowPage, NDV } from '../pages';
|
import { WorkflowPage, NDV } from '../pages';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { getPopper, getVisiblePopper, getVisibleSelect } from '../utils';
|
||||||
|
|
||||||
const workflowPage = new WorkflowPage();
|
const workflowPage = new WorkflowPage();
|
||||||
const ndv = new NDV();
|
const ndv = new NDV();
|
||||||
|
@ -289,6 +290,38 @@ describe('NDV', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not retrieve remote options when required params throw errors', () => {
|
||||||
|
workflowPage.actions.addInitialNodeToCanvas('E2e Test', {action: 'Remote Options'});
|
||||||
|
|
||||||
|
ndv.getters.parameterInput('remoteOptions').click();
|
||||||
|
getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3);
|
||||||
|
|
||||||
|
ndv.actions.setInvalidExpression('fieldId');
|
||||||
|
|
||||||
|
ndv.getters.container().click(); // remove focus from input, hide expression preview
|
||||||
|
|
||||||
|
ndv.getters.parameterInput('remoteOptions').click();
|
||||||
|
getPopper().should('not.be.visible');
|
||||||
|
|
||||||
|
ndv.getters.parameterInputIssues('remoteOptions').realHover();
|
||||||
|
getVisiblePopper().should('include.text', `node doesn't exist`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve remote options when non-required params throw errors', () => {
|
||||||
|
workflowPage.actions.addInitialNodeToCanvas('E2e Test', {action: 'Remote Options'});
|
||||||
|
|
||||||
|
ndv.getters.parameterInput('remoteOptions').click();
|
||||||
|
getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3);
|
||||||
|
ndv.getters.parameterInput('remoteOptions').click();
|
||||||
|
|
||||||
|
ndv.actions.setInvalidExpression('otherField');
|
||||||
|
|
||||||
|
ndv.getters.container().click(); // remove focus from input, hide expression preview
|
||||||
|
|
||||||
|
ndv.getters.parameterInput('remoteOptions').click();
|
||||||
|
getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3);
|
||||||
|
});
|
||||||
|
|
||||||
it('should flag issues as soon as params are set', () => {
|
it('should flag issues as soon as params are set', () => {
|
||||||
workflowPage.actions.addInitialNodeToCanvas('Webhook');
|
workflowPage.actions.addInitialNodeToCanvas('Webhook');
|
||||||
workflowPage.getters.canvasNodes().first().dblclick();
|
workflowPage.getters.canvasNodes().first().dblclick();
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { BasePage } from './base';
|
import { BasePage } from './base';
|
||||||
import { getVisibleSelect } from '../utils';
|
import { getVisiblePopper, getVisibleSelect } from '../utils';
|
||||||
|
|
||||||
export class NDV extends BasePage {
|
export class NDV extends BasePage {
|
||||||
getters = {
|
getters = {
|
||||||
|
@ -39,6 +39,7 @@ export class NDV extends BasePage {
|
||||||
inlineExpressionEditorInput: () => cy.getByTestId('inline-expression-editor-input'),
|
inlineExpressionEditorInput: () => cy.getByTestId('inline-expression-editor-input'),
|
||||||
nodeParameters: () => cy.getByTestId('node-parameters'),
|
nodeParameters: () => cy.getByTestId('node-parameters'),
|
||||||
parameterInput: (parameterName: string) => cy.getByTestId(`parameter-input-${parameterName}`),
|
parameterInput: (parameterName: string) => cy.getByTestId(`parameter-input-${parameterName}`),
|
||||||
|
parameterInputIssues: (parameterName: string) => cy.getByTestId(`parameter-input-${parameterName}`).should('have.length', 1).findChildByTestId('parameter-issues'),
|
||||||
parameterExpressionPreview: (parameterName: string) =>
|
parameterExpressionPreview: (parameterName: string) =>
|
||||||
this.getters
|
this.getters
|
||||||
.nodeParameters()
|
.nodeParameters()
|
||||||
|
@ -64,6 +65,8 @@ export class NDV extends BasePage {
|
||||||
resourceLocatorErrorMessage: () => cy.getByTestId('rlc-error-container'),
|
resourceLocatorErrorMessage: () => cy.getByTestId('rlc-error-container'),
|
||||||
resourceLocatorModeSelector: (paramName: string) =>
|
resourceLocatorModeSelector: (paramName: string) =>
|
||||||
this.getters.resourceLocator(paramName).find('[data-test-id="rlc-mode-selector"]'),
|
this.getters.resourceLocator(paramName).find('[data-test-id="rlc-mode-selector"]'),
|
||||||
|
resourceMapperFieldsContainer: () => cy.getByTestId('mapping-fields-container'),
|
||||||
|
resourceMapperSelectColumn: () => cy.getByTestId('matching-column-select'),
|
||||||
};
|
};
|
||||||
|
|
||||||
actions = {
|
actions = {
|
||||||
|
@ -99,8 +102,8 @@ export class NDV extends BasePage {
|
||||||
clearParameterInput: (parameterName: string) => {
|
clearParameterInput: (parameterName: string) => {
|
||||||
this.getters.parameterInput(parameterName).type(`{selectall}{backspace}`);
|
this.getters.parameterInput(parameterName).type(`{selectall}{backspace}`);
|
||||||
},
|
},
|
||||||
typeIntoParameterInput: (parameterName: string, content: string) => {
|
typeIntoParameterInput: (parameterName: string, content: string, opts?: { parseSpecialCharSequences: boolean }) => {
|
||||||
this.getters.parameterInput(parameterName).type(content);
|
this.getters.parameterInput(parameterName).type(content, opts);
|
||||||
},
|
},
|
||||||
selectOptionInParameterDropdown: (parameterName: string, content: string) => {
|
selectOptionInParameterDropdown: (parameterName: string, content: string) => {
|
||||||
getVisibleSelect().find('.option-headline').contains(content).click();
|
getVisibleSelect().find('.option-headline').contains(content).click();
|
||||||
|
@ -171,6 +174,19 @@ export class NDV extends BasePage {
|
||||||
.find('span')
|
.find('span')
|
||||||
.should('include.html', asEncodedHTML(value));
|
.should('include.html', asEncodedHTML(value));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
refreshResourceMapperColumns: () => {
|
||||||
|
this.getters.resourceMapperSelectColumn().realHover();
|
||||||
|
this.getters.resourceMapperSelectColumn().findChildByTestId('action-toggle').should('have.length', 1).click();
|
||||||
|
|
||||||
|
getVisiblePopper().find('li').last().click();
|
||||||
|
},
|
||||||
|
|
||||||
|
setInvalidExpression: (fieldName: string, invalidExpression?: string) => {
|
||||||
|
this.actions.typeIntoParameterInput(fieldName, "=");
|
||||||
|
this.actions.typeIntoParameterInput(fieldName, invalidExpression ?? "{{ $('unknown')", { parseSpecialCharSequences: false });
|
||||||
|
this.actions.validateExpressionPreview(fieldName, `node doesn't exist`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { META_KEY } from '../constants';
|
import { META_KEY } from '../constants';
|
||||||
import { BasePage } from './base';
|
import { BasePage } from './base';
|
||||||
import { getVisibleSelect } from '../utils';
|
import { getVisibleSelect } from '../utils';
|
||||||
|
import { NodeCreator } from './features/node-creator';
|
||||||
|
|
||||||
|
const nodeCreator = new NodeCreator();
|
||||||
export class WorkflowPage extends BasePage {
|
export class WorkflowPage extends BasePage {
|
||||||
url = '/workflow/new';
|
url = '/workflow/new';
|
||||||
getters = {
|
getters = {
|
||||||
|
@ -130,12 +132,16 @@ export class WorkflowPage extends BasePage {
|
||||||
win.preventNodeViewBeforeUnload = preventNodeViewUnload;
|
win.preventNodeViewBeforeUnload = preventNodeViewUnload;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
addInitialNodeToCanvas: (nodeDisplayName: string, { keepNdvOpen } = { keepNdvOpen: false }) => {
|
addInitialNodeToCanvas: (nodeDisplayName: string, opts?: { keepNdvOpen?: boolean, action?: string }) => {
|
||||||
this.getters.canvasPlusButton().click();
|
this.getters.canvasPlusButton().click();
|
||||||
this.getters.nodeCreatorSearchBar().type(nodeDisplayName);
|
this.getters.nodeCreatorSearchBar().type(nodeDisplayName);
|
||||||
this.getters.nodeCreatorSearchBar().type('{enter}');
|
this.getters.nodeCreatorSearchBar().type('{enter}');
|
||||||
if (keepNdvOpen) return;
|
if (opts?.action) {
|
||||||
|
nodeCreator.getters.getCreatorItem(opts.action).click();
|
||||||
|
}
|
||||||
|
else if (!opts?.keepNdvOpen) {
|
||||||
cy.get('body').type('{esc}');
|
cy.get('body').type('{esc}');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
addNodeToCanvas: (
|
addNodeToCanvas: (
|
||||||
nodeDisplayName: string,
|
nodeDisplayName: string,
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
|
export function getPopper() {
|
||||||
|
return cy.get('.el-popper');
|
||||||
|
}
|
||||||
|
|
||||||
export function getVisiblePopper() {
|
export function getVisiblePopper() {
|
||||||
return cy.get('.el-popper').filter(':visible');
|
return getPopper().filter(':visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getVisibleSelect() {
|
export function getVisibleSelect() {
|
||||||
|
|
|
@ -31,6 +31,7 @@ import {
|
||||||
CUSTOM_API_CALL_NAME,
|
CUSTOM_API_CALL_NAME,
|
||||||
inTest,
|
inTest,
|
||||||
CLI_DIR,
|
CLI_DIR,
|
||||||
|
inE2ETests,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
|
||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
|
@ -64,6 +65,11 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
if (!inTest) module.constructor._initPaths();
|
if (!inTest) module.constructor._initPaths();
|
||||||
|
|
||||||
|
if (!inE2ETests) {
|
||||||
|
this.excludeNodes = this.excludeNodes ?? [];
|
||||||
|
this.excludeNodes.push('n8n-nodes-base.e2eTest');
|
||||||
|
}
|
||||||
|
|
||||||
this.downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
|
this.downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
|
||||||
|
|
||||||
// Load nodes from `n8n-nodes-base`
|
// Load nodes from `n8n-nodes-base`
|
||||||
|
|
|
@ -888,7 +888,8 @@ export default defineComponent({
|
||||||
if (
|
if (
|
||||||
this.node === null ||
|
this.node === null ||
|
||||||
this.hasRemoteMethod === false ||
|
this.hasRemoteMethod === false ||
|
||||||
this.remoteParameterOptionsLoading
|
this.remoteParameterOptionsLoading ||
|
||||||
|
!this.parameter
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -900,7 +901,8 @@ export default defineComponent({
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const currentNodeParameters = (this.ndvStore.activeNode as INodeUi).parameters;
|
const currentNodeParameters = (this.ndvStore.activeNode as INodeUi).parameters;
|
||||||
const resolvedNodeParameters = this.resolveParameter(
|
const resolvedNodeParameters = this.resolveRequiredParameters(
|
||||||
|
this.parameter,
|
||||||
currentNodeParameters,
|
currentNodeParameters,
|
||||||
) as INodeParameters;
|
) as INodeParameters;
|
||||||
const loadOptionsMethod = this.getArgument('loadOptionsMethod') as string | undefined;
|
const loadOptionsMethod = this.getArgument('loadOptionsMethod') as string | undefined;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style['parameter-issues']" v-if="issues.length">
|
<div :class="$style['parameter-issues']" data-test-id="parameter-issues" v-if="issues.length">
|
||||||
<n8n-tooltip placement="top">
|
<n8n-tooltip placement="top">
|
||||||
<template #content>
|
<template #content>
|
||||||
<titled-list :title="`${$locale.baseText('parameterInput.issues')}:`" :items="issues" />
|
<titled-list :title="`${$locale.baseText('parameterInput.issues')}:`" :items="issues" />
|
||||||
|
|
|
@ -681,7 +681,10 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedNodeParameters = this.resolveParameter(params.parameters) as INodeParameters;
|
const resolvedNodeParameters = this.resolveRequiredParameters(
|
||||||
|
this.parameter,
|
||||||
|
params.parameters,
|
||||||
|
) as INodeParameters;
|
||||||
const loadOptionsMethod = this.getPropertyArgument(this.currentMode, 'searchListMethod') as
|
const loadOptionsMethod = this.getPropertyArgument(this.currentMode, 'searchListMethod') as
|
||||||
| string
|
| string
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
@update:modelValue="onFilterInput"
|
@update:modelValue="onFilterInput"
|
||||||
ref="search"
|
ref="search"
|
||||||
:placeholder="$locale.baseText('resourceLocator.search.placeholder')"
|
:placeholder="$locale.baseText('resourceLocator.search.placeholder')"
|
||||||
|
data-test-id="rlc-search"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<font-awesome-icon :class="$style.searchIcon" icon="search" />
|
<font-awesome-icon :class="$style.searchIcon" icon="search" />
|
||||||
|
@ -47,6 +48,7 @@
|
||||||
[$style.selected]: result.value === modelValue,
|
[$style.selected]: result.value === modelValue,
|
||||||
[$style.hovering]: hoverIndex === i,
|
[$style.hovering]: hoverIndex === i,
|
||||||
}"
|
}"
|
||||||
|
data-test-id="rlc-item"
|
||||||
@click="() => onItemClick(result.value)"
|
@click="() => onItemClick(result.value)"
|
||||||
@mouseenter="() => onItemHover(i)"
|
@mouseenter="() => onItemHover(i)"
|
||||||
@mouseleave="() => onItemHoverLeave()"
|
@mouseleave="() => onItemHoverLeave()"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { IUpdateInformation, ResourceMapperReqParams } from '@/Interface';
|
import type { IUpdateInformation, ResourceMapperReqParams } from '@/Interface';
|
||||||
import { resolveParameter } from '@/mixins/workflowHelpers';
|
import { resolveRequiredParameters } from '@/mixins/workflowHelpers';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import type {
|
import type {
|
||||||
INode,
|
INode,
|
||||||
|
@ -229,7 +229,10 @@ async function loadFieldsToMap(): Promise<void> {
|
||||||
name: props.node?.type,
|
name: props.node?.type,
|
||||||
version: props.node.typeVersion,
|
version: props.node.typeVersion,
|
||||||
},
|
},
|
||||||
currentNodeParameters: resolveParameter(props.node.parameters) as INodeParameters,
|
currentNodeParameters: resolveRequiredParameters(
|
||||||
|
props.parameter,
|
||||||
|
props.node.parameters,
|
||||||
|
) as INodeParameters,
|
||||||
path: props.path,
|
path: props.path,
|
||||||
methodName: props.parameter.typeOptions?.resourceMapper?.resourceMapperMethod,
|
methodName: props.parameter.typeOptions?.resourceMapper?.resourceMapperMethod,
|
||||||
credentials: props.node.credentials,
|
credentials: props.node.credentials,
|
||||||
|
|
|
@ -26,7 +26,7 @@ describe('ResourceMapper.vue', () => {
|
||||||
.spyOn(nodeTypeStore, 'getResourceMapperFields')
|
.spyOn(nodeTypeStore, 'getResourceMapperFields')
|
||||||
.mockResolvedValue(MAPPING_COLUMNS_RESPONSE);
|
.mockResolvedValue(MAPPING_COLUMNS_RESPONSE);
|
||||||
resolveParameterSpy = vi
|
resolveParameterSpy = vi
|
||||||
.spyOn(workflowHelpers, 'resolveParameter')
|
.spyOn(workflowHelpers, 'resolveRequiredParameters')
|
||||||
.mockReturnValue(NODE_PARAMETER_VALUES);
|
.mockReturnValue(NODE_PARAMETER_VALUES);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,8 @@ import type {
|
||||||
IExecuteData,
|
IExecuteData,
|
||||||
INodeConnection,
|
INodeConnection,
|
||||||
IWebhookDescription,
|
IWebhookDescription,
|
||||||
|
INodeProperties,
|
||||||
|
IWorkflowSettings,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { NodeHelpers } from 'n8n-workflow';
|
import { NodeHelpers } from 'n8n-workflow';
|
||||||
|
|
||||||
|
@ -39,6 +41,7 @@ import type {
|
||||||
XYPosition,
|
XYPosition,
|
||||||
ITag,
|
ITag,
|
||||||
TargetItem,
|
TargetItem,
|
||||||
|
ICredentialsResponse,
|
||||||
} from '../Interface';
|
} from '../Interface';
|
||||||
|
|
||||||
import { externalHooks } from '@/mixins/externalHooks';
|
import { externalHooks } from '@/mixins/externalHooks';
|
||||||
|
@ -53,7 +56,6 @@ import { getSourceItems } from '@/utils';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||||
import type { IWorkflowSettings } from 'n8n-workflow';
|
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
import { useTemplatesStore } from '@/stores/templates.store';
|
import { useTemplatesStore } from '@/stores/templates.store';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
@ -62,7 +64,6 @@ import { useEnvironmentsStore } from '@/stores/environments.ee.store';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
import { getWorkflowPermissions } from '@/permissions';
|
import { getWorkflowPermissions } from '@/permissions';
|
||||||
import type { IPermissions } from '@/permissions';
|
import type { IPermissions } from '@/permissions';
|
||||||
import type { ICredentialsResponse } from '@/Interface';
|
|
||||||
|
|
||||||
export function resolveParameter(
|
export function resolveParameter(
|
||||||
parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
|
parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
|
||||||
|
@ -122,7 +123,7 @@ export function resolveParameter(
|
||||||
);
|
);
|
||||||
|
|
||||||
let runExecutionData: IRunExecutionData;
|
let runExecutionData: IRunExecutionData;
|
||||||
if (executionData === null || !executionData.data) {
|
if (!executionData?.data) {
|
||||||
runExecutionData = {
|
runExecutionData = {
|
||||||
resultData: {
|
resultData: {
|
||||||
runData: {},
|
runData: {},
|
||||||
|
@ -176,6 +177,47 @@ export function resolveParameter(
|
||||||
) as IDataObject;
|
) as IDataObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveRequiredParameters(
|
||||||
|
currentParameter: INodeProperties,
|
||||||
|
parameters: INodeParameters,
|
||||||
|
opts: {
|
||||||
|
targetItem?: TargetItem;
|
||||||
|
inputNodeName?: string;
|
||||||
|
inputRunIndex?: number;
|
||||||
|
inputBranchIndex?: number;
|
||||||
|
} = {},
|
||||||
|
): IDataObject | null {
|
||||||
|
const loadOptionsDependsOn = currentParameter?.typeOptions?.loadOptionsDependsOn ?? [];
|
||||||
|
|
||||||
|
const initial: { required: INodeParameters; nonrequired: INodeParameters } = {
|
||||||
|
required: {},
|
||||||
|
nonrequired: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { required, nonrequired }: INodeParameters = Object.keys(parameters).reduce(
|
||||||
|
(accu, name: string) => {
|
||||||
|
const required = loadOptionsDependsOn.includes(name);
|
||||||
|
accu[required ? 'required' : 'nonrequired'][name] = parameters[name];
|
||||||
|
|
||||||
|
return accu;
|
||||||
|
},
|
||||||
|
initial,
|
||||||
|
);
|
||||||
|
|
||||||
|
const resolvedRequired = resolveParameter(required, opts);
|
||||||
|
let resolvedNonrequired: IDataObject | null = {};
|
||||||
|
try {
|
||||||
|
resolvedNonrequired = resolveParameter(nonrequired, opts);
|
||||||
|
} catch (e) {
|
||||||
|
// ignore any expression errors for example
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...resolvedRequired,
|
||||||
|
...(resolvedNonrequired ?? {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getCurrentWorkflow(copyData?: boolean): Workflow {
|
function getCurrentWorkflow(copyData?: boolean): Workflow {
|
||||||
return useWorkflowsStore().getCurrentWorkflow(copyData);
|
return useWorkflowsStore().getCurrentWorkflow(copyData);
|
||||||
}
|
}
|
||||||
|
@ -210,7 +252,7 @@ function connectionInputData(
|
||||||
) {
|
) {
|
||||||
connectionInputData = [];
|
connectionInputData = [];
|
||||||
} else {
|
} else {
|
||||||
connectionInputData = _executeData.data![inputName][nodeConnection.sourceIndex];
|
connectionInputData = _executeData.data[inputName][nodeConnection.sourceIndex];
|
||||||
|
|
||||||
if (connectionInputData !== null) {
|
if (connectionInputData !== null) {
|
||||||
// Update the pairedItem information on items
|
// Update the pairedItem information on items
|
||||||
|
@ -314,7 +356,7 @@ function executeData(
|
||||||
executeData.data = workflowRunData[parentNodeName][runIndex].data!;
|
executeData.data = workflowRunData[parentNodeName][runIndex].data!;
|
||||||
if (workflowRunData[currentNode] && workflowRunData[currentNode][runIndex]) {
|
if (workflowRunData[currentNode] && workflowRunData[currentNode][runIndex]) {
|
||||||
executeData.source = {
|
executeData.source = {
|
||||||
[inputName]: workflowRunData[currentNode][runIndex].source!,
|
[inputName]: workflowRunData[currentNode][runIndex].source,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// The current node did not get executed in UI yet so build data manually
|
// The current node did not get executed in UI yet so build data manually
|
||||||
|
@ -357,6 +399,7 @@ export const workflowHelpers = defineComponent({
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
resolveParameter,
|
resolveParameter,
|
||||||
|
resolveRequiredParameters,
|
||||||
getCurrentWorkflow,
|
getCurrentWorkflow,
|
||||||
getNodes,
|
getNodes,
|
||||||
getWorkflow,
|
getWorkflow,
|
||||||
|
@ -572,9 +615,7 @@ export const workflowHelpers = defineComponent({
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!this.displayParameter(node.parameters, credentialTypeDescription, '', node)) {
|
||||||
this.displayParameter(node.parameters, credentialTypeDescription, '', node) === false
|
|
||||||
) {
|
|
||||||
// Credential should not be displayed so do also not save
|
// Credential should not be displayed so do also not save
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -661,7 +702,7 @@ export const workflowHelpers = defineComponent({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const obj = returnData['__xxxxxxx__'];
|
const obj = returnData.__xxxxxxx__;
|
||||||
if (typeof obj === 'object') {
|
if (typeof obj === 'object') {
|
||||||
const proxy = obj as { isProxy: boolean; toJSON?: () => unknown } | null;
|
const proxy = obj as { isProxy: boolean; toJSON?: () => unknown } | null;
|
||||||
if (proxy?.isProxy && proxy.toJSON) return JSON.stringify(proxy.toJSON());
|
if (proxy?.isProxy && proxy.toJSON) return JSON.stringify(proxy.toJSON());
|
||||||
|
@ -921,7 +962,7 @@ export const workflowHelpers = defineComponent({
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
void this.$router.replace({
|
void this.$router.replace({
|
||||||
name: VIEWS.WORKFLOW,
|
name: VIEWS.WORKFLOW,
|
||||||
params: { name: workflowData.id as string, action: 'workflowSave' },
|
params: { name: workflowData.id, action: 'workflowSave' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
18
packages/nodes-base/nodes/E2eTest/E2eTest.node.json
Normal file
18
packages/nodes-base/nodes/E2eTest/E2eTest.node.json
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"node": "n8n-nodes-base.e2eTest",
|
||||||
|
"nodeVersion": "1.0",
|
||||||
|
"codexVersion": "1.0",
|
||||||
|
"details": "The is a test node for e2e testing.",
|
||||||
|
"categories": ["Core Nodes"],
|
||||||
|
"resources": {
|
||||||
|
"primaryDocumentation": [
|
||||||
|
{
|
||||||
|
"url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.editimage/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"subcategories": {
|
||||||
|
"Core Nodes": ["Helpers"]
|
||||||
|
},
|
||||||
|
"e2e": true
|
||||||
|
}
|
207
packages/nodes-base/nodes/E2eTest/E2eTest.node.ts
Normal file
207
packages/nodes-base/nodes/E2eTest/E2eTest.node.ts
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
import type {
|
||||||
|
IExecuteFunctions,
|
||||||
|
ILoadOptionsFunctions,
|
||||||
|
INodeExecutionData,
|
||||||
|
INodeListSearchResult,
|
||||||
|
INodePropertyOptions,
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
ResourceMapperFields,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import { remoteOptions, resourceMapperFields, returnData, searchOptions } from './mock';
|
||||||
|
|
||||||
|
export class E2eTest implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'E2E Test',
|
||||||
|
name: 'e2eTest',
|
||||||
|
icon: 'fa:play',
|
||||||
|
group: ['output'],
|
||||||
|
version: 1,
|
||||||
|
subtitle: '={{$parameter["operation"]}}',
|
||||||
|
description: 'Dummy node used for e2e testing',
|
||||||
|
defaults: {
|
||||||
|
name: 'E2E Test',
|
||||||
|
},
|
||||||
|
inputs: ['main'],
|
||||||
|
outputs: ['main'],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Remote Options',
|
||||||
|
value: 'remoteOptions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Resource Locator',
|
||||||
|
value: 'resourceLocator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Resource Mapping Component',
|
||||||
|
value: 'resourceMapper',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'remoteOptions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Field ID',
|
||||||
|
name: 'fieldId',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Remote Options Name or ID',
|
||||||
|
name: 'remoteOptions',
|
||||||
|
description:
|
||||||
|
'Remote options to load. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsDependsOn: ['fieldId'],
|
||||||
|
loadOptionsMethod: 'getOptions',
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
default: [],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: ['remoteOptions'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Resource Locator',
|
||||||
|
name: 'rlc',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
default: { mode: 'list', value: '' },
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: ['resourceLocator'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'From List',
|
||||||
|
name: 'list',
|
||||||
|
type: 'list',
|
||||||
|
typeOptions: {
|
||||||
|
searchListMethod: 'optionsSearch',
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By URL',
|
||||||
|
name: 'url',
|
||||||
|
type: 'string',
|
||||||
|
placeholder: 'https://example.com/user/a4071e98-7d40-41fb-8911-ce3e7bf94fb2',
|
||||||
|
validation: [
|
||||||
|
{
|
||||||
|
type: 'regex',
|
||||||
|
properties: {
|
||||||
|
regex:
|
||||||
|
'https://example.com/user/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
|
||||||
|
errorMessage: 'Not a valid example URL',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
extractValue: {
|
||||||
|
type: 'regex',
|
||||||
|
regex:
|
||||||
|
'https://example.com/user/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'ID',
|
||||||
|
name: 'id',
|
||||||
|
type: 'string',
|
||||||
|
validation: [
|
||||||
|
{
|
||||||
|
type: 'regex',
|
||||||
|
properties: {
|
||||||
|
regex: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
|
||||||
|
errorMessage: 'Not a valid UUI',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
placeholder: 'a4071e98-7d40-41fb-8911-ce3e7bf94fb2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Resource Mapping Component',
|
||||||
|
name: 'resourceMapper',
|
||||||
|
type: 'resourceMapper',
|
||||||
|
noDataExpression: true,
|
||||||
|
default: {
|
||||||
|
mappingMode: 'defineBelow',
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsDependsOn: ['fieldId'],
|
||||||
|
resourceMapper: {
|
||||||
|
resourceMapperMethod: 'getMappingColumns',
|
||||||
|
mode: 'upsert',
|
||||||
|
fieldWords: {
|
||||||
|
singular: 'column',
|
||||||
|
plural: 'columns',
|
||||||
|
},
|
||||||
|
addAllFields: true,
|
||||||
|
multiKeyMatch: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: ['resourceMapper'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Other Non Important Field',
|
||||||
|
name: 'otherField',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
methods = {
|
||||||
|
loadOptions: {
|
||||||
|
async getOptions(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
|
return remoteOptions;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
listSearch: {
|
||||||
|
async optionsSearch(
|
||||||
|
this: ILoadOptionsFunctions,
|
||||||
|
filter?: string,
|
||||||
|
paginationToken?: string,
|
||||||
|
): Promise<INodeListSearchResult> {
|
||||||
|
const pageSize = 5;
|
||||||
|
let results = searchOptions;
|
||||||
|
if (filter) {
|
||||||
|
results = results.filter((option) => option.name.includes(filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = paginationToken ? parseInt(paginationToken, 10) : 0;
|
||||||
|
results = results.slice(offset, offset + pageSize);
|
||||||
|
|
||||||
|
return {
|
||||||
|
results,
|
||||||
|
paginationToken: offset + pageSize,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resourceMapping: {
|
||||||
|
async getMappingColumns(this: ILoadOptionsFunctions): Promise<ResourceMapperFields> {
|
||||||
|
return resourceMapperFields;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
return this.prepareOutputData(returnData);
|
||||||
|
}
|
||||||
|
}
|
64
packages/nodes-base/nodes/E2eTest/mock.ts
Normal file
64
packages/nodes-base/nodes/E2eTest/mock.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import { array, name, uuid } from 'minifaker';
|
||||||
|
import 'minifaker/locales/en';
|
||||||
|
import type {
|
||||||
|
INodeExecutionData,
|
||||||
|
INodeListSearchResult,
|
||||||
|
INodePropertyOptions,
|
||||||
|
ResourceMapperFields,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export const returnData: INodeExecutionData[] = [
|
||||||
|
{
|
||||||
|
json: {
|
||||||
|
id: '23423532',
|
||||||
|
name: 'Hello World',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const remoteOptions: INodePropertyOptions[] = [
|
||||||
|
{
|
||||||
|
name: 'Resource 1',
|
||||||
|
value: 'resource1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Resource 2',
|
||||||
|
value: 'resource2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Resource 3',
|
||||||
|
value: 'resource3',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const resourceMapperFields: ResourceMapperFields = {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
id: 'id',
|
||||||
|
displayName: 'ID',
|
||||||
|
defaultMatch: true,
|
||||||
|
canBeUsedToMatch: true,
|
||||||
|
required: true,
|
||||||
|
display: true,
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'name',
|
||||||
|
displayName: 'Name',
|
||||||
|
defaultMatch: false,
|
||||||
|
canBeUsedToMatch: false,
|
||||||
|
required: false,
|
||||||
|
display: true,
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const searchOptions: INodeListSearchResult['results'] = array(100, () => {
|
||||||
|
const value = uuid.v4();
|
||||||
|
return {
|
||||||
|
name: name(),
|
||||||
|
value,
|
||||||
|
url: 'https://example.com/user/' + value,
|
||||||
|
};
|
||||||
|
});
|
|
@ -466,6 +466,7 @@
|
||||||
"dist/nodes/Dropbox/Dropbox.node.js",
|
"dist/nodes/Dropbox/Dropbox.node.js",
|
||||||
"dist/nodes/Dropcontact/Dropcontact.node.js",
|
"dist/nodes/Dropcontact/Dropcontact.node.js",
|
||||||
"dist/nodes/EditImage/EditImage.node.js",
|
"dist/nodes/EditImage/EditImage.node.js",
|
||||||
|
"dist/nodes/E2eTest/E2eTest.node.js",
|
||||||
"dist/nodes/Egoi/Egoi.node.js",
|
"dist/nodes/Egoi/Egoi.node.js",
|
||||||
"dist/nodes/Elastic/Elasticsearch/Elasticsearch.node.js",
|
"dist/nodes/Elastic/Elasticsearch/Elasticsearch.node.js",
|
||||||
"dist/nodes/Elastic/ElasticSecurity/ElasticSecurity.node.js",
|
"dist/nodes/Elastic/ElasticSecurity/ElasticSecurity.node.js",
|
||||||
|
|
Loading…
Reference in a new issue