diff --git a/package-lock.json b/package-lock.json index f8e152f29513b..4a5e9e3e77390 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53136,6 +53136,7 @@ "eslint": "^8.0.0", "eslint-plugin-import": "^2.23.4", "eslint-plugin-vue": "^7.16.0", + "fast-json-stable-stringify": "^2.1.0", "file-saver": "^2.0.2", "flatted": "^3.2.4", "jquery": "^3.4.1", @@ -82863,6 +82864,7 @@ "eslint": "^8.0.0", "eslint-plugin-import": "^2.23.4", "eslint-plugin-vue": "^7.16.0", + "fast-json-stable-stringify": "^2.1.0", "file-saver": "^2.0.2", "flatted": "^3.2.4", "jquery": "^3.4.1", diff --git a/packages/design-system/src/components/N8nButton/Button.vue b/packages/design-system/src/components/N8nButton/Button.vue index 05033ba02a788..f798ab1c163e3 100644 --- a/packages/design-system/src/components/N8nButton/Button.vue +++ b/packages/design-system/src/components/N8nButton/Button.vue @@ -372,6 +372,10 @@ $loading-overlay-background-color: rgba(255, 255, 255, 0); --button-hover-color: var(--color-success); } + &.tertiary { + --button-hover-color: var(--color-primary); + } + &.warning { --button-color: var(--color-warning); --button-active-color: var(--color-warning); diff --git a/packages/design-system/src/components/N8nInput/Input.vue b/packages/design-system/src/components/N8nInput/Input.vue index 2577fb58e180f..5aa88ce1b0403 100644 --- a/packages/design-system/src/components/N8nInput/Input.vue +++ b/packages/design-system/src/components/N8nInput/Input.vue @@ -52,6 +52,9 @@ export default Vue.extend({ disabled: { type: Boolean, }, + readonly: { + type: Boolean, + }, clearable: { type: Boolean, }, diff --git a/packages/design-system/src/components/N8nLink/Link.vue b/packages/design-system/src/components/N8nLink/Link.vue index 12dadeaac9427..b34caa1213147 100644 --- a/packages/design-system/src/components/N8nLink/Link.vue +++ b/packages/design-system/src/components/N8nLink/Link.vue @@ -72,6 +72,10 @@ export default Vue.extend({ .text { color: var(--color-text-base); + &:hover { + color: var(--color-primary); + } + &:active { color: saturation(--color-primary-h, --color-primary-s, --color-primary-l, -(30%)); } diff --git a/packages/design-system/theme/src/input.scss b/packages/design-system/theme/src/input.scss index a2ec1cb955332..ed909a7d85256 100644 --- a/packages/design-system/theme/src/input.scss +++ b/packages/design-system/theme/src/input.scss @@ -133,7 +133,7 @@ @include mixins.e(suffix) { position: absolute; height: 100%; - right: 10px; + right: var(--spacing-2xs); top: 0; text-align: center; color: var(--color-text-light); diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 7c3777fe4e137..3e4bbff0d2793 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -100,6 +100,7 @@ "vue-template-compiler": "~2.6.11", "vue-typed-mixins": "^0.2.0", "vue2-touch-events": "^3.2.1", + "fast-json-stable-stringify": "^2.1.0", "vuex": "^3.1.1" } } diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 1d88fe5cf4dd5..1fd5a4b07c4be 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -23,6 +23,7 @@ import { IWorkflowSettings as IWorkflowSettingsWorkflow, WorkflowExecuteMode, PublicInstalledPackage, + INodeListSearchItems, } from 'n8n-workflow'; import { FAKE_DOOR_FEATURES } from './constants'; @@ -1072,3 +1073,18 @@ export interface ITab { align?: 'right'; tooltip?: string; } + +export interface IResourceLocatorReqParams { + nodeTypeAndVersion: INodeTypeNameVersion; + path: string; + methodName?: string; + searchList?: ILoadOptions; + currentNodeParameters: INodeParameters; + credentials?: INodeCredentials; + filter?: string; + paginationToken?: unknown; +} + +export interface IResourceLocatorResultExpanded extends INodeListSearchItems { + linkAlt?: string; +} diff --git a/packages/editor-ui/src/api/nodeTypes.ts b/packages/editor-ui/src/api/nodeTypes.ts index 2328db1a70f7b..b6ebfe8f2bf4a 100644 --- a/packages/editor-ui/src/api/nodeTypes.ts +++ b/packages/editor-ui/src/api/nodeTypes.ts @@ -1,11 +1,14 @@ import { makeRestApiRequest } from './helpers'; import type { INodeTranslationHeaders, + IResourceLocatorReqParams, IRestApiContext, } from '@/Interface'; import type { + IDataObject, ILoadOptions, INodeCredentials, + INodeListSearchResult, INodeParameters, INodePropertyOptions, INodeTypeDescription, @@ -45,3 +48,11 @@ export async function getNodeParameterOptions( ): Promise { return makeRestApiRequest(context, 'GET', '/node-parameter-options', sendData); } + +export async function getResourceLocatorResults( + context: IRestApiContext, + sendData: IResourceLocatorReqParams, +): Promise { + return makeRestApiRequest(context, 'GET', '/nodes-list-search', sendData as unknown as IDataObject); +} + diff --git a/packages/editor-ui/src/components/BreakpointsObserver.vue b/packages/editor-ui/src/components/BreakpointsObserver.vue index b34a54799d967..bad6647408951 100644 --- a/packages/editor-ui/src/components/BreakpointsObserver.vue +++ b/packages/editor-ui/src/components/BreakpointsObserver.vue @@ -23,8 +23,9 @@ import { import mixins from "vue-typed-mixins"; import { genericHelpers } from "@/components/mixins/genericHelpers"; +import { debounceHelper } from "./mixins/debounce"; -export default mixins(genericHelpers).extend({ +export default mixins(genericHelpers, debounceHelper).extend({ name: "BreakpointsObserver", props: [ "valueXS", @@ -98,4 +99,4 @@ export default mixins(genericHelpers).extend({ }, }, }); - \ No newline at end of file + diff --git a/packages/editor-ui/src/components/CredentialsList.vue b/packages/editor-ui/src/components/CredentialsList.vue index 1b5a4a66995dc..b7e0951435b94 100644 --- a/packages/editor-ui/src/components/CredentialsList.vue +++ b/packages/editor-ui/src/components/CredentialsList.vue @@ -110,7 +110,7 @@ export default mixins( }, editCredential (credential: ICredentialsResponse) { - this.$store.dispatch('ui/openExisitngCredential', { id: credential.id}); + this.$store.dispatch('ui/openExistingCredential', { id: credential.id}); this.$telemetry.track('User opened Credential modal', { credential_type: credential.type, source: 'primary_menu', new_credential: false, workflow_id: this.$store.getters.workflowId }); }, diff --git a/packages/editor-ui/src/components/ExpressionEdit.vue b/packages/editor-ui/src/components/ExpressionEdit.vue index 0a2a5dea57695..482c97e1b9c71 100644 --- a/packages/editor-ui/src/components/ExpressionEdit.vue +++ b/packages/editor-ui/src/components/ExpressionEdit.vue @@ -53,10 +53,12 @@ import { genericHelpers } from '@/components/mixins/genericHelpers'; import mixins from 'vue-typed-mixins'; import { hasExpressionMapping } from './helpers'; +import { debounceHelper } from './mixins/debounce'; export default mixins( externalHooks, genericHelpers, + debounceHelper, ).extend({ name: 'ExpressionEdit', props: [ diff --git a/packages/editor-ui/src/components/NodeCredentials.vue b/packages/editor-ui/src/components/NodeCredentials.vue index f9d8056d4a466..1d6f119eabff3 100644 --- a/packages/editor-ui/src/components/NodeCredentials.vue +++ b/packages/editor-ui/src/components/NodeCredentials.vue @@ -304,7 +304,7 @@ export default mixins( editCredential(credentialType: string): void { const { id } = this.node.credentials[credentialType]; - this.$store.dispatch('ui/openExisitngCredential', { id }); + this.$store.dispatch('ui/openExistingCredential', { id }); this.$telemetry.track('User opened Credential modal', { credential_type: credentialType, source: 'node', new_credential: false, workflow_id: this.$store.getters.workflowId }); diff --git a/packages/editor-ui/src/components/ParameterInput.vue b/packages/editor-ui/src/components/ParameterInput.vue index 31438874e60b3..bace4083bd37b 100644 --- a/packages/editor-ui/src/components/ParameterInput.vue +++ b/packages/editor-ui/src/components/ParameterInput.vue @@ -2,7 +2,7 @@
+ @drop="onResourceLocatorDrop" + />
- + @@ -317,7 +315,8 @@ import { workflowHelpers } from '@/components/mixins/workflowHelpers'; import mixins from 'vue-typed-mixins'; import { CUSTOM_API_CALL_KEY } from '@/constants'; import { mapGetters } from 'vuex'; -import { hasExpressionMapping } from './helpers'; +import { hasExpressionMapping, isValueExpression } from './helpers'; +import { isResourceLocatorValue } from '@/typeGuards'; export default mixins( externalHooks, @@ -326,7 +325,7 @@ export default mixins( workflowHelpers, ) .extend({ - name: 'ParameterInput', + name: 'parameter-input', components: { CodeEdit, ExpressionEdit, @@ -353,7 +352,6 @@ export default mixins( 'activeDrop', 'droppable', 'forceShowExpression', - 'isValueExpression', ], data () { return { @@ -415,6 +413,9 @@ export default mixins( }, computed: { ...mapGetters('credentials', ['allCredentialTypes']), + isValueExpression(): boolean { + return isValueExpression(this.parameter, this.value); + }, areExpressionsDisabled(): boolean { return this.$store.getters['ui/areExpressionsDisabled']; }, @@ -479,7 +480,7 @@ export default mixins( let returnValue; if (this.isValueExpression === false) { - returnValue = this.isResourceLocatorParameter ? this.value.value : this.value; + returnValue = this.isResourceLocatorParameter ? (this.value ? this.value.value: '') : this.value; } else { returnValue = this.expressionValueComputed; } @@ -670,7 +671,7 @@ export default mixins( const styles = { width: '100%', }; - if (this.parameter.type === 'credentialsSelect') { + if (this.parameter.type === 'credentialsSelect' || this.isResourceLocatorParameter) { return styles; } if (this.getIssues.length) { @@ -744,9 +745,9 @@ export default mixins( this.remoteParameterOptions.length = 0; // Get the resolved parameter values of the current node - const currentNodeParameters = this.$store.getters.activeNode.parameters; try { + const currentNodeParameters = (this.$store.getters.activeNode as INodeUi).parameters; const resolvedNodeParameters = this.resolveParameter(currentNodeParameters) as INodeParameters; const loadOptionsMethod = this.getArgument('loadOptionsMethod') as string | undefined; const loadOptions = this.getArgument('loadOptions') as ILoadOptions | undefined; @@ -834,7 +835,7 @@ export default mixins( onBlur () { this.$emit('blur'); }, - onDrop(data: string) { + onResourceLocatorDrop(data: string) { this.$emit('drop', data); }, setFocus () { @@ -936,7 +937,11 @@ export default mixins( if (this.parameter.type === 'number' || this.parameter.type === 'boolean') { this.valueChanged({ value: `={{${this.value}}}`, mode: this.value.mode }); } else if (this.isResourceLocatorParameter) { - this.valueChanged({ value: `=${this.value.value}`, mode: this.value.mode }); + if (isResourceLocatorValue(this.value)) { + this.valueChanged({ value: `=${this.value.value}`, mode: this.value.mode }); + } else { + this.valueChanged({ value: `=${this.value}`, mode: '' }); + } } else { this.valueChanged(`=${this.value}`); } @@ -959,6 +964,12 @@ export default mixins( this.valueChanged(typeof value !== 'undefined' ? value : null); } } else if (command === 'refreshOptions') { + if (this.isResourceLocatorParameter) { + const resourceLocator = this.$refs.resourceLocator; + if (resourceLocator) { + (resourceLocator as Vue).$emit('refreshList'); + } + } this.loadRemoteParameterOptions(); } diff --git a/packages/editor-ui/src/components/ParameterInputFull.vue b/packages/editor-ui/src/components/ParameterInputFull.vue index 20219a81d9305..a715068381edd 100644 --- a/packages/editor-ui/src/components/ParameterInputFull.vue +++ b/packages/editor-ui/src/components/ParameterInputFull.vue @@ -13,38 +13,37 @@ :value="value" :isReadOnly="isReadOnly" :showOptions="displayOptions" - :isValueExpression="isValueExpression" + :showExpressionSelector="showExpressionSelector" @optionSelected="optionSelected" @menu-expanded="onMenuExpanded" /> @@ -65,13 +64,16 @@ import DraggableTarget from '@/components/DraggableTarget.vue'; import mixins from 'vue-typed-mixins'; import { showMessage } from './mixins/showMessage'; import { LOCAL_STORAGE_MAPPING_FLAG } from '@/constants'; -import { hasExpressionMapping, isValueExpression } from './helpers'; +import { hasExpressionMapping } from './helpers'; +import { hasOnlyListMode } from './ResourceLocator/helpers'; +import { INodePropertyMode } from 'n8n-workflow'; +import { isResourceLocatorValue } from '@/typeGuards'; export default mixins( showMessage, ) .extend({ - name: 'ParameterInputFull', + name: 'parameter-input-full', components: { ParameterInput, InputHint, @@ -103,8 +105,8 @@ export default mixins( isDropDisabled (): boolean { return this.parameter.noDataExpression || this.isReadOnly || this.isResourceLocator; }, - isValueExpression (): boolean { - return isValueExpression(this.parameter, this.value); + showExpressionSelector (): boolean { + return this.isResourceLocator ? !hasOnlyListMode(this.parameter): true; }, }, methods: { @@ -144,11 +146,43 @@ export default mixins( updatedValue = `=${data}`; } - const parameterData = { - node: this.node.name, - name: this.path, - value: this.isResourceLocator ? { value: updatedValue, mode: this.value.mode } : updatedValue, - }; + + let parameterData; + if (this.isResourceLocator) { + if (!isResourceLocatorValue(this.value)) { + parameterData = { + node: this.node.name, + name: this.path, + value: { value: updatedValue, mode: '' }, + }; + } + else if (this.value.mode === 'list' && this.parameter.modes && this.parameter.modes.length > 1) { + let mode = this.parameter.modes.find((mode: INodePropertyMode) => mode.name === 'id') || null; + if (!mode) { + mode = this.parameter.modes.filter((mode: INodePropertyMode) => mode.name !== 'list')[0]; + } + + parameterData = { + node: this.node.name, + name: this.path, + value: { value: updatedValue, mode: mode ? mode.name : '' }, + }; + } + else { + parameterData = { + node: this.node.name, + name: this.path, + value: { value: updatedValue, mode: this.value.mode }, + }; + } + + } else { + parameterData = { + node: this.node.name, + name: this.path, + value: updatedValue, + }; + } this.$emit('valueChanged', parameterData); diff --git a/packages/editor-ui/src/components/ParameterOptions.vue b/packages/editor-ui/src/components/ParameterOptions.vue index 6b4e5cdba65e6..e68464db0ee96 100644 --- a/packages/editor-ui/src/components/ParameterOptions.vue +++ b/packages/editor-ui/src/components/ParameterOptions.vue @@ -11,7 +11,7 @@ @visible-change="onMenuToggle" /> diff --git a/packages/editor-ui/src/components/ResourceLocator/ResourceLocatorDropdown.vue b/packages/editor-ui/src/components/ResourceLocator/ResourceLocatorDropdown.vue new file mode 100644 index 0000000000000..3b4a133aa5ff9 --- /dev/null +++ b/packages/editor-ui/src/components/ResourceLocator/ResourceLocatorDropdown.vue @@ -0,0 +1,327 @@ + + + + + diff --git a/packages/editor-ui/src/components/ResourceLocator/helpers.ts b/packages/editor-ui/src/components/ResourceLocator/helpers.ts index 49cba6fefe30b..42d3007236a6e 100644 --- a/packages/editor-ui/src/components/ResourceLocator/helpers.ts +++ b/packages/editor-ui/src/components/ResourceLocator/helpers.ts @@ -1,36 +1,7 @@ import { - INodePropertyMode, - INodePropertyModeValidation, - INodePropertyRegexValidation, + INodeProperties, } from 'n8n-workflow'; -const RESOURCE_LOCATOR_MODE_LABEL_MAPPING: { [key: string]: string } = { - 'id': 'parameterInput.resourceLocator.mode.id', - 'url': 'parameterInput.resourceLocator.mode.url', - 'list': 'parameterInput.resourceLocator.mode.list', -}; - -export const getParameterModeLabel = (type: string) : string | null => { - return RESOURCE_LOCATOR_MODE_LABEL_MAPPING[type] || null; -}; - -// Validates resource locator node parameters based on validation ruled defined in each parameter mode -export const validateResourceLocatorParameter = (displayValue: string, parameterMode: INodePropertyMode) : string[] => { - const validationErrors: string[] = []; - // Each mode can have multiple validations specified - if (parameterMode.validation) { - for (const validation of parameterMode.validation) { - // Currently only regex validation is supported on the front-end - if (validation && (validation as INodePropertyModeValidation).type === 'regex') { - const regexValidation = validation as INodePropertyRegexValidation; - const regex = new RegExp(regexValidation.properties.regex); - - if (!regex.test(displayValue)) { - validationErrors.push(regexValidation.properties.errorMessage); - } - } - } - } - - return validationErrors; +export const hasOnlyListMode = (parameter: INodeProperties) : boolean => { + return parameter.modes !== undefined && parameter.modes.length === 1 && parameter.modes[0].name === 'list'; }; diff --git a/packages/editor-ui/src/components/helpers.ts b/packages/editor-ui/src/components/helpers.ts index 20861d7c49105..fae6de2ffb3ea 100644 --- a/packages/editor-ui/src/components/helpers.ts +++ b/packages/editor-ui/src/components/helpers.ts @@ -1,9 +1,11 @@ import { CORE_NODES_CATEGORY, ERROR_TRIGGER_NODE_TYPE, MAPPING_PARAMS, TEMPLATES_NODES_FILTER } from '@/constants'; import { INodeUi, ITemplatesNode } from '@/Interface'; +import { isResourceLocatorValue } from '@/typeGuards'; import dateformat from 'dateformat'; -import {IDataObject, INodeParameterResourceLocator, INodeProperties, INodeTypeDescription} from 'n8n-workflow'; +import {IDataObject, INodeProperties, INodeTypeDescription, NodeParameterValueType} from 'n8n-workflow'; -const KEYWORDS_TO_FILTER = ['API', 'OAuth1', 'OAuth2']; +const CRED_KEYWORDS_TO_FILTER = ['API', 'OAuth1', 'OAuth2']; +const NODE_KEYWORDS_TO_FILTER = ['Trigger']; const SI_SYMBOL = ['', 'k', 'M', 'G', 'T', 'P', 'E']; const COMMUNITY_PACKAGE_NAME_REGEX = /(@\w+\/)?n8n-nodes-(?!base\b)\b\w+/g; @@ -29,7 +31,11 @@ export function convertToHumanReadableDate (epochTime: number) { } export function getAppNameFromCredType(name: string) { - return name.split(' ').filter((word) => !KEYWORDS_TO_FILTER.includes(word)).join(' '); + return name.split(' ').filter((word) => !CRED_KEYWORDS_TO_FILTER.includes(word)).join(' '); +} + +export function getAppNameFromNodeName(name: string) { + return name.split(' ').filter((word) => !NODE_KEYWORDS_TO_FILTER.includes(word)).join(' '); } export function getStyleTokenValue(name: string): string { @@ -100,14 +106,14 @@ export function hasExpressionMapping(value: unknown) { return typeof value === 'string' && !!MAPPING_PARAMS.find((param) => value.includes(param)); } -export function isValueExpression (parameter: INodeProperties, paramValue: string| INodeParameterResourceLocator): boolean { +export function isValueExpression (parameter: INodeProperties, paramValue: NodeParameterValueType): boolean { if (parameter.noDataExpression === true) { return false; } if (typeof paramValue === 'string' && paramValue.charAt(0) === '=') { return true; } - if (typeof paramValue === 'object' && paramValue.value && paramValue.value.toString().charAt(0) === '=') { + if (isResourceLocatorValue(paramValue) && paramValue.value && paramValue.value.toString().charAt(0) === '=') { return true; } return false; diff --git a/packages/editor-ui/src/components/mixins/debounce.ts b/packages/editor-ui/src/components/mixins/debounce.ts new file mode 100644 index 0000000000000..fe11b341c3d4d --- /dev/null +++ b/packages/editor-ui/src/components/mixins/debounce.ts @@ -0,0 +1,24 @@ +import { debounce } from 'lodash'; +import Vue from 'vue'; + +export const debounceHelper = Vue.extend({ + data () { + return { + debouncedFunctions: [] as any[], // tslint:disable-line:no-any + }; + }, + methods: { + async callDebounced (...inputParameters: any[]): Promise { // tslint:disable-line:no-any + const functionName = inputParameters.shift() as string; + const { trailing, debounceTime } = inputParameters.shift(); + + // @ts-ignore + if (this.debouncedFunctions[functionName] === undefined) { + // @ts-ignore + this.debouncedFunctions[functionName] = debounce(this[functionName], debounceTime, trailing ? { trailing } : { leading: true } ); + } + // @ts-ignore + await this.debouncedFunctions[functionName].apply(this, inputParameters); + }, + }, +}); diff --git a/packages/editor-ui/src/components/mixins/genericHelpers.ts b/packages/editor-ui/src/components/mixins/genericHelpers.ts index ba56603786401..1306532152563 100644 --- a/packages/editor-ui/src/components/mixins/genericHelpers.ts +++ b/packages/editor-ui/src/components/mixins/genericHelpers.ts @@ -1,6 +1,5 @@ import { showMessage } from '@/components/mixins/showMessage'; import { VIEWS } from '@/constants'; -import { debounce } from 'lodash'; import mixins from 'vue-typed-mixins'; @@ -8,7 +7,6 @@ export const genericHelpers = mixins(showMessage).extend({ data () { return { loadingService: null as any | null, // tslint:disable-line:no-any - debouncedFunctions: [] as any[], // tslint:disable-line:no-any }; }, computed: { @@ -71,18 +69,5 @@ export const genericHelpers = mixins(showMessage).extend({ this.loadingService = null; } }, - - async callDebounced (...inputParameters: any[]): Promise { // tslint:disable-line:no-any - const functionName = inputParameters.shift() as string; - const { trailing, debounceTime } = inputParameters.shift(); - - // @ts-ignore - if (this.debouncedFunctions[functionName] === undefined) { - // @ts-ignore - this.debouncedFunctions[functionName] = debounce(this[functionName], debounceTime, trailing ? { trailing } : { leading: true } ); - } - // @ts-ignore - await this.debouncedFunctions[functionName].apply(this, inputParameters); - }, }, }); diff --git a/packages/editor-ui/src/modules/nodeTypes.ts b/packages/editor-ui/src/modules/nodeTypes.ts index 79f4a4838db88..32c628ab0c90c 100644 --- a/packages/editor-ui/src/modules/nodeTypes.ts +++ b/packages/editor-ui/src/modules/nodeTypes.ts @@ -3,6 +3,7 @@ import { ActionContext, Module } from 'vuex'; import type { ILoadOptions, INodeCredentials, + INodeListSearchResult, INodeParameters, INodeTypeDescription, INodeTypeNameVersion, @@ -15,9 +16,10 @@ import { getNodesInformation, getNodeTranslationHeaders, getNodeTypes, + getResourceLocatorResults, } from '@/api/nodeTypes'; import { omit } from '@/utils'; -import type { IRootState, INodeTypesState } from '../Interface'; +import type { IRootState, INodeTypesState, IResourceLocatorReqParams } from '../Interface'; const module: Module = { namespaced: true, @@ -142,6 +144,12 @@ const module: Module = { ) { return getNodeParameterOptions(context.rootGetters.getRestApiContext, sendData); }, + async getResourceLocatorResults( + context: ActionContext, + sendData: IResourceLocatorReqParams, + ): Promise { + return getResourceLocatorResults(context.rootGetters.getRestApiContext, sendData); + }, }, }; diff --git a/packages/editor-ui/src/modules/ui.ts b/packages/editor-ui/src/modules/ui.ts index 23cfb0ebe8c12..2c0d126922201 100644 --- a/packages/editor-ui/src/modules/ui.ts +++ b/packages/editor-ui/src/modules/ui.ts @@ -299,7 +299,7 @@ const module: Module = { context.commit('setActiveId', { name: DELETE_USER_MODAL_KEY, id }); context.commit('openModal', DELETE_USER_MODAL_KEY); }, - openExisitngCredential: async (context: ActionContext, { id }: {id: string}) => { + openExistingCredential: async (context: ActionContext, { id }: {id: string}) => { context.commit('setActiveId', { name: CREDENTIAL_EDIT_MODAL_KEY, id }); context.commit('setMode', { name: CREDENTIAL_EDIT_MODAL_KEY, mode: 'edit' }); context.commit('openModal', CREDENTIAL_EDIT_MODAL_KEY); diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index b00a41b6baabe..559379d7c2e77 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -599,9 +599,6 @@ "parameterInput.parameterHasIssuesAndExpression": "Parameter: \"{shortPath}\" has issues and an expression", "parameterInput.refreshList": "Refresh List", "parameterInput.resetValue": "Reset Value", - "parameterInput.resourceLocator.mode.id": "By ID", - "parameterInput.resourceLocator.mode.url": "By URL", - "parameterInput.resourceLocator.mode.list": "From list", "parameterInput.select": "Select", "parameterInput.selectDateAndTime": "Select date and time", "parameterInput.selectACredentialTypeFromTheDropdown": "Select a credential type from the dropdown", @@ -677,6 +674,23 @@ "pushConnectionTracker.connectionLost": "Connection lost", "pushConnection.pollingNode.dataNotFound": "No {service} data found", "pushConnection.pollingNode.dataNotFound.message": "We didn’t find any data in {service} to simulate an event. Please create one in {service} and try again.", + "resourceLocator.id.placeholder": "Enter ID...", + "resourceLocator.mode.id": "By ID", + "resourceLocator.mode.url": "By URL", + "resourceLocator.mode.list": "From list", + "resourceLocator.mode.list.disabled.title": "Change to Fixed mode to choose From List", + "resourceLocator.mode.list.error.title": "Could not load list", + "resourceLocator.mode.list.error.description.part1": "Check that your", + "resourceLocator.mode.list.error.description.part2": "credential", + "resourceLocator.mode.list.error.description.part3": "is set up correctly", + "resourceLocator.mode.list.noResults": "No results", + "resourceLocator.mode.list.openUrl": "Open URL", + "resourceLocator.mode.list.placeholder": "Choose...", + "resourceLocator.mode.list.searchRequired": "Enter a search term to show results", + "resourceLocator.modeSelector.placeholder": "Mode...", + "resourceLocator.openResource": "Open {entity} in {appName}", + "resourceLocator.search.placeholder": "Search...", + "resourceLocator.url.placeholder": "Enter URL...", "runData.emptyItemHint": "This is an item, but it's empty.", "runData.emptyArray": "[empty array]", "runData.emptyString": "[empty]", diff --git a/packages/editor-ui/src/typeGuards.ts b/packages/editor-ui/src/typeGuards.ts new file mode 100644 index 0000000000000..2818cc871bc34 --- /dev/null +++ b/packages/editor-ui/src/typeGuards.ts @@ -0,0 +1,5 @@ +import { INodeParameterResourceLocator } from "n8n-workflow"; + +export function isResourceLocatorValue(value: unknown): value is INodeParameterResourceLocator { + return Boolean(typeof value === 'object' && value && 'mode' in value && 'mode' in value); +} diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 1eff378543b34..8c9bc998dbf24 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -239,6 +239,7 @@ import '../plugins/PlusEndpointType'; import { getAccountAge } from '@/modules/userHelpers'; import { IUser } from 'n8n-design-system'; import {dataPinningEventBus} from "@/event-bus/data-pinning-event-bus"; +import { debounceHelper } from '@/components/mixins/debounce'; interface AddNodeOptions { position?: XYPosition; @@ -258,6 +259,7 @@ export default mixins( workflowHelpers, workflowRun, newVersions, + debounceHelper, ) .extend({ name: 'NodeView', diff --git a/packages/editor-ui/src/views/TemplatesSearchView.vue b/packages/editor-ui/src/views/TemplatesSearchView.vue index ba2b24f40fb72..34f60b57a6edd 100644 --- a/packages/editor-ui/src/views/TemplatesSearchView.vue +++ b/packages/editor-ui/src/views/TemplatesSearchView.vue @@ -85,6 +85,7 @@ import { mapGetters } from 'vuex'; import { IDataObject } from 'n8n-workflow'; import { setPageTitle } from '@/components/helpers'; import { VIEWS } from '@/constants'; +import { debounceHelper } from '@/components/mixins/debounce'; interface ISearchEvent { search_string: string; @@ -94,7 +95,7 @@ interface ISearchEvent { wf_template_repo_session_id: number; } -export default mixins(genericHelpers).extend({ +export default mixins(genericHelpers, debounceHelper).extend({ name: 'TemplatesSearchView', components: { CollectionsCarousel, diff --git a/packages/nodes-base/nodes/Airtable/Airtable.node.ts b/packages/nodes-base/nodes/Airtable/Airtable.node.ts index 5536870d55cd7..8afca599c2268 100644 --- a/packages/nodes-base/nodes/Airtable/Airtable.node.ts +++ b/packages/nodes-base/nodes/Airtable/Airtable.node.ts @@ -119,7 +119,7 @@ export class Airtable implements INodeType { displayName: 'Base ID', name: 'applicationRLC', type: 'resourceLocator', - default: { mode: 'list', value: '' }, + default: { mode: 'url', value: '' }, required: true, displayOptions: { show: { @@ -130,43 +130,43 @@ export class Airtable implements INodeType { modes: [ // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'ID', - name: 'id', + displayName: 'By URL', + name: 'url', type: 'string', - hint: 'Enter base Id', + hint: 'Enter base URL', + placeholder: 'https://airtable.com/app12DiScdfes/tblAAAAAAAAAAAAA/viwHdfasdfeieg5p', validation: [ { type: 'regex', properties: { - regex: '[a-zA-Z0-9]+', - errorMessage: 'ID value cannot be empty', + regex: 'https://airtable.com/([a-zA-Z0-9]{2,})/.*', + errorMessage: + 'URL has to be in the format: https://airtable.com/[base ID]/[table ID]/.*', }, }, ], - placeholder: 'appD3dfaeidke', - url: '=https://airtable.com/{{$value}}', + extractValue: { + type: 'regex', + regex: 'https://airtable.com/([a-zA-Z0-9]{2,})', + }, }, // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'By URL', - name: 'url', + displayName: 'ID', + name: 'id', type: 'string', - hint: 'Enter base URL', - placeholder: 'https://airtable.com/app12DiScdfes/tblAAAAAAAAAAAAA/viwHdfasdfeieg5p', + hint: 'Enter base Id', validation: [ { type: 'regex', properties: { - regex: 'https://airtable.com/([a-zA-Z0-9]+)/[a-zA-Z0-9/]+', - errorMessage: - 'URL has to be in the format: https://airtable.com///', + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Id value must be alphanumeric and at least 2 characters', }, }, ], - extractValue: { - type: 'regex', - regex: 'https://airtable.com/([a-zA-Z0-9]+)', - }, + placeholder: 'appD3dfaeidke', + url: '=https://airtable.com/{{$value}}', }, ], }, @@ -174,7 +174,7 @@ export class Airtable implements INodeType { displayName: 'Table ID', name: 'tableRLC', type: 'resourceLocator', - default: { mode: 'list', value: '' }, + default: { mode: 'url', value: '' }, required: true, displayOptions: { show: { @@ -185,42 +185,42 @@ export class Airtable implements INodeType { modes: [ // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'ID', - name: 'id', + displayName: 'By URL', + name: 'url', type: 'string', - hint: 'Enter table Id', + hint: 'Enter table URL', + placeholder: 'https://airtable.com/app12DiScdfes/tblAAAAAAAAAAAAA/viwHdfasdfeieg5p', validation: [ { type: 'regex', properties: { - regex: '[a-zA-Z0-9]+', - errorMessage: 'ID value cannot be empty', + regex: 'https://airtable.com/[a-zA-Z0-9]{2,}/([a-zA-Z0-9]{2,})/.*', + errorMessage: + 'URL has to be in the format: https://airtable.com/[base ID]/[table ID]/.*', }, }, ], - placeholder: 'tbl3dirwqeidke', + extractValue: { + type: 'regex', + regex: 'https://airtable.com/[a-zA-Z0-9]{2,}/([a-zA-Z0-9]{2,})', + }, }, // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'By URL', - name: 'url', + displayName: 'ID', + name: 'id', type: 'string', - hint: 'Enter table URL', - placeholder: 'https://airtable.com/app12DiScdfes/tblAAAAAAAAAAAAA/viwHdfasdfeieg5p', + hint: 'Enter table Id', validation: [ { type: 'regex', properties: { - regex: 'https://airtable.com/[a-zA-Z0-9]+/([a-zA-Z0-9]+)', - errorMessage: - 'URL has to be in the format: https://airtable.com//
/', + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Id value must be alphanumeric and at least 2 characters', }, }, ], - extractValue: { - type: 'regex', - regex: 'https://airtable.com/[a-zA-Z0-9]+/([a-zA-Z0-9]+)', - }, + placeholder: 'tbl3dirwqeidke', }, ], }, diff --git a/packages/nodes-base/nodes/Google/Drive/v3/VersionDescription.ts b/packages/nodes-base/nodes/Google/Drive/v3/VersionDescription.ts index 04bd58e3f182f..b3500e01c99b4 100644 --- a/packages/nodes-base/nodes/Google/Drive/v3/VersionDescription.ts +++ b/packages/nodes-base/nodes/Google/Drive/v3/VersionDescription.ts @@ -205,16 +205,20 @@ export const versionDescription: INodeTypeDescription = { modes: [ // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'ID', - name: 'id', - type: 'string', - hint: 'ID of the file', - placeholder: 'File ID', + displayName: 'File', + name: 'list', + type: 'list', + hint: 'File to use', + placeholder: 'File', + typeOptions: { + searchListMethod: 'fileSearch', + searchable: true, + }, }, // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { displayName: 'Link', - name: 'link', + name: 'url', type: 'string', hint: 'Link to the file', placeholder: @@ -224,23 +228,38 @@ export const versionDescription: INodeTypeDescription = { regex: 'https:\\/\\/(?:drive|docs)\\.google.com\\/\\w+\\/(?:d|folders)\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)', }, + validation: [ + { + type: 'regex', + properties: { + regex: 'https:\\/\\/(?:drive|docs)\\.google.com\\/\\w+\\/(?:d|folders)\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)', + errorMessage: + 'URL has to be in the format: https:\\/\\/(?:drive|docs)\\.google.com\\/\\w+\\/(?:d|folders)\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)', + }, + }, + ], }, // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'File', - name: 'list', - type: 'list', - hint: 'File to use', - placeholder: 'File', - typeOptions: { - searchListMethod: 'fileSearch', - searchable: true, - }, + displayName: 'ID', + name: 'id', + type: 'string', + hint: 'ID of the file', + placeholder: 'File ID', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Id value must be alphanumeric and at least 2 characters', + }, + }, + ], }, ], displayOptions: { show: { - operation: ['download', 'copy', 'download', 'update', 'share', 'delete'], + operation: ['download', 'copy', 'download', 'update'], resource: ['file'], }, }, @@ -248,7 +267,7 @@ export const versionDescription: INodeTypeDescription = { }, { - displayName: 'File ID', + displayName: 'File/Folder ID', name: 'fileOrFolderId', type: 'resourceLocator', default: { mode: 'list', value: '' }, @@ -256,18 +275,22 @@ export const versionDescription: INodeTypeDescription = { modes: [ // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'ID', - name: 'id', - type: 'string', - hint: 'ID of the folder', - placeholder: 'Folder ID', + displayName: 'File/Folder', + name: 'list', + type: 'list', + hint: 'File/folder to use', + placeholder: 'File/folder', + typeOptions: { + searchListMethod: 'fileSearch', + searchable: true, + }, }, // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { displayName: 'Link', - name: 'link', + name: 'url', type: 'string', - hint: 'Link to the folder', + hint: 'Link to the file/folder', placeholder: 'https://docs.google.com/spreadsheets/d/1-i6Vx0NN-3333eeeeeeeeee333333333/edit', extractValue: { @@ -275,27 +298,42 @@ export const versionDescription: INodeTypeDescription = { regex: 'https:\\/\\/(?:drive|docs)\\.google.com\\/\\w+\\/(?:d|folders)\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)', }, + validation: [ + { + type: 'regex', + properties: { + regex: 'https:\\/\\/(?:drive|docs)\\.google.com\\/\\w+\\/(?:d|folders)\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)', + errorMessage: + 'URL has to be in the format: https:\\/\\/(?:drive|docs)\\.google.com\\/\\w+\\/(?:d|folders)\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)', + }, + }, + ], }, // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'File', - name: 'list', - type: 'list', - hint: 'File to use', - placeholder: 'File', - typeOptions: { - searchListMethod: 'fileSearch', - searchable: true, - }, + displayName: 'ID', + name: 'id', + type: 'string', + hint: 'ID of the file/folder', + placeholder: 'File/folder ID', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Id value must be alphanumeric and at least 2 characters', + }, + }, + ], }, ], displayOptions: { show: { operation: ['share', 'delete'], - resource: ['folder'], + resource: ['folder', 'file'], }, }, - description: 'The ID of the folder', + description: 'The ID of the file or folder', }, // ---------------------------------- @@ -1410,7 +1448,7 @@ export const versionDescription: INodeTypeDescription = { }, { - displayName: 'File', + displayName: 'Drive ID', name: 'driveId', type: 'resourceLocator', default: { mode: 'list', value: '' }, @@ -1418,16 +1456,20 @@ export const versionDescription: INodeTypeDescription = { modes: [ // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'ID', - name: 'id', - type: 'string', - hint: 'The ID of the shared drive', - placeholder: 'Drive ID', + displayName: 'Drive', + name: 'list', + type: 'list', + hint: 'Shared drive to use', + placeholder: 'Drive', + typeOptions: { + searchListMethod: 'driveSearch', + searchable: true, + }, }, // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { displayName: 'Link', - name: 'link', + name: 'url', type: 'string', hint: 'Link to the shared drive', placeholder: 'https://drive.google.com/drive/folders/0AaaaaAAAAAAAaa', @@ -1435,18 +1477,33 @@ export const versionDescription: INodeTypeDescription = { type: 'regex', regex: 'https:\\/\\/drive\\.google.com\\/\\w+\\/folders\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)', }, + validation: [ + { + type: 'regex', + properties: { + regex: 'https:\\/\\/drive\\.google.com\\/\\w+\\/folders\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)', + errorMessage: + 'URL has to be in the format: https:\\/\\/drive\\.google.com\\/\\w+\\/folders\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)', + }, + }, + ], }, // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'Drive', - name: 'list', - type: 'list', - hint: 'Shared drive to use', - placeholder: 'Drive', - typeOptions: { - searchListMethod: 'driveSearch', - searchable: true, - }, + displayName: 'ID', + name: 'id', + type: 'string', + hint: 'The ID of the shared drive', + placeholder: 'Drive ID', + validation: [ + { + type: 'regex', + properties: { + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Id value must be alphanumeric and at least 2 characters', + }, + }, + ], }, ], displayOptions: { @@ -1455,7 +1512,7 @@ export const versionDescription: INodeTypeDescription = { resource: ['drive'], }, }, - description: 'The ID of the file', + description: 'The ID of the drive', }, // ---------------------------------- diff --git a/packages/nodes-base/nodes/Trello/AttachmentDescription.ts b/packages/nodes-base/nodes/Trello/AttachmentDescription.ts index 535c95b323061..8568c8e3dbf29 100644 --- a/packages/nodes-base/nodes/Trello/AttachmentDescription.ts +++ b/packages/nodes-base/nodes/Trello/AttachmentDescription.ts @@ -67,43 +67,43 @@ export const attachmentFields: INodeProperties[] = [ }, // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'ID', - name: 'id', + displayName: 'By URL', + name: 'url', type: 'string', - hint: 'Enter Card Id', + hint: 'Enter Card URL', + placeholder: 'https://trello.com/c/e123456/card-name', validation: [ { type: 'regex', properties: { - regex: '[a-zA-Z0-9]+', - errorMessage: 'ID value cannot be empty', + regex: 'http(s)?://trello.com/c/([a-zA-Z0-9]{2,})/.*', + errorMessage: + 'URL has to be in the format: http(s)://trello.com/c/[card ID]/.*', }, }, ], - placeholder: 'wiIaGwqE', - url: '=https://trello.com/c/{{$value}}', + extractValue: { + type: 'regex', + regex: 'https://trello.com/c/([a-zA-Z0-9]{2,})', + }, }, // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'By URL', - name: 'url', + displayName: 'ID', + name: 'id', type: 'string', - hint: 'Enter Card URL', - placeholder: 'https://trello.com/c/e123456/card-name', + hint: 'Enter Card Id', validation: [ { type: 'regex', properties: { - regex: 'http(s)?://trello.com/c/([a-zA-Z0-9]+)/[a-zA-Z0-9]+', - errorMessage: - 'URL has to be in the format: http(s)://trello.com/c//', + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Id value must be alphanumeric and at least 2 characters', }, }, ], - extractValue: { - type: 'regex', - regex: 'https://trello.com/c/([a-zA-Z0-9]+)', - }, + placeholder: 'wiIaGwqE', + url: '=https://trello.com/c/{{$value}}', }, ], displayOptions: { diff --git a/packages/nodes-base/nodes/Trello/BoardDescription.ts b/packages/nodes-base/nodes/Trello/BoardDescription.ts index 8a5054be2e94f..4a3f9b55191af 100644 --- a/packages/nodes-base/nodes/Trello/BoardDescription.ts +++ b/packages/nodes-base/nodes/Trello/BoardDescription.ts @@ -326,43 +326,43 @@ export const boardFields: INodeProperties[] = [ }, // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'ID', - name: 'id', + displayName: 'By URL', + name: 'url', type: 'string', - hint: 'Enter Board Id', + hint: 'Enter board URL', + placeholder: 'https://trello.com/b/e123456/board-name', validation: [ { type: 'regex', properties: { - regex: '[a-zA-Z0-9]+', - errorMessage: 'ID value cannot be empty', + regex: 'http(s)?://trello.com/b/([a-zA-Z0-9]{2,})/.*', + errorMessage: + 'URL has to be in the format: http(s)://trello.com/b/[board ID]/.*', }, }, ], - placeholder: 'KdEAAdde', - url: '=https://trello.com/b/{{$value}}', + extractValue: { + type: 'regex', + regex: 'https://trello.com/b/([a-zA-Z0-9]{2,})', + }, }, // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'By URL', - name: 'url', + displayName: 'ID', + name: 'id', type: 'string', - hint: 'Enter board URL', - placeholder: 'https://trello.com/b/e123456/board-name', + hint: 'Enter Board Id', validation: [ { type: 'regex', properties: { - regex: 'http(s)?://trello.com/b/([a-zA-Z0-9]+)/[a-zA-Z0-9]+', - errorMessage: - 'URL has to be in the format: http(s)://trello.com/b//', + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Id value must be alphanumeric and at least 2 characters', }, }, ], - extractValue: { - type: 'regex', - regex: 'https://trello.com/b/([a-zA-Z0-9]+)', - }, + placeholder: 'KdEAAdde', + url: '=https://trello.com/b/{{$value}}', }, ], }, diff --git a/packages/nodes-base/nodes/Trello/CardCommentDescription.ts b/packages/nodes-base/nodes/Trello/CardCommentDescription.ts index 0652a526f4589..ccf4c6d366361 100644 --- a/packages/nodes-base/nodes/Trello/CardCommentDescription.ts +++ b/packages/nodes-base/nodes/Trello/CardCommentDescription.ts @@ -58,43 +58,43 @@ export const cardCommentFields: INodeProperties[] = [ }, // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'ID', - name: 'id', + displayName: 'By URL', + name: 'url', type: 'string', - hint: 'Enter Card Id', + hint: 'Enter Card URL', + placeholder: 'https://trello.com/c/e123456/card-name', validation: [ { type: 'regex', properties: { - regex: '[a-zA-Z0-9]+', - errorMessage: 'ID value cannot be empty', + regex: 'http(s)?://trello.com/c/([a-zA-Z0-9]{2,})/.*', + errorMessage: + 'URL has to be in the format: http(s)://trello.com/c/[card ID]/.*', }, }, ], - placeholder: 'wiIaGwqE', - url: '=https://trello.com/c/{{$value}}', + extractValue: { + type: 'regex', + regex: 'https://trello.com/c/([a-zA-Z0-9]{2,})', + }, }, // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'By URL', - name: 'url', + displayName: 'ID', + name: 'id', type: 'string', - hint: 'Enter Card URL', - placeholder: 'https://trello.com/c/e123456/card-name', + hint: 'Enter Card Id', validation: [ { type: 'regex', properties: { - regex: 'http(s)?://trello.com/c/([a-zA-Z0-9]+)/[a-zA-Z0-9]+', - errorMessage: - 'URL has to be in the format: http(s)://trello.com/c//', + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Id value must be alphanumeric and at least 2 characters', }, }, ], - extractValue: { - type: 'regex', - regex: 'https://trello.com/c/([a-zA-Z0-9]+)', - }, + placeholder: 'wiIaGwqE', + url: '=https://trello.com/c/{{$value}}', }, ], displayOptions: { diff --git a/packages/nodes-base/nodes/Trello/CardDescription.ts b/packages/nodes-base/nodes/Trello/CardDescription.ts index 15749acbb018a..2f345d5c61a2d 100644 --- a/packages/nodes-base/nodes/Trello/CardDescription.ts +++ b/packages/nodes-base/nodes/Trello/CardDescription.ts @@ -185,43 +185,43 @@ export const cardFields: INodeProperties[] = [ }, // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'ID', - name: 'id', + displayName: 'By URL', + name: 'url', type: 'string', - hint: 'Enter Card Id', + hint: 'Enter Card URL', + placeholder: 'https://trello.com/c/e123456/card-name', validation: [ { type: 'regex', properties: { - regex: '[a-zA-Z0-9]+', - errorMessage: 'ID value cannot be empty', + regex: 'http(s)?://trello.com/c/([a-zA-Z0-9]{2,})/.*', + errorMessage: + 'URL has to be in the format: http(s)://trello.com/c/[card ID]/.*', }, }, ], - placeholder: 'wiIaGwqE', - url: '=https://trello.com/c/{{$value}}', + extractValue: { + type: 'regex', + regex: 'https://trello.com/c/([a-zA-Z0-9]{2,})', + }, }, // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'By URL', - name: 'url', + displayName: 'ID', + name: 'id', type: 'string', - hint: 'Enter Card URL', - placeholder: 'https://trello.com/c/e123456/card-name', + hint: 'Enter Card Id', validation: [ { type: 'regex', properties: { - regex: 'http(s)?://trello.com/c/([a-zA-Z0-9]+)/[a-zA-Z0-9]+', - errorMessage: - 'URL has to be in the format: http(s)://trello.com/c//', + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Id value must be alphanumeric and at least 2 characters', }, }, ], - extractValue: { - type: 'regex', - regex: 'https://trello.com/c/([a-zA-Z0-9]+)', - }, + placeholder: 'wiIaGwqE', + url: '=https://trello.com/c/{{$value}}', }, ], displayOptions: { diff --git a/packages/nodes-base/nodes/Trello/ChecklistDescription.ts b/packages/nodes-base/nodes/Trello/ChecklistDescription.ts index 9ffe499161fae..9cd2acafa37c4 100644 --- a/packages/nodes-base/nodes/Trello/ChecklistDescription.ts +++ b/packages/nodes-base/nodes/Trello/ChecklistDescription.ts @@ -97,43 +97,43 @@ export const checklistFields: INodeProperties[] = [ }, // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'ID', - name: 'id', + displayName: 'By URL', + name: 'url', type: 'string', - hint: 'Enter Card Id', + hint: 'Enter Card URL', + placeholder: 'https://trello.com/c/e123456/card-name', validation: [ { type: 'regex', properties: { - regex: '[a-zA-Z0-9]+', - errorMessage: 'ID value cannot be empty', + regex: 'http(s)?://trello.com/c/([a-zA-Z0-9]{2,})/.*', + errorMessage: + 'URL has to be in the format: http(s)://trello.com/c/[card ID]/.*', }, }, ], - placeholder: 'wiIaGwqE', - url: '=https://trello.com/c/{{$value}}', + extractValue: { + type: 'regex', + regex: 'https://trello.com/c/([a-zA-Z0-9]{2,})', + }, }, // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'By URL', - name: 'url', + displayName: 'ID', + name: 'id', type: 'string', - hint: 'Enter Card URL', - placeholder: 'https://trello.com/c/e123456/card-name', + hint: 'Enter Card Id', validation: [ { type: 'regex', properties: { - regex: 'http(s)?://trello.com/c/([a-zA-Z0-9]+)/[a-zA-Z0-9]+', - errorMessage: - 'URL has to be in the format: http(s)://trello.com/c//', + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Id value must be alphanumeric and at least 2 characters', }, }, ], - extractValue: { - type: 'regex', - regex: 'https://trello.com/c/([a-zA-Z0-9]+)', - }, + placeholder: 'wiIaGwqE', + url: '=https://trello.com/c/{{$value}}', }, ], displayOptions: { diff --git a/packages/nodes-base/nodes/Trello/LabelDescription.ts b/packages/nodes-base/nodes/Trello/LabelDescription.ts index 7a995ab9157b7..3e69dfc159028 100644 --- a/packages/nodes-base/nodes/Trello/LabelDescription.ts +++ b/packages/nodes-base/nodes/Trello/LabelDescription.ts @@ -95,43 +95,43 @@ export const labelFields: INodeProperties[] = [ }, // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'ID', - name: 'id', + displayName: 'By URL', + name: 'url', type: 'string', - hint: 'Enter Board Id', + hint: 'Enter board URL', + placeholder: 'https://trello.com/b/e123456/board-name', validation: [ { type: 'regex', properties: { - regex: '[a-zA-Z0-9]+', - errorMessage: 'ID value cannot be empty', + regex: 'http(s)?://trello.com/b/([a-zA-Z0-9]{2,})/.*', + errorMessage: + 'URL has to be in the format: http(s)://trello.com/b/[board ID]/.*', }, }, ], - placeholder: 'KdEAAdde', - url: '=https://trello.com/b/{{$value}}', + extractValue: { + type: 'regex', + regex: 'https://trello.com/b/([a-zA-Z0-9]{2,})', + }, }, // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'By URL', - name: 'url', + displayName: 'ID', + name: 'id', type: 'string', - hint: 'Enter board URL', - placeholder: 'https://trello.com/b/e123456/board-name', + hint: 'Enter Board Id', validation: [ { type: 'regex', properties: { - regex: 'http(s)?://trello.com/b/([a-zA-Z0-9]+)/[a-zA-Z0-9]+', - errorMessage: - 'URL has to be in the format: http(s)://trello.com/b//', + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Id value must be alphanumeric and at least 2 characters', }, }, ], - extractValue: { - type: 'regex', - regex: 'https://trello.com/b/([a-zA-Z0-9]+)', - }, + placeholder: 'KdEAAdde', + url: '=https://trello.com/b/{{$value}}', }, ], }, @@ -351,43 +351,43 @@ export const labelFields: INodeProperties[] = [ }, // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'ID', - name: 'id', + displayName: 'By URL', + name: 'url', type: 'string', - hint: 'Enter Card Id', + hint: 'Enter Card URL', + placeholder: 'https://trello.com/c/e123456/card-name', validation: [ { type: 'regex', properties: { - regex: '[a-zA-Z0-9]+', - errorMessage: 'ID value cannot be empty', + regex: 'http(s)?://trello.com/c/([a-zA-Z0-9]{2,})/.*', + errorMessage: + 'URL has to be in the format: http(s)://trello.com/c/[card ID]/.*', }, }, ], - placeholder: 'wiIaGwqE', - url: '=https://trello.com/c/{{$value}}', + extractValue: { + type: 'regex', + regex: 'https://trello.com/c/([a-zA-Z0-9]{2,})', + }, }, // eslint-disable-next-line n8n-nodes-base/node-param-default-missing { - displayName: 'By URL', - name: 'url', + displayName: 'ID', + name: 'id', type: 'string', - hint: 'Enter Card URL', - placeholder: 'https://trello.com/c/e123456/card-name', + hint: 'Enter Card Id', validation: [ { type: 'regex', properties: { - regex: 'http(s)?://trello.com/c/([a-zA-Z0-9]+)/[a-zA-Z0-9]+', - errorMessage: - 'URL has to be in the format: http(s)://trello.com/c//', + regex: '[a-zA-Z0-9]{2,}', + errorMessage: 'Id value must be alphanumeric and at least 2 characters', }, }, ], - extractValue: { - type: 'regex', - regex: 'https://trello.com/c/([a-zA-Z0-9]+)', - }, + placeholder: 'wiIaGwqE', + url: '=https://trello.com/c/{{$value}}', }, ], displayOptions: { diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index de9a3a54acaff..9116e7685435e 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -889,10 +889,17 @@ export interface INodeExecuteFunctions { export type NodeParameterValue = string | number | boolean | undefined | null; export type ResourceLocatorModes = 'id' | 'url' | 'list' | string; +export interface IResourceLocatorResult { + name: string; + value: string; + url?: string; +} export interface INodeParameterResourceLocator { mode: ResourceLocatorModes; value: NodeParameterValue; + cachedResultName?: string; + cachedResultUrl?: string; } export type NodeParameterValueType = @@ -1001,7 +1008,7 @@ export interface INodePropertyMode { validation?: Array< INodePropertyModeValidation | { (this: IExecuteSingleFunctions, value: string): void } >; - placeholder: string; + placeholder?: string; url?: string; extractValue?: INodePropertyValueExtractor; initType?: string; @@ -1042,10 +1049,8 @@ export interface INodePropertyOptions { } export interface INodeListSearchItems extends INodePropertyOptions { - breadcrumb?: string[]; icon?: string; url?: string; - disabled?: boolean; } export interface INodeListSearchResult { diff --git a/packages/workflow/src/NodeHelpers.ts b/packages/workflow/src/NodeHelpers.ts index 371fdfbd9edb5..8a3332c413e63 100644 --- a/packages/workflow/src/NodeHelpers.ts +++ b/packages/workflow/src/NodeHelpers.ts @@ -23,9 +23,13 @@ import { INodeExecutionData, INodeIssueObjectProperty, INodeIssues, + INodeParameterResourceLocator, INodeParameters, INodeProperties, INodePropertyCollection, + INodePropertyMode, + INodePropertyModeValidation, + INodePropertyRegexValidation, INodeType, INodeVersionedType, IParameterDependencies, @@ -1124,6 +1128,34 @@ export function nodeIssuesToString(issues: INodeIssues, node?: INode): string[] return nodeIssues; } +/* + * Validates resource locator node parameters based on validation ruled defined in each parameter mode + * +*/ +export const validateResourceLocatorParameter = (value: INodeParameterResourceLocator, parameterMode: INodePropertyMode) : string[] => { + const valueToValidate = (value && value.value && value.value.toString()) || ''; + if (valueToValidate.startsWith('=')) { + return []; + } + + const validationErrors: string[] = []; + // Each mode can have multiple validations specified + if (parameterMode.validation) { + for (const validation of parameterMode.validation) { + if (validation && (validation as INodePropertyModeValidation).type === 'regex') { + const regexValidation = validation as INodePropertyRegexValidation; + const regex = new RegExp(`^${regexValidation.properties.regex}$`); + + if (!regex.test(valueToValidate)) { + validationErrors.push(regexValidation.properties.errorMessage); + } + } + } + } + + return validationErrors; +}; + /** * Adds an issue if the parameter is not defined * @@ -1135,14 +1167,15 @@ export function nodeIssuesToString(issues: INodeIssues, node?: INode): string[] export function addToIssuesIfMissing( foundIssues: INodeIssues, nodeProperties: INodeProperties, - value: NodeParameterValue, + value: NodeParameterValue | INodeParameterResourceLocator, ) { // TODO: Check what it really has when undefined if ( (nodeProperties.type === 'string' && (value === '' || value === undefined)) || (nodeProperties.type === 'multiOptions' && Array.isArray(value) && value.length === 0) || (nodeProperties.type === 'dateTime' && value === undefined) || - (nodeProperties.type === 'options' && (value === '' || value === undefined)) + (nodeProperties.type === 'options' && (value === '' || value === undefined)) || + (nodeProperties.type === 'resourceLocator' && (!value || (typeof value === 'object' && !value.value))) ) { // Parameter is required but empty if (foundIssues.parameters === undefined) { @@ -1175,6 +1208,10 @@ export function getParameterValueByPath( return get(nodeValues, path ? `${path}.${parameterName}` : parameterName); } +function isINodeParameterResourceLocator(value: any): value is INodeParameterResourceLocator { + return typeof value === 'object' && value !== null && 'value' in value && 'mode' in value; +} + /** * Returns all the issues with the given node-values * @@ -1190,12 +1227,11 @@ export function getParameterIssues( path: string, node: INode, ): INodeIssues { - const foundIssues: INodeIssues = {}; - let value; + const foundIssues: INodeIssues = {}; if (nodeProperties.required === true) { if (displayParameterPath(nodeValues, nodeProperties, path, node)) { - value = getParameterValueByPath(nodeValues, nodeProperties.name, path); + const value = getParameterValueByPath(nodeValues, nodeProperties.name, path); if ( // eslint-disable-next-line @typescript-eslint/prefer-optional-chain @@ -1215,6 +1251,30 @@ export function getParameterIssues( } } + if (nodeProperties.type === 'resourceLocator') { + if (displayParameterPath(nodeValues, nodeProperties, path, node)) { + const value = getParameterValueByPath(nodeValues, nodeProperties.name, path); + if (isINodeParameterResourceLocator(value)) { + const mode = nodeProperties.modes && nodeProperties.modes.find((option) => option.name === value.mode); + if (mode) { + const errors = validateResourceLocatorParameter(value, mode); + errors.forEach((error) => { + if (foundIssues.parameters === undefined) { + foundIssues.parameters = {}; + } + if (foundIssues.parameters[nodeProperties.name] === undefined) { + foundIssues.parameters[nodeProperties.name] = []; + } + + foundIssues.parameters[nodeProperties.name].push( + error, + ); + }); + } + } + } + } + // Check if there are any child parameters if (nodeProperties.options === undefined) { // There are none so nothing else to check @@ -1250,7 +1310,7 @@ export function getParameterIssues( let propertyOptions: INodePropertyCollection; for (propertyOptions of nodeProperties.options as INodePropertyCollection[]) { // Check if the option got set and if not skip it - value = getParameterValueByPath(nodeValues, propertyOptions.name, basePath.slice(0, -1)); + const value = getParameterValueByPath(nodeValues, propertyOptions.name, basePath.slice(0, -1)); if (value === undefined) { continue; } diff --git a/packages/workflow/src/RoutingNode.ts b/packages/workflow/src/RoutingNode.ts index d18ab0b7a00b1..6cbc35ef20810 100644 --- a/packages/workflow/src/RoutingNode.ts +++ b/packages/workflow/src/RoutingNode.ts @@ -587,7 +587,7 @@ export class RoutingNode { executeData: IExecuteData, additionalKeys?: IWorkflowDataProxyAdditionalKeys, returnObjectAsString = false, - ): NodeParameterValueType | string { + ): NodeParameterValueType { if ( typeof parameterValue === 'object' || (typeof parameterValue === 'string' && parameterValue.charAt(0) === '=')