From 095ce93cf774528b264bb57a654ef302c84a32b0 Mon Sep 17 00:00:00 2001 From: pemontto Date: Sun, 17 Oct 2021 17:48:24 +0100 Subject: [PATCH 001/315] =?UTF-8?q?=E2=9C=A8=20Add=20loadOptions=20for=20H?= =?UTF-8?q?ome=20Assistant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HomeAssistant/CameraProxyDescription.ts | 5 +- .../nodes/HomeAssistant/GenericFunctions.ts | 49 ++++++++++++++++++- .../nodes/HomeAssistant/HomeAssistant.node.ts | 26 ++++++++++ .../nodes/HomeAssistant/ServiceDescription.ts | 13 ++++- .../nodes/HomeAssistant/StateDescription.ts | 10 +++- 5 files changed, 96 insertions(+), 7 deletions(-) diff --git a/packages/nodes-base/nodes/HomeAssistant/CameraProxyDescription.ts b/packages/nodes-base/nodes/HomeAssistant/CameraProxyDescription.ts index 35764bfc2a..3f28c6dc9a 100644 --- a/packages/nodes-base/nodes/HomeAssistant/CameraProxyDescription.ts +++ b/packages/nodes-base/nodes/HomeAssistant/CameraProxyDescription.ts @@ -33,7 +33,10 @@ export const cameraProxyFields = [ { displayName: 'Camera Entity ID', name: 'cameraEntityId', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCameraEntities', + }, default: '', required: true, displayOptions: { diff --git a/packages/nodes-base/nodes/HomeAssistant/GenericFunctions.ts b/packages/nodes-base/nodes/HomeAssistant/GenericFunctions.ts index 796b03ee51..673caab7c3 100644 --- a/packages/nodes-base/nodes/HomeAssistant/GenericFunctions.ts +++ b/packages/nodes-base/nodes/HomeAssistant/GenericFunctions.ts @@ -4,15 +4,17 @@ import { import { IExecuteFunctions, + ILoadOptionsFunctions, } from 'n8n-core'; import { IDataObject, + INodePropertyOptions, NodeApiError, NodeOperationError, } from 'n8n-workflow'; -export async function homeAssistantApiRequest(this: IExecuteFunctions, method: string, resource: string, body: IDataObject = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}) { +export async function homeAssistantApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions, method: string, resource: string, body: IDataObject = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}) { const credentials = await this.getCredentials('homeAssistantApi'); if (credentials === undefined) { @@ -35,8 +37,51 @@ export async function homeAssistantApiRequest(this: IExecuteFunctions, method: s delete options.body; } try { - return await this.helpers.request(options); + if (this.helpers.request) { + return await this.helpers.request(options); + } } catch (error) { throw new NodeApiError(this.getNode(), error); } } + +export async function getHomeAssistantEntities(this: IExecuteFunctions | ILoadOptionsFunctions, domain = '') { + const returnData: INodePropertyOptions[] = []; + const entities = await homeAssistantApiRequest.call(this, 'GET', '/states'); + for (const entity of entities) { + const entityId = entity.entity_id as string; + if (domain === '' || domain && entityId.startsWith(domain)) { + const entityName = entity.attributes.friendly_name as string || entityId; + returnData.push({ + name: entityName, + value: entityId, + }); + } + } + return returnData; +} + +export async function getHomeAssistantServices(this: IExecuteFunctions | ILoadOptionsFunctions, domain = '') { + const returnData: INodePropertyOptions[] = []; + const services = await homeAssistantApiRequest.call(this, 'GET', '/services'); + if (domain === '') { + // If no domain specified return domains + const domains = services.map(({ domain }: IDataObject) => domain as string).sort(); + returnData.push(...domains.map((service: string) => ({ name: service, value: service }))); + return returnData; + } else { + // If we have a domain, return all relevant services + const domainServices = services.filter((service: IDataObject) => service.domain === domain); + for (const domainService of domainServices) { + for (const [serviceID, value] of Object.entries(domainService.services)) { + const serviceProperties = value as IDataObject; + const serviceName = serviceProperties.description || serviceID; + returnData.push({ + name: serviceName as string, + value: serviceID, + }); + } + } + } + return returnData; +} diff --git a/packages/nodes-base/nodes/HomeAssistant/HomeAssistant.node.ts b/packages/nodes-base/nodes/HomeAssistant/HomeAssistant.node.ts index d84525f159..008ebf5680 100644 --- a/packages/nodes-base/nodes/HomeAssistant/HomeAssistant.node.ts +++ b/packages/nodes-base/nodes/HomeAssistant/HomeAssistant.node.ts @@ -4,7 +4,9 @@ import { import { IDataObject, + ILoadOptionsFunctions, INodeExecutionData, + INodePropertyOptions, INodeType, INodeTypeDescription, } from 'n8n-workflow'; @@ -49,6 +51,8 @@ import { } from './CameraProxyDescription'; import { + getHomeAssistantEntities, + getHomeAssistantServices, homeAssistantApiRequest, } from './GenericFunctions'; @@ -133,6 +137,28 @@ export class HomeAssistant implements INodeType { ], }; + methods = { + loadOptions: { + async getAllEntities(this: ILoadOptionsFunctions): Promise { + return await getHomeAssistantEntities.call(this); + }, + async getCameraEntities(this: ILoadOptionsFunctions): Promise { + return await getHomeAssistantEntities.call(this, 'camera'); + }, + async getDomains(this: ILoadOptionsFunctions): Promise { + return await getHomeAssistantServices.call(this); + }, + async getDomainServices(this: ILoadOptionsFunctions): Promise { + const currentDomain = this.getCurrentNodeParameter('domain') as string; + if (currentDomain) { + return await getHomeAssistantServices.call(this, currentDomain); + } else { + return []; + } + }, + }, + }; + async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: IDataObject[] = []; diff --git a/packages/nodes-base/nodes/HomeAssistant/ServiceDescription.ts b/packages/nodes-base/nodes/HomeAssistant/ServiceDescription.ts index d278d47227..100bbff1fb 100644 --- a/packages/nodes-base/nodes/HomeAssistant/ServiceDescription.ts +++ b/packages/nodes-base/nodes/HomeAssistant/ServiceDescription.ts @@ -83,7 +83,10 @@ export const serviceFields = [ { displayName: 'Domain', name: 'domain', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDomains', + }, default: '', required: true, displayOptions: { @@ -100,7 +103,13 @@ export const serviceFields = [ { displayName: 'Service', name: 'service', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'domain', + ], + loadOptionsMethod: 'getDomainServices', + }, default: '', required: true, displayOptions: { diff --git a/packages/nodes-base/nodes/HomeAssistant/StateDescription.ts b/packages/nodes-base/nodes/HomeAssistant/StateDescription.ts index aa356f1c7a..580e1fa292 100644 --- a/packages/nodes-base/nodes/HomeAssistant/StateDescription.ts +++ b/packages/nodes-base/nodes/HomeAssistant/StateDescription.ts @@ -43,7 +43,10 @@ export const stateFields = [ { displayName: 'Entity ID', name: 'entityId', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getAllEntities', + }, displayOptions: { show: { operation: [ @@ -110,7 +113,10 @@ export const stateFields = [ { displayName: 'Entity ID', name: 'entityId', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getAllEntities', + }, displayOptions: { show: { operation: [ From 119989bc3760b9a8b1a1e57bfa25c23e657e647a Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 26 Oct 2021 11:32:54 -0500 Subject: [PATCH 002/315] :zap: Add support for moment types to If Node --- packages/nodes-base/nodes/If.node.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/nodes-base/nodes/If.node.ts b/packages/nodes-base/nodes/If.node.ts index 2a2a3bd0bd..5a89f07a10 100644 --- a/packages/nodes-base/nodes/If.node.ts +++ b/packages/nodes-base/nodes/If.node.ts @@ -1,3 +1,4 @@ +import moment = require('moment'); import { IExecuteFunctions } from 'n8n-core'; import { INodeExecutionData, @@ -333,6 +334,8 @@ export class If implements INodeType { returnValue = new Date(value).getTime(); } else if (typeof value === 'number') { returnValue = value; + } if (moment.isMoment(value)) { + returnValue = value.unix(); } if ((value as unknown as object) instanceof Date) { returnValue = (value as unknown as Date).getTime(); } From dc642419df5780aa6adcccf7760a697ccf6d51f7 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 26 Oct 2021 11:32:33 -0500 Subject: [PATCH 003/315] :zap: Make sure that DateTime Node always returns strings --- packages/nodes-base/nodes/DateTime.node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/DateTime.node.ts b/packages/nodes-base/nodes/DateTime.node.ts index df586997fd..89e9def2c4 100644 --- a/packages/nodes-base/nodes/DateTime.node.ts +++ b/packages/nodes-base/nodes/DateTime.node.ts @@ -496,7 +496,7 @@ export class DateTime implements INodeType { newItem.binary = item.binary; } - set(newItem, `json.${dataPropertyName}`, newDate); + set(newItem, `json.${dataPropertyName}`, newDate.toISOString()); returnData.push(newItem); } From db738fc824ef695af6b522a86091a9594d7e9e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 27 Oct 2021 01:54:03 +0200 Subject: [PATCH 004/315] :zap: Add workflow name and ID to settings (#2369) * :zap: Add workflow name and ID to settings * :hammer: Refactor to use mapGetters --- packages/editor-ui/src/components/WorkflowSettings.vue | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/editor-ui/src/components/WorkflowSettings.vue b/packages/editor-ui/src/components/WorkflowSettings.vue index 77e929b177..04c7d218dc 100644 --- a/packages/editor-ui/src/components/WorkflowSettings.vue +++ b/packages/editor-ui/src/components/WorkflowSettings.vue @@ -3,7 +3,7 @@ :name="WORKFLOW_SETTINGS_MODAL_KEY" width="65%" maxHeight="80%" - title="Workflow Settings" + :title="`Settings for ${workflowName} (#${workflowId})`" :eventBus="modalBus" :scrollable="true" > @@ -191,6 +191,8 @@ import { WORKFLOW_SETTINGS_MODAL_KEY } from '../constants'; import mixins from 'vue-typed-mixins'; +import { mapGetters } from "vuex"; + export default mixins( externalHooks, genericHelpers, @@ -235,6 +237,11 @@ export default mixins( WORKFLOW_SETTINGS_MODAL_KEY, }; }, + + computed: { + ...mapGetters(['workflowName', 'workflowId']), + }, + async mounted () { if (this.$route.params.name === undefined) { this.$showMessage({ From 15d05c7f0130f6ed2cf3c454d34d18f9cf963477 Mon Sep 17 00:00:00 2001 From: Omar Ajoue Date: Wed, 27 Oct 2021 01:58:08 +0200 Subject: [PATCH 005/315] :zap: Fixed ignore response code flag to work properly with return full response (#2370) --- packages/core/src/NodeExecuteFunctions.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 9aa1cf3e8d..6ef54f37ac 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -438,17 +438,17 @@ async function proxyRequestToAxios( } }) .catch((error) => { - if (configObject.simple === true && error.response) { - resolve({ - body: error.response.data, - headers: error.response.headers, - statusCode: error.response.status, - statusMessage: error.response.statusText, - }); - return; - } if (configObject.simple === false && error.response) { - resolve(error.response.data); + if (configObject.resolveWithFullResponse) { + resolve({ + body: error.response.data, + headers: error.response.headers, + statusCode: error.response.status, + statusMessage: error.response.statusText, + }); + } else { + resolve(error.response.data); + } return; } From 3e1fb3e0c9f7d2588ef64fc77b0ac90159e39b3e Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Tue, 26 Oct 2021 23:45:26 -0400 Subject: [PATCH 006/315] :zap: Add search filters to contacts and companies (AgileCRM) (#2373) * Added search options for the AgileCRM node * Adjusting AgileCRM getAll operation (using Magento 2 node as a reference) * :zap: Small improvements to #2238 Co-authored-by: Valentina --- .../nodes/AgileCrm/AgileCrm.node.ts | 105 ++++-- .../nodes/AgileCrm/CompanyDescription.ts | 277 +++++++++++++++- .../nodes/AgileCrm/ContactDescription.ts | 311 ++++++++++++++++-- .../nodes/AgileCrm/DealDescription.ts | 2 - .../nodes/AgileCrm/FilterInterface.ts | 19 ++ .../nodes/AgileCrm/GenericFunctions.ts | 86 ++++- 6 files changed, 745 insertions(+), 55 deletions(-) create mode 100644 packages/nodes-base/nodes/AgileCrm/FilterInterface.ts diff --git a/packages/nodes-base/nodes/AgileCrm/AgileCrm.node.ts b/packages/nodes-base/nodes/AgileCrm/AgileCrm.node.ts index c3401f7d86..a31fdb2164 100644 --- a/packages/nodes-base/nodes/AgileCrm/AgileCrm.node.ts +++ b/packages/nodes-base/nodes/AgileCrm/AgileCrm.node.ts @@ -1,4 +1,7 @@ -import { IExecuteFunctions } from 'n8n-core'; +import { + IExecuteFunctions, +} from 'n8n-core'; + import { IDataObject, INodeExecutionData, @@ -22,16 +25,34 @@ import { dealOperations } from './DealDescription'; -import { IContact, IContactUpdate } from './ContactInterface'; -import { agileCrmApiRequest, agileCrmApiRequestUpdate, validateJSON } from './GenericFunctions'; -import { IDeal } from './DealInterface'; +import { + IContact, + IContactUpdate, +} from './ContactInterface'; +import { + agileCrmApiRequest, agileCrmApiRequestAllItems, + agileCrmApiRequestUpdate, + getFilterRules, + simplifyResponse, + validateJSON, +} from './GenericFunctions'; + +import { + IDeal, +} from './DealInterface'; + +import { + IFilter, + ISearchConditions, +} from './FilterInterface'; export class AgileCrm implements INodeType { description: INodeTypeDescription = { displayName: 'Agile CRM', name: 'agileCrm', icon: 'file:agilecrm.png', + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', group: ['transform'], version: 1, description: 'Consume Agile CRM API', @@ -86,7 +107,6 @@ export class AgileCrm implements INodeType { }; - async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); @@ -113,26 +133,59 @@ export class AgileCrm implements INodeType { responseData = await agileCrmApiRequest.call(this, 'DELETE', endpoint, {}); } else if (operation === 'getAll') { - const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const simple = this.getNodeParameter('simple', 0) as boolean; + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + const filterType = this.getNodeParameter('filterType', i) as string; + const sort = this.getNodeParameter('options.sort.sort', i, {}) as { direction: string, field: string }; + const body: IDataObject = {}; + const filterJson: IFilter = {}; + let contactType = ''; if (resource === 'contact') { - if (returnAll) { - const endpoint = 'api/contacts'; - responseData = await agileCrmApiRequest.call(this, 'GET', endpoint, {}); - } else { - const limit = this.getNodeParameter('limit', i) as number; - const endpoint = `api/contacts?page_size=${limit}`; - responseData = await agileCrmApiRequest.call(this, 'GET', endpoint, {}); - } + contactType = 'PERSON'; } else { - if (returnAll) { - const endpoint = 'api/contacts/companies/list'; - responseData = await agileCrmApiRequest.call(this, 'POST', endpoint, {}); + contactType = 'COMPANY'; + } + filterJson.contact_type = contactType; + + if (filterType === 'manual') { + const conditions = this.getNodeParameter('filters.conditions', i, []) as ISearchConditions[]; + const matchType = this.getNodeParameter('matchType', i) as string; + let rules; + if (conditions.length !== 0) { + rules = getFilterRules(conditions, matchType); + Object.assign(filterJson, rules); } else { - const limit = this.getNodeParameter('limit', i) as number; - const endpoint = `api/contacts/companies/list?page_size=${limit}`; - responseData = await agileCrmApiRequest.call(this, 'POST', endpoint, {}); + throw new NodeOperationError(this.getNode(), 'At least one condition must be added.'); } + } else if (filterType === 'json') { + const filterJsonRules = this.getNodeParameter('filterJson', i) as string; + if (validateJSON(filterJsonRules) !== undefined) { + Object.assign(filterJson, JSON.parse(filterJsonRules) as IFilter); + } else { + throw new NodeOperationError(this.getNode(), 'Filter (JSON) must be a valid json'); + } + } + body.filterJson = JSON.stringify(filterJson); + + if (sort) { + if (sort.direction === 'ASC') { + body.global_sort_key = sort.field; + } else if (sort.direction === 'DESC') { + body.global_sort_key = `-${sort.field}`; + } + } + + if (returnAll) { + body.page_size = 100; + responseData = await agileCrmApiRequestAllItems.call(this, 'POST', `api/filters/filter/dynamic-filter`, body, undefined, undefined, true); + } else { + body.page_size = this.getNodeParameter('limit', 0) as number; + responseData = await agileCrmApiRequest.call(this, 'POST', `api/filters/filter/dynamic-filter`, body, undefined, undefined, true); + } + + if (simple) { + responseData = simplifyResponse(responseData); } } else if (operation === 'create') { @@ -461,15 +514,15 @@ export class AgileCrm implements INodeType { responseData = await agileCrmApiRequest.call(this, 'DELETE', endpoint, {}); } else if (operation === 'getAll') { - const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + const endpoint = 'api/opportunity'; if (returnAll) { - const endpoint = 'api/opportunity'; - responseData = await agileCrmApiRequest.call(this, 'GET', endpoint, {}); + const limit = 100; + responseData = await agileCrmApiRequestAllItems.call(this, 'GET', endpoint, undefined, { page_size: limit }); } else { - const limit = this.getNodeParameter('limit', i) as number; - const endpoint = `api/opportunity?page_size=${limit}`; - responseData = await agileCrmApiRequest.call(this, 'GET', endpoint, {}); + const limit = this.getNodeParameter('limit', 0) as number; + responseData = await agileCrmApiRequest.call(this, 'GET', endpoint, undefined, { page_size: limit }); } } else if (operation === 'create') { diff --git a/packages/nodes-base/nodes/AgileCrm/CompanyDescription.ts b/packages/nodes-base/nodes/AgileCrm/CompanyDescription.ts index da5eeef238..6b888cf4ee 100644 --- a/packages/nodes-base/nodes/AgileCrm/CompanyDescription.ts +++ b/packages/nodes-base/nodes/AgileCrm/CompanyDescription.ts @@ -1,6 +1,7 @@ import { INodeProperties, } from 'n8n-workflow'; + export const companyOperations = [ { displayName: 'Operation', @@ -44,6 +45,7 @@ export const companyOperations = [ description: 'The operation to perform.', }, ] as INodeProperties[]; + export const companyFields = [ /* -------------------------------------------------------------------------- */ /* company:get */ @@ -91,7 +93,6 @@ export const companyFields = [ displayName: 'Limit', name: 'limit', type: 'number', - default: 20, displayOptions: { show: { resource: [ @@ -105,7 +106,280 @@ export const companyFields = [ ], }, }, + default: 20, + description: 'Number of results to fetch.', }, + + { + displayName: 'Filter', + name: 'filterType', + type: 'options', + options: [ + { + name: 'None', + value: 'none', + }, + { + name: 'Build Manually', + value: 'manual', + }, + { + name: 'JSON', + value: 'json', + }, + ], + displayOptions: { + show: { + resource: [ + 'company', + ], + operation: [ + 'getAll', + ], + }, + }, + default: 'none', + }, + { + displayName: 'Must Match', + name: 'matchType', + type: 'options', + options: [ + { + name: 'Any filter', + value: 'anyFilter', + }, + { + name: 'All Filters', + value: 'allFilters', + }, + ], + displayOptions: { + show: { + resource: [ + 'company', + ], + operation: [ + 'getAll', + ], + filterType: [ + 'manual', + ], + }, + }, + default: 'anyFilter', + }, + { + displayName: 'Simplify Response', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'company', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'Return a simplified version of the response instead of the raw data.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + resource: [ + 'company', + ], + operation: [ + 'getAll', + ], + filterType: [ + 'manual', + ], + }, + }, + default: '', + placeholder: 'Add Condition', + options: [ + { + displayName: 'Conditions', + name: 'conditions', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'string', + default: '', + description: 'Any searchable field.', + }, + { + displayName: 'Condition Type', + name: 'condition_type', + type: 'options', + options: [ + { + name: 'Equals', + value: 'EQUALS', + }, + { + name: 'Not Equal', + value: 'NOTEQUALS', + }, + { + name: 'Last', + value: 'LAST', + }, + { + name: 'Between', + value: 'BETWEEN', + }, + { + name: 'On', + value: 'ON', + }, + { + name: 'Before', + value: 'BEFORE', + }, + { + name: 'After', + value: 'AFTER', + }, + ], + default: 'EQUALS', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + }, + { + displayName: 'Value 2', + name: 'value2', + type: 'string', + displayOptions: { + show: { + condition_type: [ + 'BETWEEN', + ], + }, + }, + default: '', + }, + ], + }, + ], + }, + { + displayName: 'See Agile CRM guide to creating filters', + name: 'jsonNotice', + type: 'notice', + displayOptions: { + show: { + resource: [ + 'company', + ], + operation: [ + 'getAll', + ], + filterType: [ + 'json', + ], + }, + }, + default: '', + }, + { + displayName: 'Filters (JSON)', + name: 'filterJson', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + resource: [ + 'company', + ], + operation: [ + 'getAll', + ], + filterType: [ + 'json', + ], + }, + }, + default: '', + description: '', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'company', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Sort', + name: 'sort', + type: 'fixedCollection', + placeholder: 'Add Sort', + default: [], + options: [ + { + displayName: 'Sort', + name: 'sort', + values: [ + { + displayName: 'Direction', + name: 'direction', + type: 'options', + options: [ + { + name: 'Ascending', + value: 'ASC', + }, + { + name: 'Descending', + value: 'DESC', + }, + ], + default: 'ASC', + description: 'The sorting direction', + }, + { + displayName: 'Field', + name: 'field', + type: 'string', + default: '', + description: `The sorting field`, + }, + ], + }, + ], + }, + ], + }, + /* -------------------------------------------------------------------------- */ /* company:create */ /* -------------------------------------------------------------------------- */ @@ -657,4 +931,5 @@ export const companyFields = [ }, ], }, + ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/AgileCrm/ContactDescription.ts b/packages/nodes-base/nodes/AgileCrm/ContactDescription.ts index 82cd03afe1..3ccf9b1cb9 100644 --- a/packages/nodes-base/nodes/AgileCrm/ContactDescription.ts +++ b/packages/nodes-base/nodes/AgileCrm/ContactDescription.ts @@ -71,25 +71,7 @@ export const contactFields = [ /* -------------------------------------------------------------------------- */ /* contact:get all */ /* -------------------------------------------------------------------------- */ - { - displayName: 'Limit', - name: 'limit', - type: 'number', - default: 20, - displayOptions: { - show: { - resource: [ - 'contact', - ], - operation: [ - 'getAll', - ], - returnAll: [ - false, - ], - }, - }, - }, + { displayName: 'Return All', name: 'returnAll', @@ -107,6 +89,296 @@ export const contactFields = [ default: false, description: 'If all results should be returned or only up to a given limit.', }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + default: 20, + description: 'Number of results to fetch.', + }, + + { + displayName: 'Filter', + name: 'filterType', + type: 'options', + options: [ + { + name: 'None', + value: 'none', + }, + { + name: 'Build Manually', + value: 'manual', + }, + { + name: 'JSON', + value: 'json', + }, + ], + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + }, + }, + default: 'none', + }, + { + displayName: 'Must Match', + name: 'matchType', + type: 'options', + options: [ + { + name: 'Any filter', + value: 'anyFilter', + }, + { + name: 'All Filters', + value: 'allFilters', + }, + ], + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + filterType: [ + 'manual', + ], + }, + }, + default: 'anyFilter', + }, + { + displayName: 'Simplify Response', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'Return a simplified version of the response instead of the raw data.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + filterType: [ + 'manual', + ], + }, + }, + default: '', + placeholder: 'Add Condition', + options: [ + { + displayName: 'Conditions', + name: 'conditions', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'string', + default: '', + description: 'Any searchable field.', + }, + { + displayName: 'Condition Type', + name: 'condition_type', + type: 'options', + options: [ + { + name: 'Equals', + value: 'EQUALS', + }, + { + name: 'Not Equal', + value: 'NOTEQUALS', + }, + { + name: 'Last', + value: 'LAST', + }, + { + name: 'Between', + value: 'BETWEEN', + }, + { + name: 'On', + value: 'ON', + }, + { + name: 'Before', + value: 'BEFORE', + }, + { + name: 'After', + value: 'AFTER', + }, + ], + default: 'EQUALS', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + }, + { + displayName: 'Value 2', + name: 'value2', + type: 'string', + displayOptions: { + show: { + condition_type: [ + 'BETWEEN', + ], + }, + }, + default: '', + }, + ], + }, + ], + }, + { + displayName: 'See Agile CRM guide to creating filters', + name: 'jsonNotice', + type: 'notice', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + filterType: [ + 'json', + ], + }, + }, + default: '', + }, + { + displayName: 'Filters (JSON)', + name: 'filterJson', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + filterType: [ + 'json', + ], + }, + }, + default: '', + description: '', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Sort', + name: 'sort', + type: 'fixedCollection', + placeholder: 'Add Sort', + default: [], + options: [ + { + displayName: 'Sort', + name: 'sort', + values: [ + { + displayName: 'Direction', + name: 'direction', + type: 'options', + options: [ + { + name: 'Ascending', + value: 'ASC', + }, + { + name: 'Descending', + value: 'DESC', + }, + ], + default: 'ASC', + description: 'The sorting direction', + }, + { + displayName: 'Field', + name: 'field', + type: 'string', + default: '', + description: `The sorting field`, + }, + ], + }, + ], + }, + ], + }, /* -------------------------------------------------------------------------- */ /* contact:create */ @@ -988,4 +1260,5 @@ export const contactFields = [ }, ], }, + ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/AgileCrm/DealDescription.ts b/packages/nodes-base/nodes/AgileCrm/DealDescription.ts index 7fb7f55e5f..7dd93643e0 100644 --- a/packages/nodes-base/nodes/AgileCrm/DealDescription.ts +++ b/packages/nodes-base/nodes/AgileCrm/DealDescription.ts @@ -110,8 +110,6 @@ export const dealFields = [ default: false, description: 'If all results should be returned or only up to a given limit.', }, - - /* -------------------------------------------------------------------------- */ /* deal:create */ /* -------------------------------------------------------------------------- */ diff --git a/packages/nodes-base/nodes/AgileCrm/FilterInterface.ts b/packages/nodes-base/nodes/AgileCrm/FilterInterface.ts new file mode 100644 index 0000000000..001dea948a --- /dev/null +++ b/packages/nodes-base/nodes/AgileCrm/FilterInterface.ts @@ -0,0 +1,19 @@ +export interface ISearchConditions { + field?: string; + condition_type?: string; + value?: string; + value2?: string; +} + +export interface IFilterRules { + LHS?: string; + CONDITION?: string; + RHS?: string; + RHS_NEW?: string; +} + +export interface IFilter { + or_rules?: IFilterRules; + rules?: IFilterRules; + contact_type?: string; +} diff --git a/packages/nodes-base/nodes/AgileCrm/GenericFunctions.ts b/packages/nodes-base/nodes/AgileCrm/GenericFunctions.ts index 0143b0f624..6c01d3c0b8 100644 --- a/packages/nodes-base/nodes/AgileCrm/GenericFunctions.ts +++ b/packages/nodes-base/nodes/AgileCrm/GenericFunctions.ts @@ -1,6 +1,4 @@ -import { - OptionsWithUri -} from 'request'; +import { OptionsWithUri } from 'request'; import { IExecuteFunctions, @@ -10,12 +8,21 @@ import { } from 'n8n-core'; import { - IDataObject, NodeApiError, + IDataObject, + NodeApiError, } from 'n8n-workflow'; -import { IContactUpdate } from './ContactInterface'; +import { + IContactUpdate, +} from './ContactInterface'; -export async function agileCrmApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query: IDataObject = {}, uri?: string): Promise { // tslint:disable-line:no-any +import { + IFilterRules, + ISearchConditions, +} from './FilterInterface'; + +export async function agileCrmApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, + method: string, endpoint: string, body: any = {}, query: IDataObject = {}, uri?: string, sendAsForm?: boolean): Promise { // tslint:disable-line:no-any const credentials = await this.getCredentials('agileCrmApi'); const options: OptionsWithUri = { @@ -27,12 +34,18 @@ export async function agileCrmApiRequest(this: IHookFunctions | IExecuteFunction username: credentials!.email as string, password: credentials!.apiKey as string, }, + qs: query, uri: uri || `https://${credentials!.subdomain}.agilecrm.com/dev/${endpoint}`, json: true, }; + // To send the request as 'content-type': 'application/x-www-form-urlencoded' add form to options instead of body + if(sendAsForm) { + options.form = body; + } // Only add Body property if method not GET or DELETE to avoid 400 response - if (method !== 'GET' && method !== 'DELETE') { + // And when not sending a form + else if (method !== 'GET' && method !== 'DELETE') { options.body = body; } @@ -41,7 +54,30 @@ export async function agileCrmApiRequest(this: IHookFunctions | IExecuteFunction } catch (error) { throw new NodeApiError(this.getNode(), error); } +} +export async function agileCrmApiRequestAllItems(this: IHookFunctions | ILoadOptionsFunctions | IExecuteFunctions, + method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, sendAsForm?: boolean): Promise { // tslint:disable-line:no-any + // https://github.com/agilecrm/rest-api#11-listing-contacts- + + const returnData: IDataObject[] = []; + let responseData; + do { + responseData = await agileCrmApiRequest.call(this, method, resource, body, query, uri, sendAsForm); + if (responseData.length !== 0) { + returnData.push.apply(returnData, responseData); + if (sendAsForm) { + body.cursor = responseData[responseData.length-1].cursor; + } else { + query.cursor = responseData[responseData.length-1].cursor; + } + } + } while ( + responseData.length !== 0 && + responseData[responseData.length-1].hasOwnProperty('cursor') + ); + + return returnData; } export async function agileCrmApiRequestUpdate(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method = 'PUT', endpoint?: string, body: any = {}, query: IDataObject = {}, uri?: string): Promise { // tslint:disable-line:no-any @@ -131,3 +167,39 @@ export function validateJSON(json: string | undefined): any { // tslint:disable- } return result; } + +export function getFilterRules(conditions: ISearchConditions[], matchType: string): IDataObject { // tslint:disable-line:no-any + const rules = []; + + for (const key in conditions) { + if (conditions.hasOwnProperty(key)) { + const searchConditions: ISearchConditions = conditions[key] as ISearchConditions; + const rule: IFilterRules = { + LHS: searchConditions.field, + CONDITION: searchConditions.condition_type, + RHS: searchConditions.value as string, + RHS_NEW: searchConditions.value2 as string, + }; + rules.push(rule); + } + } + + if (matchType === 'anyFilter') { + return { + or_rules: rules, + }; + } + else { + return { + rules, + }; + } +} + +export function simplifyResponse(records: [{ id: string, properties: [{ name: string, value: string }] } ]) { + const results = []; + for (const record of records) { + results.push(record.properties.reduce((obj, value) => Object.assign(obj, { [`${value.name}`]: value.value }), { id: record.id })); + } + return results; +} From 171f5a458cce96923f5fb4b46ab19d262cf60478 Mon Sep 17 00:00:00 2001 From: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> Date: Wed, 27 Oct 2021 21:55:37 +0200 Subject: [PATCH 007/315] :zap: Update parameter inputs to be multi-line (#2299) * introduce analytics * add user survey backend * add user survey backend * set answers on survey submit Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> * change name to personalization * lint Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> * N8n 2495 add personalization modal (#2280) * update modals * add onboarding modal * implement questions * introduce analytics * simplify impl * implement survey handling * add personalized cateogry * update modal behavior * add thank you view * handle empty cases * rename modal * standarize modal names * update image, add tags to headings * remove unused file * remove unused interfaces * clean up footer spacing * introduce analytics * refactor to fix bug * update endpoint * set min height * update stories * update naming from questions to survey * remove spacing after core categories * fix bug in logic * sort nodes * rename types * merge with be * rename userSurvey * clean up rest api * use constants for keys * use survey keys * clean up types * move personalization to its own file Co-authored-by: ahsan-virani * update parameter inputs to be multiline * update spacing * Survey new options (#2300) * split up options * fix quotes * remove unused import * refactor node credentials * add user created workflow event (#2301) * update multi params * simplify env vars * fix versionCli on FE * update personalization env * clean up node detail settings * fix event User opened Credentials panel * fix font sizes across modals * clean up input spacing * fix select modal spacing * increase spacing * fix input copy * fix webhook, tab spacing, retry button * fix button sizes * fix button size * add mini xlarge sizes * fix webhook spacing * fix nodes panel event * fix workflow id in workflow execute event * improve telemetry error logging * fix config and stop process events * add flush call on n8n stop * ready for release * fix input error highlighting * revert change * update toggle spacing * fix delete positioning * keep tooltip while focused * set strict size * increase left spacing * fix sort icons * remove unnessary margin * clean unused functionality * remove unnessary css * remove duplicate tracking * only show tooltip when hovering over label * update credentials section * use includes Co-authored-by: ahsan-virani Co-authored-by: Jan Oberhauser --- .../components/N8nButton/Button.stories.js | 8 +- .../src/components/N8nButton/Button.vue | 9 +- .../src/components/N8nButton/index.d.ts | 3 - .../src/components/N8nIcon/Icon.vue | 44 ++--- .../components/N8nIconButton/IconButton.vue | 11 +- .../src/components/N8nIconButton/index.d.ts | 3 - .../components/N8nInputLabel/InputLabel.vue | 96 +++++++++-- .../src/components/N8nText/Text.vue | 27 ++- .../src/components/component.d.ts | 2 +- packages/design-system/theme/src/button.scss | 22 +++ packages/design-system/theme/src/dialog.scss | 2 - packages/design-system/theme/src/tabs.scss | 2 +- packages/editor-ui/src/components/About.vue | 1 + .../editor-ui/src/components/CodeEdit.vue | 20 +-- .../src/components/CollectionParameter.vue | 10 +- .../editor-ui/src/components/CopyInput.vue | 1 + .../CredentialEdit/CredentialEdit.vue | 2 +- .../CredentialEdit/CredentialInfo.vue | 14 +- .../src/components/CredentialsList.vue | 4 +- .../editor-ui/src/components/DataDisplay.vue | 2 +- .../src/components/ExecutionsList.vue | 12 +- .../src/components/ExpressionEdit.vue | 1 + .../components/FixedCollectionParameter.vue | 68 ++++---- .../src/components/MultipleParameter.vue | 74 ++++----- .../src/components/NodeCredentials.vue | 156 +++++++----------- .../editor-ui/src/components/NodeSettings.vue | 26 ++- .../editor-ui/src/components/NodeWebhooks.vue | 20 +-- .../src/components/ParameterInput.vue | 17 +- .../src/components/ParameterInputExpanded.vue | 14 +- .../src/components/ParameterInputFull.vue | 94 +++-------- .../src/components/ParameterInputList.vue | 87 +++++----- packages/editor-ui/src/components/RunData.vue | 23 +-- .../editor-ui/src/components/TextEdit.vue | 20 +-- .../src/components/WorkflowSettings.vue | 1 + packages/editor-ui/src/components/helpers.ts | 6 - 35 files changed, 443 insertions(+), 459 deletions(-) diff --git a/packages/design-system/src/components/N8nButton/Button.stories.js b/packages/design-system/src/components/N8nButton/Button.stories.js index 504ea1af38..4958f85de5 100644 --- a/packages/design-system/src/components/N8nButton/Button.stories.js +++ b/packages/design-system/src/components/N8nButton/Button.stories.js @@ -18,7 +18,7 @@ export default { size: { control: { type: 'select', - options: ['small', 'medium', 'large'], + options: ['mini', 'small', 'medium', 'large', 'xlarge'], }, }, loading: { @@ -31,12 +31,6 @@ export default { type: 'text', }, }, - iconSize: { - control: { - type: 'select', - options: ['small', 'medium', 'large'], - }, - }, circle: { control: { type: 'boolean', diff --git a/packages/design-system/src/components/N8nButton/Button.vue b/packages/design-system/src/components/N8nButton/Button.vue index 2827b8f8b7..ff8d78b407 100644 --- a/packages/design-system/src/components/N8nButton/Button.vue +++ b/packages/design-system/src/components/N8nButton/Button.vue @@ -16,13 +16,13 @@ {{ props.label }} @@ -58,7 +58,7 @@ export default { type: String, default: 'medium', validator: (value: string): boolean => - ['small', 'medium', 'large'].indexOf(value) !== -1, + ['mini', 'small', 'medium', 'large', 'xlarge'].indexOf(value) !== -1, }, loading: { type: Boolean, @@ -71,9 +71,6 @@ export default { icon: { type: String, }, - iconSize: { - type: String, - }, round: { type: Boolean, default: true, diff --git a/packages/design-system/src/components/N8nButton/index.d.ts b/packages/design-system/src/components/N8nButton/index.d.ts index b70b0b8418..ad6c9e62b9 100644 --- a/packages/design-system/src/components/N8nButton/index.d.ts +++ b/packages/design-system/src/components/N8nButton/index.d.ts @@ -35,9 +35,6 @@ export declare class N8nButton extends N8nComponent { /** Button icon, accepts an icon name of font awesome icon component */ icon: string; - /** Size of icon */ - iconSize: N8nComponentSize; - /** Full width */ fullWidth: boolean; diff --git a/packages/design-system/src/components/N8nIcon/Icon.vue b/packages/design-system/src/components/N8nIcon/Icon.vue index 52457db0ac..8ffe65edec 100644 --- a/packages/design-system/src/components/N8nIcon/Icon.vue +++ b/packages/design-system/src/components/N8nIcon/Icon.vue @@ -1,19 +1,27 @@ + diff --git a/packages/design-system/src/components/N8nIconButton/IconButton.vue b/packages/design-system/src/components/N8nIconButton/IconButton.vue index 6064ca4a53..513c829f00 100644 --- a/packages/design-system/src/components/N8nIconButton/IconButton.vue +++ b/packages/design-system/src/components/N8nIconButton/IconButton.vue @@ -2,11 +2,10 @@ import N8nButton from '../N8nButton'; -const iconSizeMap = { - large: 'medium', - xlarge: 'large', -}; - export default { name: 'n8n-icon-button', components: { @@ -36,8 +30,6 @@ export default { size: { type: String, default: 'medium', - validator: (value: string): boolean => - ['small', 'medium', 'large', 'xlarge'].indexOf(value) !== -1, }, loading: { type: Boolean, @@ -55,6 +47,5 @@ export default { type: String, }, }, - iconSizeMap, }; diff --git a/packages/design-system/src/components/N8nIconButton/index.d.ts b/packages/design-system/src/components/N8nIconButton/index.d.ts index d939a7e040..46ab70c572 100644 --- a/packages/design-system/src/components/N8nIconButton/index.d.ts +++ b/packages/design-system/src/components/N8nIconButton/index.d.ts @@ -12,9 +12,6 @@ export declare class N8nIconButton extends N8nComponent { /** Button size */ size: N8nComponentSize | 'xlarge'; - /** icon size */ - iconSize: N8nComponentSize; - /** Determine whether it's loading */ loading: boolean; diff --git a/packages/design-system/src/components/N8nInputLabel/InputLabel.vue b/packages/design-system/src/components/N8nInputLabel/InputLabel.vue index a5213ca1aa..a3bc71899a 100644 --- a/packages/design-system/src/components/N8nInputLabel/InputLabel.vue +++ b/packages/design-system/src/components/N8nInputLabel/InputLabel.vue @@ -1,13 +1,13 @@