From 69ba22fc9b26cce39251106bf784472995a07a2e Mon Sep 17 00:00:00 2001 From: Rupenieks Date: Fri, 22 May 2020 10:22:00 +0200 Subject: [PATCH 01/15] Credentials Subdomain --- packages/nodes-base/credentials/AgileCrmApi.credentials.ts | 6 ++++++ packages/nodes-base/nodes/AgileCrm/GenericFunctions.ts | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/nodes-base/credentials/AgileCrmApi.credentials.ts b/packages/nodes-base/credentials/AgileCrmApi.credentials.ts index d189270932..2e25893c9a 100644 --- a/packages/nodes-base/credentials/AgileCrmApi.credentials.ts +++ b/packages/nodes-base/credentials/AgileCrmApi.credentials.ts @@ -19,5 +19,11 @@ export class AgileCrmApi implements ICredentialType { type: 'string' as NodePropertyTypes, default: '', }, + { + displayName: 'Subdomain', + name: 'subdomain', + type: 'string' as NodePropertyTypes, + default: '', + }, ]; } diff --git a/packages/nodes-base/nodes/AgileCrm/GenericFunctions.ts b/packages/nodes-base/nodes/AgileCrm/GenericFunctions.ts index 8be59de7f5..59a16d6811 100644 --- a/packages/nodes-base/nodes/AgileCrm/GenericFunctions.ts +++ b/packages/nodes-base/nodes/AgileCrm/GenericFunctions.ts @@ -45,8 +45,9 @@ export async function agileCrmApiRequest(this: IHookFunctions | IExecuteFunction } 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 - const baseUri = 'https://n8nio.agilecrm.com/dev/'; + const credentials = this.getCredentials('agileCrmApi'); + const baseUri = `https://${credentials!.subdomain}.agilecrm.com/dev/`; const options: OptionsWithUri = { method, headers: { From c6788eed2005e1c3ad663fccba7bd4a78e439400 Mon Sep 17 00:00:00 2001 From: ricardo Date: Fri, 22 May 2020 09:47:47 -0400 Subject: [PATCH 02/15] :zap: Mailchimp improvements --- .../nodes/Mailchimp/GenericFunctions.ts | 42 +- .../nodes/Mailchimp/Mailchimp.node.ts | 1201 +++++++++++++++-- 2 files changed, 1128 insertions(+), 115 deletions(-) diff --git a/packages/nodes-base/nodes/Mailchimp/GenericFunctions.ts b/packages/nodes-base/nodes/Mailchimp/GenericFunctions.ts index 6f0aeb0e8c..a78124feed 100644 --- a/packages/nodes-base/nodes/Mailchimp/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Mailchimp/GenericFunctions.ts @@ -1,13 +1,19 @@ -import { OptionsWithUri } from 'request'; +import { + OptionsWithUri, + } from 'request'; import { IExecuteFunctions, + IExecuteSingleFunctions, IHookFunctions, ILoadOptionsFunctions, - IExecuteSingleFunctions } from 'n8n-core'; -export async function mailchimpApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, endpoint: string, method: string, body: any = {}, headers?: object): Promise { // tslint:disable-line:no-any +import { + IDataObject, + } from 'n8n-workflow'; + +export async function mailchimpApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, endpoint: string, method: string, body: any = {}, qs: IDataObject = {} ,headers?: object): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('mailchimpApi'); if (credentials === undefined) { @@ -27,6 +33,7 @@ export async function mailchimpApiRequest(this: IHookFunctions | IExecuteFunctio const options: OptionsWithUri = { headers: headerWithAuthentication, method, + qs, uri: `https://${datacenter}.${host}${endpoint}`, json: true, }; @@ -34,19 +41,36 @@ export async function mailchimpApiRequest(this: IHookFunctions | IExecuteFunctio if (Object.keys(body).length !== 0) { options.body = body; } - try { return await this.helpers.request!(options); } catch (error) { - const errorMessage = error.response.body.message || error.response.body.Message; - - if (errorMessage !== undefined) { - throw errorMessage; + if (error.response.body && error.response.body.detail) { + throw new Error(`Mailchimp Error: response [${error.statusCode}]: ${error.response.body.detail}`); } - throw error.response.body; + throw new Error(error); } } +export async function mailchimpApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, endpoint: string, method: string, propertyName: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + query.offset = 0; + query.count = 500; + + do { + responseData = await mailchimpApiRequest.call(this, endpoint, method, body, query); + returnData.push.apply(returnData, responseData[propertyName]); + query.offset += query.count; + } while ( + responseData[propertyName] && responseData[propertyName].length !== 0 + ); + + return returnData; +} + export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any let result; try { diff --git a/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts b/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts index 187512ff29..cb54ac24e9 100644 --- a/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts +++ b/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts @@ -1,21 +1,25 @@ -import * as moment from 'moment'; import { - IExecuteSingleFunctions, + IExecuteFunctions, } from 'n8n-core'; + import { IDataObject, ILoadOptionsFunctions, - INodeTypeDescription, INodeExecutionData, - INodeType, INodePropertyOptions, + INodeType, + INodeTypeDescription, } from 'n8n-workflow'; + import { mailchimpApiRequest, + mailchimpApiRequestAllItems, validateJSON, } from './GenericFunctions'; +import * as moment from 'moment'; + enum Status { subscribe = 'subscribe', unsubscribed = 'unsubscribe', @@ -37,7 +41,7 @@ interface ICreateMemberBody { language?: string; vip?: boolean; location?: ILocation; - ips_signup?: string; + ip_signup?: string; timestamp_signup?: string; ip_opt?: string; timestamp_opt?: string; @@ -57,7 +61,7 @@ export class Mailchimp implements INodeType { description: 'Consume Mailchimp API', defaults: { name: 'Mailchimp', - color: '#c02428', + color: '#000000', }, inputs: ['main'], outputs: ['main'], @@ -76,7 +80,10 @@ export class Mailchimp implements INodeType { { name: 'Member', value: 'member', - description: 'Add member to list', + }, + { + name: 'Member Tag', + value: 'memberTag', }, ], default: 'member', @@ -101,10 +108,60 @@ export class Mailchimp implements INodeType { value: 'create', description: 'Create a new member on list', }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a member on list', + }, + { + name: 'Get', + value: 'get', + description: 'Get a member on list', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all members on list', + }, + { + name: 'Update', + value: 'update', + description: 'Update a new member on list', + }, ], default: 'create', description: 'The operation to perform.', }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + required: true, + displayOptions: { + show: { + resource: [ + 'memberTag', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Add tags from a list member', + }, + { + name: 'Delete', + value: 'delete', + description: 'Remove tags from a list member', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +/* -------------------------------------------------------------------------- */ +/* member:create */ +/* -------------------------------------------------------------------------- */ { displayName: 'List', name: 'list', @@ -230,8 +287,8 @@ export class Mailchimp implements INodeType { type: 'options', options: [ { - name: 'Email', - value: 'email', + name: 'HTML', + value: 'html', description: '', }, { @@ -244,11 +301,11 @@ export class Mailchimp implements INodeType { description: 'Type of email this member asked to get', }, { - displayName: 'Signup IP', - name: 'ipSignup', + displayName: 'Language', + name: 'language', type: 'string', default: '', - description: 'IP address the subscriber signed up from.', + description: `If set/detected, the subscriber's language.`, }, { displayName: 'Opt-in IP', @@ -257,6 +314,13 @@ export class Mailchimp implements INodeType { default: '', description: 'The IP address the subscriber used to confirm their opt-in status.', }, + { + displayName: 'Signup IP', + name: 'ipSignup', + type: 'string', + default: '', + description: 'IP address the subscriber signed up from.', + }, { displayName: 'Signup Timestamp', name: 'timestampSignup', @@ -265,11 +329,11 @@ export class Mailchimp implements INodeType { description: 'The date and time the subscriber signed up for the list in ISO 8601 format.', }, { - displayName: 'Language', - name: 'language', + displayName: 'Tags', + name: 'tags', type: 'string', default: '', - description: `If set/detected, the subscriber's language.`, + description: `The tags that are associated with a member separeted by ,.`, }, { displayName: 'Vip', @@ -285,14 +349,7 @@ export class Mailchimp implements INodeType { default: '', description: `The date and time the subscribe confirmed their opt-in status in ISO 8601 format.`, }, - { - displayName: 'Tags', - name: 'tags', - type: 'string', - default: '', - description: `The tags that are associated with a member separeted by ,.`, - }, - ] + ], }, { displayName: 'Location', @@ -373,7 +430,13 @@ export class Mailchimp implements INodeType { { displayName: 'Field Name', name: 'name', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getMergeFields', + loadOptionsDependsOn: [ + 'list', + ], + }, required: true, description: 'Merge Field name', default: '', @@ -436,7 +499,723 @@ export class Mailchimp implements INodeType { }, }, }, - ] +/* -------------------------------------------------------------------------- */ +/* member:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'List', + name: 'list', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLists', + }, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'delete', + ], + }, + }, + default: '', + options: [], + required: true, + description: 'List of lists', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'delete', + ], + }, + }, + default: '', + required: true, + description: `Member's email`, + }, +/* -------------------------------------------------------------------------- */ +/* member:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'List', + name: 'list', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLists', + }, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + options: [], + required: true, + description: 'List of lists', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + required: true, + description: `Member's email`, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource:[ + 'member', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + default: '', + description: 'A comma-separated list of fields to return.', + }, + { + displayName: 'Exclude Fields', + name: 'excludeFields', + type: 'string', + default: '', + description: 'A comma-separated list of fields to exclude.', + }, + ] + }, +/* -------------------------------------------------------------------------- */ +/* member:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'List', + name: 'list', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLists', + }, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'getAll', + ], + }, + }, + default: '', + options: [], + required: true, + description: 'List of lists', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'getAll', + ], + }, + }, + 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: [ + 'member', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 1000, + }, + default: 500, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource:[ + 'member', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Before Last Changed', + name: 'beforeLastChanged', + type: 'dateTime', + default: '', + description: 'Restrict results to subscribers whose information changed before the set timeframe.', + }, + { + displayName: 'Before Timestamp Opt', + name: 'beforeTimestampOpt', + type: 'dateTime', + default: '', + description: 'Restrict results to subscribers who opted-in before the set timeframe', + }, + // { + // displayName: 'Fields', + // name: 'fields', + // type: 'string', + // default: '', + // description: 'A comma-separated list of fields to return.', + // }, + // { + // displayName: 'Exclude Fields', + // name: 'excludeFields', + // type: 'string', + // default: '', + // description: 'A comma-separated list of fields to exclude.', + // }, + { + displayName: 'Email Type', + name: 'emailType', + type: 'options', + options: [ + { + name: 'HTML', + value: 'html', + description: '', + }, + { + name: 'Text', + value: 'text', + description: '', + }, + ], + default: '', + description: 'Type of email this member asked to get', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Subscribed', + value: 'subscribed', + description: '', + }, + { + name: 'Unsubscribed', + value: 'unsubscribed', + description: '', + }, + { + name: 'Cleaned', + value: 'cleaned', + description: '', + }, + { + name: 'Pending', + value: 'pending', + description: '', + }, + { + name: 'Transactional', + value: 'transactional', + description: '', + }, + ], + default: '', + description: `Subscriber's current status.`, + }, + { + displayName: 'Since Last Changed', + name: 'sinceLastChanged', + type: 'dateTime', + default: '', + description: 'Restrict results to subscribers whose information changed after the set timeframe.', + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* member:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'List', + name: 'list', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLists', + }, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + options: [], + required: true, + description: 'List of lists', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'member', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'Email address of the subscriber.', + }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + description: '', + displayOptions: { + show: { + resource:[ + 'member' + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource:[ + 'member', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Email Type', + name: 'emailType', + type: 'options', + options: [ + { + name: 'HTML', + value: 'html', + description: '', + }, + { + name: 'Text', + value: 'text', + description: '', + }, + ], + default: '', + description: 'Type of email this member asked to get', + }, + { + displayName: 'Language', + name: 'language', + type: 'string', + default: '', + description: `If set/detected, the subscriber's language.`, + }, + { + displayName: 'Merge Fields', + name: 'mergeFieldsUi', + placeholder: 'Add Merge Fields', + type: 'fixedCollection', + default: {}, + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + '/resource':[ + 'member' + ], + '/operation':[ + 'update', + ], + '/jsonParameters': [ + false, + ], + }, + }, + description: 'An individual merge var and value for a member.', + options: [ + { + name: 'mergeFieldsValues', + displayName: 'Field', + typeOptions: { + multipleValueButtonText: 'Add Field', + }, + values: [ + { + displayName: 'Field Name', + name: 'name', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getMergeFields', + loadOptionsDependsOn: [ + 'list', + ], + }, + required: true, + description: 'Merge Field name', + default: '', + }, + { + displayName: 'Field Value', + name: 'value', + required: true, + type: 'string', + default: '', + description: 'Merge field value.', + }, + ], + }, + ], + }, + { + displayName: 'Opt-in IP', + name: 'ipOptIn', + type: 'string', + default: '', + description: 'The IP address the subscriber used to confirm their opt-in status.', + }, + { + displayName: 'Signup IP', + name: 'ipSignup', + type: 'string', + default: '', + description: 'IP address the subscriber signed up from.', + }, + { + displayName: 'Signup Timestamp', + name: 'timestampSignup', + type: 'dateTime', + default: '', + description: 'The date and time the subscriber signed up for the list in ISO 8601 format.', + }, + { + displayName: 'Skip Merge Validation', + name: 'skipMergeValidation', + type: 'boolean', + default: false, + description: `If skip_merge_validation is true, member data will be accepted without merge field values,
+ even if the merge field is usually required`, + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + required: true, + options: [ + { + name: 'Subscribed', + value: 'subscribed', + description: '', + }, + { + name: 'Unsubscribed', + value: 'unsubscribed', + description: '', + }, + { + name: 'Cleaned', + value: 'cleaned', + description: '', + }, + { + name: 'Pending', + value: 'pending', + description: '', + }, + { + name: 'Transactional', + value: 'transactional', + description: '', + }, + ], + default: '', + description: `Subscriber's current status.`, + }, + { + displayName: 'Vip', + name: 'vip', + type: 'boolean', + default: false, + description: `Vip status for subscribers`, + }, + { + displayName: 'Location', + name: 'locationFieldsUi', + type: 'fixedCollection', + placeholder: 'Add Location', + default: {}, + description: `Subscriber location information.n`, + displayOptions: { + show: { + '/resource':[ + 'member' + ], + '/operation':[ + 'update', + ], + '/jsonParameters': [ + false, + ], + }, + }, + options: [ + { + name: 'locationFieldsValues', + displayName: 'Location', + values: [ + { + displayName: 'Latitude', + name: 'latitude', + type: 'string', + required: true, + description: 'The location latitude.', + default: '', + }, + { + displayName: 'Longitude', + name: 'longitude', + type: 'string', + required: true, + description: 'The location longitude.', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Opt-in Timestamp', + name: 'timestampOpt', + type: 'dateTime', + default: '', + description: `The date and time the subscribe confirmed their opt-in status in ISO 8601 format.`, + }, + ], + }, + { + displayName: 'Merge Fields', + name: 'mergeFieldsJson', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: '', + displayOptions: { + show: { + resource:[ + 'member', + ], + operation: [ + 'update', + ], + jsonParameters: [ + true, + ], + }, + }, + }, + { + displayName: 'Location', + name: 'locationJson', + type: 'json', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: '', + displayOptions: { + show: { + resource:[ + 'member', + ], + operation: [ + 'update', + ], + jsonParameters: [ + true, + ], + }, + }, + }, +/* -------------------------------------------------------------------------- */ +/* memberTag:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'List', + name: 'list', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLists', + }, + displayOptions: { + show: { + resource: [ + 'memberTag', + ], + operation: [ + 'create', + 'delete', + ], + }, + }, + default: '', + options: [], + required: true, + description: 'List of lists', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'memberTag', + ], + operation: [ + 'create', + 'delete', + ], + }, + }, + default: '', + description: 'Email address of the subscriber.', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Tag', + }, + displayOptions: { + show: { + resource:[ + 'memberTag' + ], + operation: [ + 'create', + 'delete', + ], + }, + }, + default: [], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource:[ + 'memberTag', + ], + operation: [ + 'create', + 'delete', + ], + }, + }, + options: [ + { + displayName: 'Is Syncing', + name: 'isSyncing', + type: 'boolean', + default: false, + description: 'When is_syncing is true, automations based on the tags in the request will not fire', + }, + ], + }, + ], }; @@ -447,17 +1226,10 @@ export class Mailchimp implements INodeType { // select them easily async getLists(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - let lists, response; - try { - response = await mailchimpApiRequest.call(this, '/lists', 'GET'); - lists = response.lists; - } catch (err) { - throw new Error(`Mailchimp Error: ${err}`); - } + const { lists } = await mailchimpApiRequest.call(this, '/lists', 'GET'); for (const list of lists) { const listName = list.name; const listId = list.id; - returnData.push({ name: listName, value: listId, @@ -465,94 +1237,311 @@ export class Mailchimp implements INodeType { } return returnData; }, + + // Get all the available merge fields to display them to user so that he can + // select them easily + async getMergeFields(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const listId = this.getCurrentNodeParameter('list'); + const { merge_fields } = await mailchimpApiRequest.call(this, `/lists/${listId}/merge-fields`, 'GET'); + for (const mergeField of merge_fields) { + const mergeFieldName = mergeField.name; + const mergeFieldId = mergeField.tag; + returnData.push({ + name: mergeFieldName, + value: mergeFieldId, + }); + } + return returnData; + }, } }; - async executeSingle(this: IExecuteSingleFunctions): Promise { - let response = {}; - const resource = this.getNodeParameter('resource') as string; - const operation = this.getNodeParameter('operation') as string; + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + let responseData; + const qs: IDataObject = {}; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; - if (resource === 'member') { - if (operation === 'create') { + for (let i = 0; i < length; i++) { + if (resource === 'member') { + //https://mailchimp.com/developer/reference/lists/list-members/#post_/lists/-list_id-/members + if (operation === 'create') { + const listId = this.getNodeParameter('list', i) as string; + const email = this.getNodeParameter('email', i) as string; + const status = this.getNodeParameter('status', i) as Status; + const options = this.getNodeParameter('options', i) as IDataObject; + const jsonActive = this.getNodeParameter('jsonParameters', i) as IDataObject; - const listId = this.getNodeParameter('list') as string; - const email = this.getNodeParameter('email') as string; - const status = this.getNodeParameter('status') as Status; - const options = this.getNodeParameter('options') as IDataObject; - const jsonActive = this.getNodeParameter('jsonParameters') as IDataObject; + const body: ICreateMemberBody = { + listId, + email_address: email, + status + }; + if (options.emailType) { + body.email_type = options.emailType as string; + } + if (options.language) { + body.language = options.language as string; + } + if (options.vip) { + body.vip = options.vip as boolean; + } + if (options.ipSignup) { + body.ip_signup = options.ipSignup as string; + } + if (options.ipOptIn) { + body.ip_opt = options.ipOptIn as string; + } + if (options.timestampOpt) { + body.timestamp_opt = moment(options.timestampOpt as string).format('YYYY-MM-DD HH:MM:SS') as string; + } + if (options.timestampSignup) { + body.timestamp_signup = moment(options.timestampSignup as string).format('YYYY-MM-DD HH:MM:SS') as string; + } + if (options.tags) { + // @ts-ignore + body.tags = options.tags.split(',') as string[]; + } + if (!jsonActive) { + const locationValues = (this.getNodeParameter('locationFieldsUi', i) as IDataObject).locationFieldsValues as IDataObject; + if (locationValues) { + const location: ILocation = {}; + for (const key of Object.keys(locationValues)) { + if (key === 'latitude') { + location.latitude = parseFloat(locationValues[key] as string) as number; + } else if (key === 'longitude') { + location.longitude = parseFloat(locationValues[key] as string) as number; + } + } + body.location = location; + } + const mergeFieldsValues = (this.getNodeParameter('mergeFieldsUi', i) as IDataObject).mergeFieldsValues as IDataObject[]; + if (mergeFieldsValues) { + const mergeFields = {}; + for (let i = 0; i < mergeFieldsValues.length; i++) { + // @ts-ignore + mergeFields[mergeFieldsValues[i].name] = mergeFieldsValues[i].value; + } + body.merge_fields = mergeFields; + } + } else { + const locationJson = validateJSON(this.getNodeParameter('locationJson', i) as string); + const mergeFieldsJson = validateJSON(this.getNodeParameter('mergeFieldsJson', i) as string); + if (locationJson) { + body.location = locationJson; + } + if (mergeFieldsJson) { + body.merge_fields = mergeFieldsJson; + } + } + responseData = await mailchimpApiRequest.call(this, `/lists/${listId}/members`, 'POST', body); + } + //https://mailchimp.com/developer/reference/lists/list-members/ + if (operation === 'delete') { - const body: ICreateMemberBody = { - listId, - email_address: email, - status - }; - if (options.emailType) { - body.email_type = options.emailType as string; + const listId = this.getNodeParameter('list', i) as string; + const email = this.getNodeParameter('email', i) as string; + + responseData = await mailchimpApiRequest.call(this, `/lists/${listId}/members/${email}/actions/delete-permanent`, 'POST'); + responseData = { success: true }; } - if (options.languaje) { - body.language = options.language as string; + //https://mailchimp.com/developer/reference/lists/list-members/#get_/lists/-list_id-/members/-subscriber_hash- + if (operation === 'get') { + + const listId = this.getNodeParameter('list', i) as string; + const email = this.getNodeParameter('email', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + + if (options.fields) { + qs.fields = options.fields as string; + } + + if (options.excludeFields) { + qs.exclude_fields = options.excludeFields as string; + } + + responseData = await mailchimpApiRequest.call(this, `/lists/${listId}/members/${email}`, 'GET', {}, qs); } - if (options.vip) { - body.vip = options.vip as boolean; + //https://mailchimp.com/developer/reference/lists/list-members/#get_/lists/-list_id-/members + if (operation === 'getAll') { + const listId = this.getNodeParameter('list', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + + if (options.beforeLastChanged) { + qs.before_last_changed = options.beforeLastChanged as string; + } + if (options.beforeTimestampOpt) { + qs.before_timestamp_opt = options.beforeTimestampOpt as string; + } + // TODO + //figure why for some reason when either fields or exclude_fields is set the endpoint returns nothing + // interestingly it works perfect when retriving just one member + + // if (options.fields) { + // qs.fields = options.fields as string; + // } + // if (options.excludeFields) { + // qs.exclude_fields = options.excludeFields as string; + // } + if (options.emailType) { + qs.email_type = options.emailType as string; + } + if (options.status) { + qs.status = options.status as string; + } + if (options.sinceLastChanged) { + qs.since_last_changed = options.sinceLastChanged as string; + } + if (returnAll === true) { + responseData = await mailchimpApiRequestAllItems.call(this, `/lists/${listId}/members`, 'GET', 'members', {}, qs); + } else { + qs.count = this.getNodeParameter('limit', i) as number; + responseData = await mailchimpApiRequest.call(this, `/lists/${listId}/members`, 'GET', {}, qs); + responseData = responseData.members; + } } - if (options.ipSignup) { - body.ips_signup = options.ipSignup as string; - } - if (options.ipOptIn) { - body.ip_opt = options.ipOptIn as string; - } - if (options.timestampOpt) { - body.timestamp_opt = moment(options.timestampOpt as string).format('YYYY-MM-DD HH:MM:SS') as string; - } - if (options.timestampSignup) { - body.timestamp_signup = moment(options.timestampSignup as string).format('YYYY-MM-DD HH:MM:SS') as string; - } - if (options.tags) { - // @ts-ignore - body.tags = options.tags.split(',') as string[]; - } - if (!jsonActive) { - const locationValues = (this.getNodeParameter('locationFieldsUi') as IDataObject).locationFieldsValues as IDataObject; - if (locationValues) { - const location: ILocation = {}; - for (const key of Object.keys(locationValues)) { - if (key === 'latitude') { - location.latitude = parseInt(locationValues[key] as string, 10) as number; - } else if (key === 'longitude') { - location.longitude = parseInt(locationValues[key] as string, 10) as number; + //https://mailchimp.com/developer/reference/lists/list-members/#put_/lists/-list_id-/members/-subscriber_hash- + if (operation === 'update') { + + const listId = this.getNodeParameter('list', i) as string; + const email = this.getNodeParameter('email', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const jsonActive = this.getNodeParameter('jsonParameters', i) as IDataObject; + const body: ICreateMemberBody = { + listId, + email_address: email, + }; + if (updateFields.skipMergeValidation) { + qs.skip_merge_validation = updateFields.skipMergeValidation as boolean; + } + if (updateFields.status) { + body.status = updateFields.status as Status; + } + if (updateFields.emailType) { + body.email_type = updateFields.emailType as string; + } + if (updateFields.language) { + body.language = updateFields.language as string; + } + if (updateFields.vip) { + body.vip = updateFields.vip as boolean; + } + if (updateFields.ipSignup) { + body.ip_signup = updateFields.ipSignup as string; + } + if (updateFields.ipOptIn) { + body.ip_opt = updateFields.ipOptIn as string; + } + if (updateFields.timestampOpt) { + body.timestamp_opt = moment(updateFields.timestampOpt as string).format('YYYY-MM-DD HH:MM:SS') as string; + } + if (updateFields.timestampSignup) { + body.timestamp_signup = moment(updateFields.timestampSignup as string).format('YYYY-MM-DD HH:MM:SS') as string; + } + if (!jsonActive) { + if (updateFields.locationFieldsUi) { + const locationValues = (updateFields.locationFieldsUi as IDataObject).locationFieldsValues as IDataObject; + if (locationValues) { + const location: ILocation = {}; + for (const key of Object.keys(locationValues)) { + if (key === 'latitude') { + location.latitude = parseFloat(locationValues[key] as string) as number; + } else if (key === 'longitude') { + location.longitude = parseFloat(locationValues[key] as string) as number; + } + } + body.location = location; } } - body.location = location; - } - const mergeFieldsValues = (this.getNodeParameter('mergeFieldsUi') as IDataObject).mergeFieldsValues as IDataObject[]; - if (mergeFieldsValues) { - const mergeFields = {}; - for (let i = 0; i < mergeFieldsValues.length; i++) { - // @ts-ignore - mergeFields[mergeFieldsValues[i].name] = mergeFieldsValues[i].value; + if (updateFields.mergeFieldsUi) { + const mergeFieldsValues = (updateFields.mergeFieldsUi as IDataObject).mergeFieldsValues as IDataObject[]; + if (mergeFieldsValues) { + const mergeFields = {}; + for (let i = 0; i < mergeFieldsValues.length; i++) { + // @ts-ignore + mergeFields[mergeFieldsValues[i].name] = mergeFieldsValues[i].value; + } + body.merge_fields = mergeFields; + } + } + } else { + const locationJson = validateJSON(this.getNodeParameter('locationJson', i) as string); + const mergeFieldsJson = validateJSON(this.getNodeParameter('mergeFieldsJson', i) as string); + if (locationJson) { + body.location = locationJson; + } + if (mergeFieldsJson) { + body.merge_fields = mergeFieldsJson; } - body.merge_fields = mergeFields; } - } else { - const locationJson = validateJSON(this.getNodeParameter('locationJson') as string); - const mergeFieldsJson = validateJSON(this.getNodeParameter('mergeFieldsJson') as string); - if (locationJson) { - body.location = locationJson; - } - if (mergeFieldsJson) { - body.merge_fields = mergeFieldsJson; - } - } - try { - response = await mailchimpApiRequest.call(this, `/lists/${listId}/members`, 'POST', body); - } catch (err) { - throw new Error(`Mailchimp Error: ${JSON.stringify(err)}`); + responseData = await mailchimpApiRequest.call(this, `/lists/${listId}/members/${email}`, 'PUT', body); } } + if (resource === 'memberTag') { + //https://mailchimp.com/developer/reference/lists/list-members/list-member-tags/#post_/lists/-list_id-/members/-subscriber_hash-/tags + if (operation === 'create') { + const listId = this.getNodeParameter('list', i) as string; + const email = this.getNodeParameter('email', i) as string; + const tags = this.getNodeParameter('tags', i) as string[]; + const options = this.getNodeParameter('options', i) as IDataObject; + + const body: IDataObject = { + tags: [], + }; + + if (options.isSyncing) { + body.is_syncing = options.isSyncing as boolean; + } + + for (const tag of tags) { + //@ts-ignore + body.tags.push({ + name: tag, + status: 'active', + }); + } + + responseData = await mailchimpApiRequest.call(this, `/lists/${listId}/members/${email}/tags`, 'POST', body); + responseData = { success: true }; + } + //https://mailchimp.com/developer/reference/lists/list-members/list-member-tags/#post_/lists/-list_id-/members/-subscriber_hash-/tags + if (operation === 'delete') { + + const listId = this.getNodeParameter('list', i) as string; + const email = this.getNodeParameter('email', i) as string; + const tags = this.getNodeParameter('tags', i) as string[]; + const options = this.getNodeParameter('options', i) as IDataObject; + + const body: IDataObject = { + tags: [], + }; + + if (options.isSyncing) { + body.is_syncing = options.isSyncing as boolean; + } + + for (const tag of tags) { + //@ts-ignore + body.tags.push({ + name: tag, + status: 'inactive', + }); + } + responseData = await mailchimpApiRequest.call(this, `/lists/${listId}/members/${email}/tags`, 'POST', body); + responseData = { success: true }; + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as IDataObject); + } } - return { - json: response, - }; + return [this.helpers.returnJsonArray(returnData)]; } } From c0dfaed4d0a10edb40137df529deade960b6f682 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 22 May 2020 21:17:27 +0200 Subject: [PATCH 03/15] :zap: Fix some minor issue in AgileCRM --- packages/nodes-base/credentials/AgileCrmApi.credentials.ts | 6 ++++-- packages/nodes-base/nodes/AgileCrm/GenericFunctions.ts | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/nodes-base/credentials/AgileCrmApi.credentials.ts b/packages/nodes-base/credentials/AgileCrmApi.credentials.ts index 2e25893c9a..65dbc7dd5e 100644 --- a/packages/nodes-base/credentials/AgileCrmApi.credentials.ts +++ b/packages/nodes-base/credentials/AgileCrmApi.credentials.ts @@ -12,8 +12,8 @@ export class AgileCrmApi implements ICredentialType { name: 'email', type: 'string' as NodePropertyTypes, default: '', - }, - { + }, + { displayName: 'API Key', name: 'apiKey', type: 'string' as NodePropertyTypes, @@ -24,6 +24,8 @@ export class AgileCrmApi implements ICredentialType { name: 'subdomain', type: 'string' as NodePropertyTypes, default: '', + placeholder: 'example', + description: 'If the domain is https://example.agilecrm.com "example" would have to be entered.', }, ]; } diff --git a/packages/nodes-base/nodes/AgileCrm/GenericFunctions.ts b/packages/nodes-base/nodes/AgileCrm/GenericFunctions.ts index 59a16d6811..71d5a68c14 100644 --- a/packages/nodes-base/nodes/AgileCrm/GenericFunctions.ts +++ b/packages/nodes-base/nodes/AgileCrm/GenericFunctions.ts @@ -27,7 +27,7 @@ export async function agileCrmApiRequest(this: IHookFunctions | IExecuteFunction username: credentials!.email as string, password: credentials!.apiKey as string }, - uri: uri || `https://n8nio.agilecrm.com/dev/${endpoint}`, + uri: uri || `https://${credentials!.subdomain}.agilecrm.com/dev/${endpoint}`, json: true, }; @@ -45,7 +45,7 @@ export async function agileCrmApiRequest(this: IHookFunctions | IExecuteFunction } 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 - + const credentials = this.getCredentials('agileCrmApi'); const baseUri = `https://${credentials!.subdomain}.agilecrm.com/dev/`; const options: OptionsWithUri = { From c03a7d12a1559ee650c083087046f995e6564e28 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 22 May 2020 21:44:39 +0200 Subject: [PATCH 04/15] :zap: Minor fix to Mailchimp-Node --- packages/nodes-base/nodes/Mailchimp/GenericFunctions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/nodes/Mailchimp/GenericFunctions.ts b/packages/nodes-base/nodes/Mailchimp/GenericFunctions.ts index a78124feed..91dfcdde85 100644 --- a/packages/nodes-base/nodes/Mailchimp/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Mailchimp/GenericFunctions.ts @@ -45,9 +45,9 @@ export async function mailchimpApiRequest(this: IHookFunctions | IExecuteFunctio return await this.helpers.request!(options); } catch (error) { if (error.response.body && error.response.body.detail) { - throw new Error(`Mailchimp Error: response [${error.statusCode}]: ${error.response.body.detail}`); + throw new Error(`Mailchimp Error response [${error.statusCode}]: ${error.response.body.detail}`); } - throw new Error(error); + throw error; } } From ab1dcf64b9c98f7b24981eb9b58cfae6c7d9e1cb Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Fri, 22 May 2020 17:10:52 -0400 Subject: [PATCH 05/15] :twisted_rightwards_arrows: Feature/mattermost extended (#588) --- .../nodes/Mattermost/Mattermost.node.ts | 112 +++++++++++++++++- 1 file changed, 107 insertions(+), 5 deletions(-) diff --git a/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts b/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts index cc03e53557..0aa50f3d0e 100644 --- a/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts +++ b/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts @@ -98,9 +98,9 @@ export class Mattermost implements INodeType { description: 'Soft-deletes a channel', }, { - name: 'Members', + name: 'Member', value: 'members', - description: 'Returns the members of a channel.', + description: 'Get a page of members for a channel.', }, { name: 'Restore', @@ -317,6 +317,23 @@ export class Mattermost implements INodeType { }, description: 'The Mattermost Team.', }, + { + displayName: 'Resolve Data', + name: 'resolveData', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'members', + ], + }, + }, + default: true, + description: 'By default the response only contain the ID of the user.
If this option gets activated it will resolve the user automatically.', + }, { displayName: 'Return All', name: 'returnAll', @@ -950,6 +967,11 @@ export class Mattermost implements INodeType { value: 'getByEmail', description: 'Get a user by email', }, + { + name: 'Get By ID', + value: 'getById', + description: 'Get a user by id', + }, ], default: '', description: 'The operation to perform.', @@ -1113,6 +1135,54 @@ export class Mattermost implements INodeType { default: '', description: `User's email`, }, + + // ---------------------------------- + // user:getById + // ---------------------------------- + { + displayName: 'User IDs', + name: 'userIds', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getById', + ], + }, + }, + default: '', + description: `User's ID`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getById', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Since', + name: 'since', + type: 'dateTime', + default: '', + description: 'Only return users that have been modified since the given Unix timestamp (in milliseconds).', + }, + ], + }, ], }; @@ -1245,6 +1315,10 @@ export class Mattermost implements INodeType { let resource: string; let requestMethod = 'POST'; let returnAll = false; + let userIds: string[] = []; + + resource = this.getNodeParameter('resource', 0) as string; + operation = this.getNodeParameter('operation', 0) as string; // For Post let body: IDataObject; @@ -1256,9 +1330,6 @@ export class Mattermost implements INodeType { body = {}; qs = {}; - resource = this.getNodeParameter('resource', i) as string; - operation = this.getNodeParameter('operation', i) as string; - if (resource === 'channel') { if (operation === 'create') { // ---------------------------------- @@ -1509,6 +1580,25 @@ export class Mattermost implements INodeType { endpoint = `users/email/${email}`; } + if (operation === 'getById') { + // ---------------------------------- + // user:getById + // ---------------------------------- + userIds = (this.getNodeParameter('userIds', i) as string).split(',') as string[]; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.since) { + qs.since = new Date(additionalFields.since as string).getTime(); + } + + requestMethod = 'POST'; + + endpoint = 'users/ids'; + + //@ts-ignore + body = userIds; + + } } else { throw new Error(`The resource "${resource}" is not known!`); @@ -1519,6 +1609,18 @@ export class Mattermost implements INodeType { responseData = await apiRequestAllItems.call(this, requestMethod, endpoint, body, qs); } else { responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs); + if (resource === 'channel' && operation === 'members') { + const resolveData = this.getNodeParameter('resolveData', i) as boolean; + if (resolveData) { + const userIds: string[] = []; + for (const data of responseData) { + userIds.push(data.user_id); + } + if (userIds.length > 0) { + responseData = await apiRequest.call(this, 'POST', 'users/ids', userIds , qs); + } + } + } } if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData); From b445eafccd69f5edf1fee183b69441c1776be8e2 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 23 May 2020 00:59:40 +0200 Subject: [PATCH 06/15] :zap: Fix some issues with Zulip --- .../nodes/Zulip/GenericFunctions.ts | 1 - .../nodes/Zulip/MessageDescription.ts | 36 +- .../nodes/Zulip/StreamDescription.ts | 589 +++++++++--------- .../nodes-base/nodes/Zulip/StreamInterface.ts | 36 +- .../nodes-base/nodes/Zulip/UserDescription.ts | 282 ++++----- .../nodes-base/nodes/Zulip/UserInterface.ts | 20 +- packages/nodes-base/nodes/Zulip/Zulip.node.ts | 37 +- 7 files changed, 506 insertions(+), 495 deletions(-) diff --git a/packages/nodes-base/nodes/Zulip/GenericFunctions.ts b/packages/nodes-base/nodes/Zulip/GenericFunctions.ts index 45540ddf64..26a57a4155 100644 --- a/packages/nodes-base/nodes/Zulip/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zulip/GenericFunctions.ts @@ -62,4 +62,3 @@ export function validateJSON(json: string | undefined): any { // tslint:disable- } return result; } - diff --git a/packages/nodes-base/nodes/Zulip/MessageDescription.ts b/packages/nodes-base/nodes/Zulip/MessageDescription.ts index 0d06c7c84c..d22df2107c 100644 --- a/packages/nodes-base/nodes/Zulip/MessageDescription.ts +++ b/packages/nodes-base/nodes/Zulip/MessageDescription.ts @@ -51,9 +51,9 @@ export const messageOperations = [ export const messageFields = [ -/* -------------------------------------------------------------------------- */ -/* message:sendPrivate */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* message:sendPrivate */ + /* -------------------------------------------------------------------------- */ { displayName: 'To', name: 'to', @@ -96,9 +96,9 @@ export const messageFields = [ }, description: 'The content of the message.', }, -/* -------------------------------------------------------------------------- */ -/* message:sendStream */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* message:sendStream */ + /* -------------------------------------------------------------------------- */ { displayName: 'Stream', name: 'stream', @@ -163,9 +163,9 @@ export const messageFields = [ }, description: 'The content of the message.', }, -/* -------------------------------------------------------------------------- */ -/* message:update */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* message:update */ + /* -------------------------------------------------------------------------- */ { displayName: 'Message ID', name: 'messageId', @@ -241,9 +241,9 @@ export const messageFields = [ }, ] }, -/* -------------------------------------------------------------------------- */ -/* message:get */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* message:get */ + /* -------------------------------------------------------------------------- */ { displayName: 'Message ID', name: 'messageId', @@ -262,9 +262,9 @@ export const messageFields = [ }, description: 'Unique identifier for the message.', }, -/* -------------------------------------------------------------------------- */ -/* message:delete */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* message:delete */ + /* -------------------------------------------------------------------------- */ { displayName: 'Message ID', name: 'messageId', @@ -283,9 +283,9 @@ export const messageFields = [ }, description: 'Unique identifier for the message.', }, -/* -------------------------------------------------------------------------- */ -/* message:updateFile */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* message:updateFile */ + /* -------------------------------------------------------------------------- */ { displayName: 'Binary Property', name: 'dataBinaryProperty', diff --git a/packages/nodes-base/nodes/Zulip/StreamDescription.ts b/packages/nodes-base/nodes/Zulip/StreamDescription.ts index 777ae7ba61..a3f45fc02e 100644 --- a/packages/nodes-base/nodes/Zulip/StreamDescription.ts +++ b/packages/nodes-base/nodes/Zulip/StreamDescription.ts @@ -13,7 +13,7 @@ export const streamOperations = [ }, }, options: [ - { + { name: 'Create', value: 'create', description: 'Create a stream.', @@ -27,17 +27,17 @@ export const streamOperations = [ name: 'Get All', value: 'getAll', description: 'Get all streams.', - }, - { + }, + { name: 'Get Subscribed', value: 'getSubscribed', description: 'Get subscribed streams.', - }, - { - name: 'Update', - value: 'update', - description: 'Update a stream.', }, + // { + // name: 'Update', + // value: 'update', + // description: 'Update a stream.', + // }, ], default: 'create', description: 'The operation to perform.', @@ -45,10 +45,10 @@ export const streamOperations = [ ] as INodeProperties[]; export const streamFields = [ -/* -------------------------------------------------------------------------- */ -/* stream:create */ -/* -------------------------------------------------------------------------- */ - { + /* -------------------------------------------------------------------------- */ + /* stream:create */ + /* -------------------------------------------------------------------------- */ + { displayName: 'JSON Parameters', name: 'jsonParameters', type: 'boolean', @@ -86,9 +86,56 @@ export const streamFields = [ ], }, }, - description: `JSON format parameters for stream creation.`, }, + { + displayName: 'Subscriptions', + name: 'subscriptions', + type: 'fixedCollection', + default: {}, + displayOptions: { + show: { + resource: [ + 'stream', + ], + operation: [ + 'create', + ], + jsonParameters: [ + false, + ], + }, + }, + required: true, + description: 'A list of dictionaries containing the the key name and value specifying the name of the stream to subscribe. If the stream does not exist a new stream is created.', + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Subscription Properties', + name: 'properties', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + default: '', + description: 'Name of Subscription.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + required: true, + default: '', + description: 'Description of Subscription.', + } + ], + }, + ], + }, { displayName: 'Additional Fields', name: 'additionalFields', @@ -109,38 +156,39 @@ export const streamFields = [ }, }, options: [ - { - displayName: 'Announce', - name: 'announce', - type: 'boolean', - default: false, - description: 'If announce is True and one of the streams specified in subscriptions has to be created (i.e. doesnt exist to begin with), an announcement will be made notifying that a new stream was created.', - }, - { - displayName: 'Authorization Errors Fatal', - name: 'authorizationErrorsFatal', - type: 'boolean', - default: false, - description: 'A boolean specifying whether authorization errors (such as when the requesting user is not authorized to access a private stream) should be considered fatal or not. When True, an authorization error is reported as such. When set to False, the returned JSON payload indicates that there was an authorization error, but the response is still considered a successful one.', - }, - { - displayName: 'History Public to Subscribers', - name: 'historyPublicToSubscribers', - type: 'boolean', - default: false, - description: 'Whether the streams message history should be available to newly subscribed members, or users can only access messages they actually received while subscribed to the stream.', - }, - { - displayName: 'Invite Only', - name: 'inviteOnly', - type: 'boolean', - default: false, - description: 'A boolean specifying whether the streams specified in subscriptions are invite-only or not.', - }, - { + { + displayName: 'Announce', + name: 'announce', + type: 'boolean', + default: false, + description: 'If announce is True and one of the streams specified in subscriptions has to be created (i.e. doesnt exist to begin with), an announcement will be made notifying that a new stream was created.', + }, + { + displayName: 'Authorization Errors Fatal', + name: 'authorizationErrorsFatal', + type: 'boolean', + default: false, + description: 'A boolean specifying whether authorization errors (such as when the requesting user is not authorized to access a private stream) should be considered fatal or not. When True, an authorization error is reported as such. When set to False, the returned JSON payload indicates that there was an authorization error, but the response is still considered a successful one.', + }, + { + displayName: 'History Public to Subscribers', + name: 'historyPublicToSubscribers', + type: 'boolean', + default: false, + description: 'Whether the streams message history should be available to newly subscribed members, or users can only access messages they actually received while subscribed to the stream.', + }, + { + displayName: 'Invite Only', + name: 'inviteOnly', + type: 'boolean', + default: false, + description: 'A boolean specifying whether the streams specified in subscriptions are invite-only or not.', + }, + { displayName: 'Principals', name: 'principals', type: 'fixedCollection', + default: {}, description: 'A list of email addresses of the users that will be subscribed/unsubscribed to the streams specified in the subscriptions argument. If not provided, then the requesting user/bot is subscribed.', typeOptions: { multipleValues: true, @@ -161,175 +209,144 @@ export const streamFields = [ ], }, ], - }, - { - displayName: 'Subscriptions', - name: 'subscriptions', - type: 'fixedCollection', - required: true, - description: '"A list of dictionaries containing the the key name and value specifying the name of the stream to subscribe. If the stream does not exist a new stream is created.', - typeOptions: { - multipleValues: true, - }, - options: [ - { - displayName: 'Subscription Properties', - name: 'properties', - values: [ - { - displayName: 'Name', - name: 'name', - type: 'string', - required: true, - default: '', - description: 'Name of Subscription.', - }, - { - displayName: 'Description', - name: 'description', - type: 'string', - required: true, - default: '', - description: 'Description of Subscription.', - } - ], - }, - ], - }, - { + }, + { displayName: 'Stream Post Policy', name: 'streamPostPolicy', - type: 'options', - default: '', + type: 'options', + default: '', description: 'Policy for which users can post messages to the stream.', options: [ - { - name: '1', - value: 1, - description: 'Any user can post.' - }, - { - name: '2', - value: 2, - description: 'Only administrators can post.' - }, - { - name: '3', - value: 3, - description: 'Only new members can post.' - }, + { + name: '1', + value: 1, + description: 'Any user can post.' + }, + { + name: '2', + value: 2, + description: 'Only administrators can post.' + }, + { + name: '3', + value: 3, + description: 'Only new members can post.' + }, ], - } - ] - }, -/* -------------------------------------------------------------------------- */ -/* stream:get all */ -/* -------------------------------------------------------------------------- */ - { - displayName: 'Additional Fields', - name: 'additionalFields', - type: 'collection', - placeholder: 'Add Field', - default: {}, - displayOptions: { - show: { - resource: [ - 'stream', - ], - operation: [ - 'getAll', - ], - }, - }, - options: [ - { - displayName: 'Include All Active', - name: 'includeAllActive', - type: 'boolean', - default: true, - description: 'Include all active streams. The user must have administrative privileges to use this parameter.', - }, - { - displayName: 'Include Default', - name: 'includeDefault', - type: 'boolean', - default: true, - description: 'Include all default streams for the users realm.', - }, - { - displayName: 'Include Owner Subscribed', - name: 'includeOwnersubscribed', - type: 'boolean', - default: true, - description: 'If the user is a bot, include all streams that the bots owner is subscribed to.', - }, - { - displayName: 'Include Public', - name: 'includePublic', - type: 'boolean', - default: true, - description: 'Include all public streams.', - }, - { - displayName: 'Include Subscribed', - name: 'includeSubscribed', - type: 'boolean', - default: true, - description: 'Include all streams that the user is subscribed to.', - }, - ] - }, -/* -------------------------------------------------------------------------- */ -/* stream:get subscribed */ -/* -------------------------------------------------------------------------- */ - { - displayName: 'Additional Fields', - name: 'additionalFields', - type: 'collection', - placeholder: 'Add Field', - default: {}, - displayOptions: { - show: { - resource: [ - 'stream', - ], - operation: [ - 'getSubscribed', - ], - }, - }, - options: [ - { - displayName: 'Include Subscribers', - name: 'includeSubscribers', - type: 'boolean', - default: true, - description: 'Whether each returned stream object should include a subscribers field containing a list of the user IDs of its subscribers.', - } - ] - }, -/* -------------------------------------------------------------------------- */ -/* stream:update */ -/* -------------------------------------------------------------------------- */ - { - displayName: 'Stream ID', - name: 'streamId', - type: 'string', - required: true, - default: '', - displayOptions: { - show: { - resource: [ - 'stream', - ], - operation: [ - 'update', - ], - }, - }, - description: 'ID of stream to update.', - }, - { + } + ] + }, + + /* -------------------------------------------------------------------------- */ + /* stream:get all */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'stream', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Include All Active', + name: 'includeAllActive', + type: 'boolean', + default: true, + description: 'Include all active streams. The user must have administrative privileges to use this parameter.', + }, + { + displayName: 'Include Default', + name: 'includeDefault', + type: 'boolean', + default: true, + description: 'Include all default streams for the users realm.', + }, + { + displayName: 'Include Owner Subscribed', + name: 'includeOwnersubscribed', + type: 'boolean', + default: true, + description: 'If the user is a bot, include all streams that the bots owner is subscribed to.', + }, + { + displayName: 'Include Public', + name: 'includePublic', + type: 'boolean', + default: true, + description: 'Include all public streams.', + }, + { + displayName: 'Include Subscribed', + name: 'includeSubscribed', + type: 'boolean', + default: true, + description: 'Include all streams that the user is subscribed to.', + }, + ] + }, + + /* -------------------------------------------------------------------------- */ + /* stream:get subscribed */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'stream', + ], + operation: [ + 'getSubscribed', + ], + }, + }, + options: [ + { + displayName: 'Include Subscribers', + name: 'includeSubscribers', + type: 'boolean', + default: true, + description: 'Whether each returned stream object should include a subscribers field containing a list of the user IDs of its subscribers.', + } + ] + }, + + /* -------------------------------------------------------------------------- */ + /* stream:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Stream ID', + name: 'streamId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'stream', + ], + operation: [ + 'update', + ], + }, + }, + description: 'ID of stream to update.', + }, + { displayName: 'JSON Parameters', name: 'jsonParameters', type: 'boolean', @@ -391,89 +408,89 @@ export const streamFields = [ }, }, options: [ - { - displayName: 'Announcement Only', - name: 'isAnnouncementOnly', - type: 'boolean', - default: false, - description: 'Whether the stream is limited to announcements.', - }, - { - displayName: 'Description', - name: 'description', - type: 'string', - default: '', - description: 'The new description for the stream.', - placeholder: 'Place of discussion' - }, - { - displayName: 'Is Private', - name: 'isPrivate', - type: 'boolean', - default: false, - description: 'Change whether the stream is a private stream.', - }, - { - displayName: 'History Public to Subscribers', - name: 'historyPublicToSubscribers', - type: 'boolean', - default: false, - description: 'Whether the streams message history should be available to newly subscribed members, or users can only access messages they actually received while subscribed to the stream.', - }, - { - displayName: 'New Name', - name: 'newName', - type: 'string', - default: '', - description: 'The new name for the stream.', - placeholder: 'Italy' - }, - { + { + displayName: 'Announcement Only', + name: 'isAnnouncementOnly', + type: 'boolean', + default: false, + description: 'Whether the stream is limited to announcements.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'The new description for the stream.', + placeholder: 'Place of discussion' + }, + { + displayName: 'Is Private', + name: 'isPrivate', + type: 'boolean', + default: false, + description: 'Change whether the stream is a private stream.', + }, + { + displayName: 'History Public to Subscribers', + name: 'historyPublicToSubscribers', + type: 'boolean', + default: false, + description: 'Whether the streams message history should be available to newly subscribed members, or users can only access messages they actually received while subscribed to the stream.', + }, + { + displayName: 'New Name', + name: 'newName', + type: 'string', + default: '', + description: 'The new name for the stream.', + placeholder: 'Italy' + }, + { displayName: 'Stream Post Policy', name: 'streamPostPolicy', - type: 'options', - default: '', + type: 'options', + default: '', description: 'Policy for which users can post messages to the stream.', options: [ - { - name: '1', - value: 1, - description: 'Any user can post.' - }, - { - name: '2', - value: 2, - description: 'Only administrators can post.' - }, - { - name: '3', - value: 3, - description: 'Only new members can post.' - }, + { + name: '1', + value: 1, + description: 'Any user can post.' + }, + { + name: '2', + value: 2, + description: 'Only administrators can post.' + }, + { + name: '3', + value: 3, + description: 'Only new members can post.' + }, ], - }, - ] - }, -/* -------------------------------------------------------------------------- */ -/* stream:delete */ -/* -------------------------------------------------------------------------- */ - { - displayName: 'Stream ID', - name: 'streamId', - type: 'string', - required: true, - default: '', - displayOptions: { - show: { - resource: [ - 'stream', - ], - operation: [ - 'delete', - ], - }, - }, - description: 'ID of stream to delete.', - }, + }, + ] + }, + /* -------------------------------------------------------------------------- */ + /* stream:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Stream ID', + name: 'streamId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'stream', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'ID of stream to delete.', + }, -] as INodeProperties[]; \ No newline at end of file +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zulip/StreamInterface.ts b/packages/nodes-base/nodes/Zulip/StreamInterface.ts index 9db2fa6bcb..f1a1b41d02 100644 --- a/packages/nodes-base/nodes/Zulip/StreamInterface.ts +++ b/packages/nodes-base/nodes/Zulip/StreamInterface.ts @@ -1,23 +1,23 @@ export interface IStream { - subscriptions?: string; - invite_only?: boolean; - principals?: string; - authorization_errors_fatal?: boolean; - history_public_to_subscribers?: boolean; - stream_post_policy?: number; - announce?: boolean; - include_public?: boolean; - include_subscribed?: boolean; - include_all_active?: boolean; - include_default?: boolean; - include_owner_subscribed?: boolean; - include_subscribers?: boolean; - description?: string; - new_name?: string; - is_private?: boolean; - is_announcement_only?: boolean; + subscriptions?: string; + invite_only?: boolean; + principals?: string; + authorization_errors_fatal?: boolean; + history_public_to_subscribers?: boolean; + stream_post_policy?: number; + announce?: boolean; + include_public?: boolean; + include_subscribed?: boolean; + include_all_active?: boolean; + include_default?: boolean; + include_owner_subscribed?: boolean; + include_subscribers?: boolean; + description?: string; + new_name?: string; + is_private?: boolean; + is_announcement_only?: boolean; } export interface IPrincipal { - email: string; + email: string; } diff --git a/packages/nodes-base/nodes/Zulip/UserDescription.ts b/packages/nodes-base/nodes/Zulip/UserDescription.ts index 1ae3269d3e..5a30b9c0ae 100644 --- a/packages/nodes-base/nodes/Zulip/UserDescription.ts +++ b/packages/nodes-base/nodes/Zulip/UserDescription.ts @@ -13,7 +13,7 @@ export const userOperations = [ }, }, options: [ - { + { name: 'Create', value: 'create', description: 'Create a user.', @@ -22,22 +22,22 @@ export const userOperations = [ name: 'Deactivate', value: 'deactivate', description: 'Deactivate a user.', - }, - { + }, + { name: 'Get', value: 'get', description: 'Get a user.', - }, + }, { name: 'Get All', value: 'getAll', description: 'Get all users.', - }, - { - name: 'Update', - value: 'update', - description: 'Update a user.', }, + // { + // name: 'Update', + // value: 'update', + // description: 'Update a user.', + // }, ], default: 'create', description: 'The operation to perform.', @@ -45,9 +45,9 @@ export const userOperations = [ ] as INodeProperties[]; export const userFields = [ -/* -------------------------------------------------------------------------- */ -/* user:create */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* user:create */ + /* -------------------------------------------------------------------------- */ { displayName: 'Email', name: 'email', @@ -65,8 +65,8 @@ export const userFields = [ }, default: '', description: 'The email address of the new user.', - }, - { + }, + { displayName: 'Full Name', name: 'fullName', type: 'string', @@ -83,8 +83,8 @@ export const userFields = [ }, default: '', description: 'The full name of the new user.', - }, - { + }, + { displayName: 'Password', name: 'password', type: 'string', @@ -101,8 +101,8 @@ export const userFields = [ }, default: '', description: 'The password of the new user.', - }, - { + }, + { displayName: 'Short Name', name: 'shortName', type: 'string', @@ -119,10 +119,11 @@ export const userFields = [ }, default: '', description: 'The short name of the new user. Not user-visible.', - }, -/* -------------------------------------------------------------------------- */ -/* user:get / getAll */ -/* -------------------------------------------------------------------------- */ + }, + + /* -------------------------------------------------------------------------- */ + /* user:get / getAll */ + /* -------------------------------------------------------------------------- */ { displayName: 'User ID', name: 'userId', @@ -140,103 +141,105 @@ export const userFields = [ }, default: '', description: 'The ID of user to get.', - }, - { - displayName: 'Additional Fields', - name: 'additionalFields', - type: 'collection', - placeholder: 'Add Field', - default: {}, - displayOptions: { - show: { - resource: [ - 'user', - ], - operation: [ - 'get', 'getAll' - ], - }, - }, - options: [ - { - displayName: 'Client Gravatar', - name: 'clientGravatar', - type: 'boolean', - default: false, - description: 'Whether the client supports computing gravatars URLs. If enabled, avatar_url will be included in the response only if there is a Zulip avatar, and will be null for users who are using gravatar as their avatar.', - }, - { - displayName: 'Custom Profile Fields', - name: 'includeCustomProfileFields', - type: 'boolean', - default: false, - description: 'Whether the client wants custom profile field data to be included in the response.', - }, - ] - }, -/* -------------------------------------------------------------------------- */ -/* user:update */ -/* -------------------------------------------------------------------------- */ - { - displayName: 'User ID', - name: 'userId', - type: 'string', - required: true, - displayOptions: { - show: { - resource: [ - 'user', - ], - operation: [ - 'update', - ], - }, - }, - default: '', - description: 'The ID of user to update.', - }, - { - displayName: 'Additional Fields', - name: 'additionalFields', - type: 'collection', - placeholder: 'Add Field', - default: {}, - displayOptions: { - show: { - resource: [ - 'user', - ], - operation: [ - 'update' - ], - }, - }, - options: [ - { - displayName: 'Full Name', - name: 'fullName', - type: 'string', - default: '', - description: 'The users full name.', - }, - { - displayName: 'Is Admin', - name: 'isAdmin', - type: 'boolean', - default: false, - description: 'Whether the target user is an administrator.', - }, - { - displayName: 'Is Guest', - name: 'isGuest', - type: 'boolean', - default: false, - description: 'Whether the target user is a guest.', - }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'get', 'getAll' + ], + }, + }, + options: [ + { + displayName: 'Client Gravatar', + name: 'clientGravatar', + type: 'boolean', + default: false, + description: 'Whether the client supports computing gravatars URLs. If enabled, avatar_url will be included in the response only if there is a Zulip avatar, and will be null for users who are using gravatar as their avatar.', + }, + { + displayName: 'Custom Profile Fields', + name: 'includeCustomProfileFields', + type: 'boolean', + default: false, + description: 'Whether the client wants custom profile field data to be included in the response.', + }, + ] + }, + + /* -------------------------------------------------------------------------- */ + /* user:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'User ID', + name: 'userId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'The ID of user to update.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'update' + ], + }, + }, + options: [ + { + displayName: 'Full Name', + name: 'fullName', + type: 'string', + default: '', + description: 'The users full name.', + }, + { + displayName: 'Is Admin', + name: 'isAdmin', + type: 'boolean', + default: false, + description: 'Whether the target user is an administrator.', + }, + { + displayName: 'Is Guest', + name: 'isGuest', + type: 'boolean', + default: false, + description: 'Whether the target user is a guest.', + }, { displayName: 'Profile Data', name: 'profileData', type: 'fixedCollection', + default: {}, description: 'A dictionary containing the to be updated custom profile field data for the user.', typeOptions: { multipleValues: true, @@ -265,27 +268,28 @@ export const userFields = [ }, ], }, - ] - }, -/* -------------------------------------------------------------------------- */ -/* user:deactivate */ -/* -------------------------------------------------------------------------- */ -{ - displayName: 'User ID', - name: 'userId', - type: 'string', - required: true, - displayOptions: { - show: { - resource: [ - 'user', - ], - operation: [ - 'deactivate', - ], - }, - }, - default: '', - description: 'The ID of user to deactivate.', -}, -] as INodeProperties[]; \ No newline at end of file + ] + }, + + /* -------------------------------------------------------------------------- */ + /* user:deactivate */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'User ID', + name: 'userId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'deactivate', + ], + }, + }, + default: '', + description: 'The ID of user to deactivate.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zulip/UserInterface.ts b/packages/nodes-base/nodes/Zulip/UserInterface.ts index 1fddf8ab42..fd7dffcea9 100644 --- a/packages/nodes-base/nodes/Zulip/UserInterface.ts +++ b/packages/nodes-base/nodes/Zulip/UserInterface.ts @@ -1,11 +1,11 @@ export interface IUser { - client_gravatar?: boolean; - include_custom_profile_fields?: boolean; - full_name?: string; - is_admin?: boolean; - is_guest?: boolean; - profile_data?: [{}]; - email?: string; - password?: string; - short_name?: string; -} \ No newline at end of file + client_gravatar?: boolean; + include_custom_profile_fields?: boolean; + full_name?: string; + is_admin?: boolean; + is_guest?: boolean; + profile_data?: [{}]; + email?: string; + password?: string; + short_name?: string; +} diff --git a/packages/nodes-base/nodes/Zulip/Zulip.node.ts b/packages/nodes-base/nodes/Zulip/Zulip.node.ts index 5dbcc67715..144452864f 100644 --- a/packages/nodes-base/nodes/Zulip/Zulip.node.ts +++ b/packages/nodes-base/nodes/Zulip/Zulip.node.ts @@ -219,7 +219,7 @@ export class Zulip implements INodeType { } } }; - responseData = await zulipApiRequest.call(this, 'POST', '/user_uploads', {}, {}, undefined, { formData } ); + responseData = await zulipApiRequest.call(this, 'POST', '/user_uploads', {}, {}, undefined, { formData }); responseData.uri = `${credentials!.url}${responseData.uri}`; } } @@ -247,6 +247,7 @@ export class Zulip implements INodeType { } responseData = await zulipApiRequest.call(this, 'GET', `/streams`, body); + responseData = responseData.streams; } if (operation === 'getSubscribed') { @@ -257,10 +258,10 @@ export class Zulip implements INodeType { } responseData = await zulipApiRequest.call(this, 'GET', `/users/me/subscriptions`, body); + responseData = responseData.subscriptions; } if (operation === 'create') { - const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; if (jsonParameters) { @@ -278,20 +279,18 @@ export class Zulip implements INodeType { } } else { - const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; - if (additionalFields.subscriptions) { - //@ts-ignore - body.subscriptions = JSON.stringify(additionalFields.subscriptions.properties); - } + const subscriptions = this.getNodeParameter('subscriptions', i) as IDataObject; + body.subscriptions = JSON.stringify(subscriptions.properties); + if (additionalFields.inviteOnly) { body.invite_only = additionalFields.inviteOnly as boolean; } if (additionalFields.principals) { - const principals : string[] = []; + const principals: string[] = []; //@ts-ignore - additionalFields.principals.properties.map((principal : IPrincipal) => { + additionalFields.principals.properties.map((principal: IPrincipal) => { principals.push(principal.email); }); body.principals = JSON.stringify(principals); @@ -368,7 +367,7 @@ export class Zulip implements INodeType { } if (resource === 'user') { - const body : IUser = {}; + const body: IUser = {}; if (operation === 'get') { const userId = this.getNodeParameter('userId', i) as string; @@ -396,6 +395,7 @@ export class Zulip implements INodeType { } responseData = await zulipApiRequest.call(this, 'GET', `/users`, body); + responseData = responseData.members; } if (operation === 'create') { @@ -407,7 +407,7 @@ export class Zulip implements INodeType { responseData = await zulipApiRequest.call(this, 'POST', `/users`, body); } - if (operation === 'update') { + if (operation === 'update') { const userId = this.getNodeParameter('userId', i) as string; const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; @@ -434,19 +434,10 @@ export class Zulip implements INodeType { responseData = await zulipApiRequest.call(this, 'DELETE', `/users/${userId}`, body); } } - // Specific checks because API returns multiple objects within 1 object with each key name - if (responseData.members) { - returnData.push.apply(returnData, responseData.members as IDataObject[]); - } - if (responseData.streams) { - returnData.push.apply(returnData, responseData.streams as IDataObject[]); - } - - if (responseData.subscriptions) { - returnData.push.apply(returnData, responseData.subscriptions as IDataObject[]); - } - else { + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { returnData.push(responseData as IDataObject); } } From d79f22d0dbb1f77bfb96456a87cae605c796420d Mon Sep 17 00:00:00 2001 From: Allan Clempe Date: Sat, 23 May 2020 09:09:05 +1000 Subject: [PATCH 07/15] :zap: Changed glob pattern to also search nodes in subfolders. (#574) --- packages/cli/src/LoadNodesAndCredentials.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/LoadNodesAndCredentials.ts b/packages/cli/src/LoadNodesAndCredentials.ts index eccfcb0804..0d0eb21df3 100644 --- a/packages/cli/src/LoadNodesAndCredentials.ts +++ b/packages/cli/src/LoadNodesAndCredentials.ts @@ -192,7 +192,7 @@ class LoadNodesAndCredentialsClass { * @memberof N8nPackagesInformationClass */ async loadDataFromDirectory(setPackageName: string, directory: string): Promise { - const files = await glob(path.join(directory, '*\.@(node|credentials)\.js')); + const files = await glob(path.join(directory, '**/*\.@(node|credentials)\.js')); let fileName: string; let type: string; From c6281f2b0ef3f29f9e93b52948c05141babb16ae Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 23 May 2020 23:54:56 +0200 Subject: [PATCH 08/15] :zap: Use only JSON-Data for display-size-calculation --- packages/editor-ui/src/components/RunData.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index 245f31b500..0727699e2c 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -450,7 +450,10 @@ export default mixins( // Check how much data there is to display const inputData = this.getNodeInputData(this.node, this.runIndex, this.outputIndex); - this.dataSize = JSON.stringify(inputData).length; + + const jsonItems = inputData.map(item => item.json); + + this.dataSize = JSON.stringify(jsonItems).length; if (this.dataSize < 204800) { // Data is reasonable small (< 200kb) so display it directly From 2cdfb69e0c0dfad63f410cd592ca6653c49b53c9 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 24 May 2020 00:53:06 +0200 Subject: [PATCH 09/15] :zap: Improve data display for large amount of data --- packages/editor-ui/src/components/RunData.vue | 40 ++++++++++++++++--- packages/editor-ui/src/constants.ts | 2 + 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index 0727699e2c..b3729e4f68 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -19,7 +19,16 @@
- Results: {{ dataCount }}  + + Results: {{ dataCount }} + + Results: + + +  / + {{ dataCount }} + +   option <= this.dataCount); + }, node (): INodeUi | null { return this.$store.getters.activeNode; }, @@ -323,19 +344,27 @@ export default mixins( return 0; }, jsonData (): IDataObject[] { - const inputData = this.getNodeInputData(this.node, this.runIndex, this.outputIndex); + let inputData = this.getNodeInputData(this.node, this.runIndex, this.outputIndex); if (inputData.length === 0 || !Array.isArray(inputData)) { return []; } + if (this.maxDisplayItems !== null) { + inputData = inputData.slice(0, this.maxDisplayItems); + } + return this.convertToJson(inputData); }, tableData (): ITableData | undefined { - const inputData = this.getNodeInputData(this.node, this.runIndex, this.outputIndex); + let inputData = this.getNodeInputData(this.node, this.runIndex, this.outputIndex); if (inputData.length === 0) { return undefined; } + if (this.maxDisplayItems !== null) { + inputData = inputData.slice(0,this.maxDisplayItems); + } + return this.convertToTable(inputData); }, binaryData (): IBinaryKeyData[] { @@ -451,11 +480,11 @@ export default mixins( // Check how much data there is to display const inputData = this.getNodeInputData(this.node, this.runIndex, this.outputIndex); - const jsonItems = inputData.map(item => item.json); + const jsonItems = inputData.slice(0, this.maxDisplayItems || inputData.length).map(item => item.json); this.dataSize = JSON.stringify(jsonItems).length; - if (this.dataSize < 204800) { + if (this.dataSize < this.MAX_DISPLAY_DATA_SIZE) { // Data is reasonable small (< 200kb) so display it directly this.showData = true; } @@ -469,6 +498,7 @@ export default mixins( node (newNode, oldNode) { // Reset the selected output index every time another node gets selected this.outputIndex = 0; + this.maxDisplayItems = 25; this.refreshDataSize(); }, jsonData () { diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index 90f8808986..22ec0a5198 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -1,2 +1,4 @@ +export const MAX_DISPLAY_DATA_SIZE = 204800; +export const MAX_DISPLAY_ITEMS_AUTO_ALL = 250; export const NODE_NAME_PREFIX = 'node-'; export const PLACEHOLDER_EMPTY_WORKFLOW_ID = '__EMPTY__'; From 5718330e2909a350ea02e09862858c45bd8185e7 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 24 May 2020 14:06:22 +0200 Subject: [PATCH 10/15] :bug: Fix issues in read-only mode #39 --- .../src/components/mixins/nodeBase.ts | 105 +++++++++--------- packages/editor-ui/src/views/NodeView.vue | 104 +++++++++-------- 2 files changed, 112 insertions(+), 97 deletions(-) diff --git a/packages/editor-ui/src/components/mixins/nodeBase.ts b/packages/editor-ui/src/components/mixins/nodeBase.ts index ccefba5463..03ad84db00 100644 --- a/packages/editor-ui/src/components/mixins/nodeBase.ts +++ b/packages/editor-ui/src/components/mixins/nodeBase.ts @@ -65,6 +65,7 @@ export const nodeBase = mixins(nodeIndex).extend({ 'name', 'nodeId', 'instance', + 'isReadOnly', ], methods: { __addNode (node: INodeUi) { @@ -182,7 +183,7 @@ export const nodeBase = mixins(nodeIndex).extend({ endpoint: inputData.endpoint, endpointStyle: inputData.endpointStyle, isSource: false, - isTarget: true, + isTarget: !this.isReadOnly, parameters: { nodeIndex: this.nodeIndex, type: inputName, @@ -246,7 +247,7 @@ export const nodeBase = mixins(nodeIndex).extend({ maxConnections: inputData.maxConnections, endpoint: inputData.endpoint, endpointStyle: inputData.endpointStyle, - isSource: true, + isSource: !this.isReadOnly, isTarget: false, parameters: { nodeIndex: this.nodeIndex, @@ -275,61 +276,63 @@ export const nodeBase = mixins(nodeIndex).extend({ this.instance.addEndpoint(this.nodeName, newEndpointData); }); - // Make nodes draggable - this.instance.draggable(this.nodeName, { - grid: [10, 10], - start: (params: { e: MouseEvent }) => { - if (params.e && !this.$store.getters.isNodeSelected(this.data.name)) { - // Only the node which gets dragged directly gets an event, for all others it is - // undefined. So check if the currently dragged node is selected and if not clear - // the drag-selection. - this.instance.clearDragSelection(); - this.$store.commit('resetSelectedNodes'); - } - - this.$store.commit('addActiveAction', 'dragActive'); - }, - stop: (params: { e: MouseEvent}) => { - if (this.$store.getters.isActionActive('dragActive')) { - const moveNodes = this.$store.getters.getSelectedNodes.slice(); - const selectedNodeNames = moveNodes.map((node: INodeUi) => node.name); - if (!selectedNodeNames.includes(this.data.name)) { - // If the current node is not in selected add it to the nodes which - // got moved manually - moveNodes.push(this.data); + if (this.isReadOnly === false) { + // Make nodes draggable + this.instance.draggable(this.nodeName, { + grid: [10, 10], + start: (params: { e: MouseEvent }) => { + if (params.e && !this.$store.getters.isNodeSelected(this.data.name)) { + // Only the node which gets dragged directly gets an event, for all others it is + // undefined. So check if the currently dragged node is selected and if not clear + // the drag-selection. + this.instance.clearDragSelection(); + this.$store.commit('resetSelectedNodes'); } - // This does for some reason just get called once for the node that got clicked - // even though "start" and "drag" gets called for all. So lets do for now - // some dirty DOM query to get the new positions till I have more time to - // create a proper solution - let newNodePositon: XYPositon; - moveNodes.forEach((node: INodeUi) => { - const nodeElement = `node-${this.getNodeIndex(node.name)}`; - const element = document.getElementById(nodeElement); - if (element === null) { - return; + this.$store.commit('addActiveAction', 'dragActive'); + }, + stop: (params: { e: MouseEvent }) => { + if (this.$store.getters.isActionActive('dragActive')) { + const moveNodes = this.$store.getters.getSelectedNodes.slice(); + const selectedNodeNames = moveNodes.map((node: INodeUi) => node.name); + if (!selectedNodeNames.includes(this.data.name)) { + // If the current node is not in selected add it to the nodes which + // got moved manually + moveNodes.push(this.data); } - newNodePositon = [ - parseInt(element.style.left!.slice(0, -2), 10), - parseInt(element.style.top!.slice(0, -2), 10), - ]; + // This does for some reason just get called once for the node that got clicked + // even though "start" and "drag" gets called for all. So lets do for now + // some dirty DOM query to get the new positions till I have more time to + // create a proper solution + let newNodePositon: XYPositon; + moveNodes.forEach((node: INodeUi) => { + const nodeElement = `node-${this.getNodeIndex(node.name)}`; + const element = document.getElementById(nodeElement); + if (element === null) { + return; + } - const updateInformation = { - name: node.name, - properties: { - // @ts-ignore, draggable does not have definitions - position: newNodePositon, - }, - }; + newNodePositon = [ + parseInt(element.style.left!.slice(0, -2), 10), + parseInt(element.style.top!.slice(0, -2), 10), + ]; - this.$store.commit('updateNodeProperties', updateInformation); - }); - } - }, - filter: '.node-description, .node-description .node-name, .node-description .node-subtitle', - }); + const updateInformation = { + name: node.name, + properties: { + // @ts-ignore, draggable does not have definitions + position: newNodePositon, + }, + }; + + this.$store.commit('updateNodeProperties', updateInformation); + }); + } + }, + filter: '.node-description, .node-description .node-name, .node-description .node-subtitle', + }); + } }, isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean { diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 03f1ff28d1..de89534e09 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -20,6 +20,7 @@ :id="'node-' + getNodeIndex(nodeData.name)" :key="getNodeIndex(nodeData.name)" :name="nodeData.name" + :isReadOnly="isReadOnly" :instance="instance" >
@@ -102,6 +103,9 @@