From 31af36b8bde129924053049038b8b3dd0d6cc6e9 Mon Sep 17 00:00:00 2001 From: Romain Dunand Date: Sun, 1 Dec 2019 23:08:37 +0100 Subject: [PATCH 001/389] Fix #139 Prevent null values from beeing treaded as objects --- packages/workflow/src/Workflow.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/workflow/src/Workflow.ts b/packages/workflow/src/Workflow.ts index 9efaaa67ec..b48e380edf 100644 --- a/packages/workflow/src/Workflow.ts +++ b/packages/workflow/src/Workflow.ts @@ -865,16 +865,16 @@ export class Workflow { // Execute the expression try { const returnValue = tmpl.tmpl(parameterValue, dataProxy.getDataProxy()); - if (typeof returnValue === 'object' && Object.keys(returnValue).length === 0) { - // When expression is incomplete it returns a Proxy which causes problems. - // Catch it with this code and return a proper error. - throw new Error('Expression is not valid.'); + if (returnValue !== null && typeof returnValue === 'object') { + if (Object.keys(returnValue).length === 0) { + // When expression is incomplete it returns a Proxy which causes problems. + // Catch it with this code and return a proper error. + throw new Error('Expression is not valid.'); + } + if (returnObjectAsString === true) { + return this.convertObjectValueToString(returnValue); + } } - - if (returnObjectAsString === true && typeof returnValue === 'object') { - return this.convertObjectValueToString(returnValue); - } - return returnValue; } catch (e) { throw new Error('Expression is not valid.'); From e4eefedcbfa5d764e2bb0edb661de1ef84b66b98 Mon Sep 17 00:00:00 2001 From: Priyanka P Date: Fri, 13 Dec 2019 16:59:33 +0530 Subject: [PATCH 002/389] Msg91 Integration(node) --- .../credentials/Msg91Api.credentials.ts | 19 ++ .../nodes/Msg91/GenericFunctions.ts | 64 ++++++ packages/nodes-base/nodes/Msg91/Msg91.node.ts | 186 ++++++++++++++++++ packages/nodes-base/nodes/Msg91/msg91.png | Bin 0 -> 2982 bytes packages/nodes-base/package.json | 2 + 5 files changed, 271 insertions(+) create mode 100644 packages/nodes-base/credentials/Msg91Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Msg91/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Msg91/Msg91.node.ts create mode 100644 packages/nodes-base/nodes/Msg91/msg91.png diff --git a/packages/nodes-base/credentials/Msg91Api.credentials.ts b/packages/nodes-base/credentials/Msg91Api.credentials.ts new file mode 100644 index 0000000000..d4f96a1ca9 --- /dev/null +++ b/packages/nodes-base/credentials/Msg91Api.credentials.ts @@ -0,0 +1,19 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class Msg91Api implements ICredentialType { + name = 'msg91Api'; + displayName = 'Msg91 Api'; + properties = [ + // User authentication key + { + displayName: 'Authentication Key', + name: 'authkey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Msg91/GenericFunctions.ts b/packages/nodes-base/nodes/Msg91/GenericFunctions.ts new file mode 100644 index 0000000000..59a5831bf4 --- /dev/null +++ b/packages/nodes-base/nodes/Msg91/GenericFunctions.ts @@ -0,0 +1,64 @@ +import { + IExecuteFunctions, + IHookFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +/** + * Make an API request to MSG91 + * + * @param {IHookFunctions} this + * @param {string} method + * @param {string} url + * @param {object} body + * @returns {Promise} + */ +export async function msg91ApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: IDataObject, query?: IDataObject): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('msg91Api'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + if (query === undefined) { + query = {}; + } + + query.authkey = credentials.authkey as string; + + const options = { + method, + form: body, + qs: query, + uri: `https://api.msg91.com/api/sendhttp.php`, + auth: { + user: '', + pass: '', + }, + json: true + }; + + try { + return await this.helpers.request(options); + } catch (error) { + if (error.statusCode === 401) { + // Return a clear error + throw new Error('The MSG91 credentials are not valid!'); + } + + if (error.response && error.response.body && error.response.body.message) { + // Try to return the error prettier + let errorMessage = `MSG91 error response [${error.statusCode}]: ${error.response.body.message}`; + if (error.response.body.more_info) { + errorMessage = `errorMessage (${error.response.body.more_info})`; + } + + throw new Error(errorMessage); + } + + // If that data does not exist for some reason return the actual error + throw error; + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Msg91/Msg91.node.ts b/packages/nodes-base/nodes/Msg91/Msg91.node.ts new file mode 100644 index 0000000000..f59dd49ece --- /dev/null +++ b/packages/nodes-base/nodes/Msg91/Msg91.node.ts @@ -0,0 +1,186 @@ +import { IExecuteFunctions } from 'n8n-core'; +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + msg91ApiRequest, +} from './GenericFunctions'; + + +export class Msg91 implements INodeType { + description: INodeTypeDescription = { + displayName: 'Msg91', + name: 'msg91', + icon: 'file:msg91.png', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Send Transactional SMS', + defaults: { + name: 'Msg91', + color: '#0000ff', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'msg91Api', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'SMS', + value: 'sms', + }, + ], + default: 'sms', + description: 'The resource to operate on.', + }, + + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'sms', + ], + }, + }, + options: [ + { + name: 'Send', + value: 'send', + description: 'Send SMS', + }, + ], + default: 'send', + description: 'The operation to perform.', + }, + { + displayName: 'Sender', + name: 'sender', + type: 'string', + default: '', + placeholder: '+14155238886', + required: true, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'sms', + ], + }, + }, + description: 'The number from which to send the message', + }, + { + displayName: 'To', + name: 'mobiles', + type: 'string', + default: '', + placeholder: 'Mobile Number With Country Code', + required: true, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'sms', + ], + }, + }, + description: 'The number to which to send the message', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'sms', + ], + }, + }, + description: 'The message to send', + }, + ] + }; + + + async execute(this: IExecuteFunctions): Promise { + + const items = this.getInputData(); + const returnData: IDataObject[] = []; + + let operation: string; + let resource: string; + + // For Post + let body: IDataObject; + // For Query string + let qs: IDataObject; + + let requestMethod: string; + let endpoint: string; + + for (let i = 0; i < items.length; i++) { + requestMethod = 'GET'; + endpoint = ''; + body = {}; + qs = {}; + + resource = this.getNodeParameter('resource', i) as string; + operation = this.getNodeParameter('operation', i) as string; + + if (resource === 'sms') { + if (operation === 'send') { + // ---------------------------------- + // sms:send + // ---------------------------------- + + requestMethod = 'GET'; + endpoint = 'https://api.msg91.com/api/sendhttp.php'; + + qs.route = 4; + qs.country = 0; + qs.sender = this.getNodeParameter('sender', i) as string; + qs.mobiles = this.getNodeParameter('mobiles', i) as string; + qs.message = this.getNodeParameter('message', i) as string; + + } else { + throw new Error(`The operation "${operation}" is not known!`); + } + } else { + throw new Error(`The resource "${resource}" is not known!`); + } + + const responseData = await msg91ApiRequest.call(this, requestMethod, endpoint, body, qs); + + returnData.push(responseData as IDataObject); + } + + return [this.helpers.returnJsonArray(returnData)]; + + } +} diff --git a/packages/nodes-base/nodes/Msg91/msg91.png b/packages/nodes-base/nodes/Msg91/msg91.png new file mode 100644 index 0000000000000000000000000000000000000000..dc10c1f269c2c7d9c03c0976959d4ccc25b9ac67 GIT binary patch literal 2982 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 D8CIAm literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index dad780ab23..6d44a32faf 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -68,6 +68,7 @@ "dist/credentials/TodoistApi.credentials.js", "dist/credentials/TrelloApi.credentials.js", "dist/credentials/TwilioApi.credentials.js", + "dist/credentials/Msg91Api.credentials.js", "dist/credentials/TypeformApi.credentials.js", "dist/credentials/MandrillApi.credentials.js", "dist/credentials/TodoistApi.credentials.js", @@ -149,6 +150,7 @@ "dist/nodes/Trello/Trello.node.js", "dist/nodes/Trello/TrelloTrigger.node.js", "dist/nodes/Twilio/Twilio.node.js", + "dist/nodes/Msg91/Msg91.node.js", "dist/nodes/Typeform/TypeformTrigger.node.js", "dist/nodes/WriteBinaryFile.node.js", "dist/nodes/Webhook.node.js", From 7d2e8576137b07587c15d86e750b8af2d00e8b84 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sun, 5 Jan 2020 13:34:09 -0500 Subject: [PATCH 003/389] done --- .../credentials/ZendeskApi.credentials.ts | 29 ++++ .../nodes/Zendesk/GenericFunctions.ts | 64 ++++++++ .../nodes/Zendesk/ZendeskTrigger.node.ts | 149 ++++++++++++++++++ packages/nodes-base/nodes/Zendesk/zendesk.png | Bin 0 -> 3433 bytes packages/nodes-base/package.json | 16 +- 5 files changed, 251 insertions(+), 7 deletions(-) create mode 100644 packages/nodes-base/credentials/ZendeskApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Zendesk/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Zendesk/zendesk.png diff --git a/packages/nodes-base/credentials/ZendeskApi.credentials.ts b/packages/nodes-base/credentials/ZendeskApi.credentials.ts new file mode 100644 index 0000000000..29048c1172 --- /dev/null +++ b/packages/nodes-base/credentials/ZendeskApi.credentials.ts @@ -0,0 +1,29 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class ZendeskApi implements ICredentialType { + name = 'zendeskApi'; + displayName = 'Zendesk API'; + properties = [ + { + displayName: 'URL', + name: 'url', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Email', + name: 'email', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'API Token', + name: 'apiToken', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts new file mode 100644 index 0000000000..8221cb402f --- /dev/null +++ b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts @@ -0,0 +1,64 @@ +import { OptionsWithUri } from 'request'; +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IExecuteSingleFunctions, +} from 'n8n-core'; +import { IDataObject } from 'n8n-workflow'; + +export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('zendeskApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + const base64Key = Buffer.from(`${credentials.email}/token:${credentials.apiToken}`).toString('base64') + let options: OptionsWithUri = { + headers: { 'Authorization': `Basic ${base64Key}`}, + method, + qs, + body, + uri: uri ||`${credentials.domain}/api/v2${resource}`, + json: true + }; + options = Object.assign({}, options, option); + if (Object.keys(options.body).length === 0) { + delete options.body; + } + + try { + return await this.helpers.request!(options); + } catch (error) { + let errorMessage = error.message; + if (error.response.body) { + errorMessage = error.response.body.message || error.response.body.Message || error.message; + } + + throw new Error(errorMessage); + } +} + +/** + * Make an API request to paginated flow endpoint + * and return all results + */ +export async function zendeskApiRequestAllItems(this: IHookFunctions | IExecuteFunctions| ILoadOptionsFunctions, propertyName: string, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + let uri: string | undefined; + + do { + responseData = await zendeskApiRequest.call(this, method, resource, body, query, uri); + query.continuation = responseData.pagination.continuation; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.pagination !== undefined && + responseData.pagination.has_more_items !== undefined && + responseData.pagination.has_more_items !== false + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts new file mode 100644 index 0000000000..479ef88f2d --- /dev/null +++ b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts @@ -0,0 +1,149 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + INodeTypeDescription, + INodeType, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + zendeskApiRequest, +} from './GenericFunctions'; + +export class ZendeskTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Zendesk Trigger', + name: 'zendesk', + icon: 'file:zendesk.png', + group: ['trigger'], + version: 1, + description: 'Handle Zendesk events via webhooks', + defaults: { + name: 'Zendesk Trigger', + color: '#559922', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'zendeskApi', + required: true, + } + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Service', + name: 'service', + type: 'options', + required: true, + options: [ + { + name: 'Support', + value: 'support', + } + ], + default: 'support', + description: '', + }, + { + displayName: 'Events', + name: 'events', + type: 'multiOptions', + displayOptions: { + show: { + service: [ + 'support' + ] + } + }, + options: [ + { + name: 'ticket.status.open', + value: 'ticket.status.open' + }, + ], + required: true, + default: [], + description: '', + }, + ], + + }; + // @ts-ignore + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + let webhooks; + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId === undefined) { + return false; + } + const endpoint = `/webhooks/${webhookData.webhookId}/`; + try { + webhooks = await zendeskApiRequest.call(this, 'GET', endpoint); + } catch (e) { + return false; + } + return true; + }, + async create(this: IHookFunctions): Promise { + let body, responseData; + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const event = this.getNodeParameter('event') as string; + const actions = this.getNodeParameter('actions') as string[]; + const endpoint = `/webhooks/`; + // @ts-ignore + body = { + endpoint_url: webhookUrl, + actions: actions.join(','), + event_id: event, + }; + try { + responseData = await zendeskApiRequest.call(this, 'POST', endpoint, body); + } catch(error) { + console.log(error) + return false; + } + // @ts-ignore + webhookData.webhookId = responseData.id; + return true; + }, + async delete(this: IHookFunctions): Promise { + let responseData; + const webhookData = this.getWorkflowStaticData('node'); + const endpoint = `/webhooks/${webhookData.webhookId}/`; + try { + responseData = await zendeskApiRequest.call(this, 'DELETE', endpoint); + } catch(error) { + return false; + } + if (!responseData.success) { + return false; + } + delete webhookData.webhookId; + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const req = this.getRequestObject(); + return { + workflowData: [ + this.helpers.returnJsonArray(req.body) + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Zendesk/zendesk.png b/packages/nodes-base/nodes/Zendesk/zendesk.png new file mode 100644 index 0000000000000000000000000000000000000000..f8c5d2e744ec9975375e33aa85634d0c30530b5a GIT binary patch literal 3433 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 literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index dc5747eb36..4ddc3b7150 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -71,11 +71,12 @@ "dist/credentials/TwilioApi.credentials.js", "dist/credentials/TypeformApi.credentials.js", "dist/credentials/MandrillApi.credentials.js", - "dist/credentials/TodoistApi.credentials.js", - "dist/credentials/TypeformApi.credentials.js", - "dist/credentials/TogglApi.credentials.js", + "dist/credentials/TodoistApi.credentials.js", + "dist/credentials/TypeformApi.credentials.js", + "dist/credentials/TogglApi.credentials.js", "dist/credentials/VeroApi.credentials.js", - "dist/credentials/WordpressApi.credentials.js" + "dist/credentials/WordpressApi.credentials.js", + "dist/credentials/ZendeskApi.credentials.js" ], "nodes": [ "dist/nodes/ActiveCampaign/ActiveCampaign.node.js", @@ -163,9 +164,10 @@ "dist/nodes/Toggl/TogglTrigger.node.js", "dist/nodes/Vero/Vero.node.js", "dist/nodes/WriteBinaryFile.node.js", - "dist/nodes/Webhook.node.js", - "dist/nodes/Wordpress/Wordpress.node.js", - "dist/nodes/Xml.node.js" + "dist/nodes/Webhook.node.js", + "dist/nodes/Wordpress/Wordpress.node.js", + "dist/nodes/Xml.node.js", + "dist/nodes/Zendesk/ZendeskTrigger.node.js" ] }, "devDependencies": { From f92a42dfe11fac49f75c5330ae501fcd12341098 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sun, 5 Jan 2020 21:32:22 -0500 Subject: [PATCH 004/389] done --- .../nodes/Zendesk/GenericFunctions.ts | 37 ++-------- .../nodes/Zendesk/ZendeskTrigger.node.ts | 74 ++++++++++++------- 2 files changed, 51 insertions(+), 60 deletions(-) diff --git a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts index 8221cb402f..9677f5652f 100644 --- a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts @@ -18,47 +18,20 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions method, qs, body, - uri: uri ||`${credentials.domain}/api/v2${resource}`, + uri: uri ||`${credentials.domain}/api/v2${resource}.json`, json: true }; options = Object.assign({}, options, option); if (Object.keys(options.body).length === 0) { delete options.body; } - try { return await this.helpers.request!(options); - } catch (error) { - let errorMessage = error.message; - if (error.response.body) { - errorMessage = error.response.body.message || error.response.body.Message || error.message; + } catch (err) { + let errorMessage = ''; + if (err.error && err.description) { + errorMessage = err.description; } - throw new Error(errorMessage); } } - -/** - * Make an API request to paginated flow endpoint - * and return all results - */ -export async function zendeskApiRequestAllItems(this: IHookFunctions | IExecuteFunctions| ILoadOptionsFunctions, propertyName: string, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any - - const returnData: IDataObject[] = []; - - let responseData; - - let uri: string | undefined; - - do { - responseData = await zendeskApiRequest.call(this, method, resource, body, query, uri); - query.continuation = responseData.pagination.continuation; - returnData.push.apply(returnData, responseData[propertyName]); - } while ( - responseData.pagination !== undefined && - responseData.pagination.has_more_items !== undefined && - responseData.pagination.has_more_items !== false - ); - - return returnData; -} diff --git a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts index 479ef88f2d..0859dd9db1 100644 --- a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts +++ b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts @@ -7,6 +7,7 @@ import { INodeTypeDescription, INodeType, IWebhookResponseData, + IDataObject, } from 'n8n-workflow'; import { @@ -59,7 +60,7 @@ export class ZendeskTrigger implements INodeType { { displayName: 'Events', name: 'events', - type: 'multiOptions', + type: 'options', displayOptions: { show: { service: [ @@ -69,12 +70,12 @@ export class ZendeskTrigger implements INodeType { }, options: [ { - name: 'ticket.status.open', - value: 'ticket.status.open' + name: 'ticket.created', + value: 'ticket.created', }, ], required: true, - default: [], + default: '', description: '', }, ], @@ -84,55 +85,72 @@ export class ZendeskTrigger implements INodeType { webhookMethods = { default: { async checkExists(this: IHookFunctions): Promise { - let webhooks; const webhookData = this.getWorkflowStaticData('node'); if (webhookData.webhookId === undefined) { return false; } - const endpoint = `/webhooks/${webhookData.webhookId}/`; + const endpoint = `/triggers/${webhookData.webhookId}`; try { - webhooks = await zendeskApiRequest.call(this, 'GET', endpoint); + await zendeskApiRequest.call(this, 'GET', endpoint); } catch (e) { return false; } return true; }, async create(this: IHookFunctions): Promise { - let body, responseData; + let condition: IDataObject = {}; const webhookUrl = this.getNodeWebhookUrl('default'); const webhookData = this.getWorkflowStaticData('node'); const event = this.getNodeParameter('event') as string; - const actions = this.getNodeParameter('actions') as string[]; - const endpoint = `/webhooks/`; - // @ts-ignore - body = { - endpoint_url: webhookUrl, - actions: actions.join(','), - event_id: event, - }; - try { - responseData = await zendeskApiRequest.call(this, 'POST', endpoint, body); - } catch(error) { - console.log(error) - return false; + if (event === 'ticket.created') { + condition = { + all: [ + { + field: 'status', + value: 'open', + }, + ], + } } + const bodyTrigger: IDataObject = { + trigger: { + conditions: { ...condition }, + actions: [ + { + field: 'notification_target', + value: [], + } + ] + }, + } + const bodyTarget: IDataObject = { + target: { + title: 'N8N webhook', + type: 'http_target', + target_url: webhookUrl, + method: 'POST', + active: true, + content_type: 'application/json', + }, + } + const { target } = await zendeskApiRequest.call(this, 'POST', '/targets', bodyTarget); // @ts-ignore - webhookData.webhookId = responseData.id; + bodyTrigger.trigger.actions[0].value = [target.id, '']; + const { trigger } = await zendeskApiRequest.call(this, 'POST', '/triggers', bodyTrigger); + webhookData.webhookId = trigger.id; + webhookData.targetId = target.id; return true; }, async delete(this: IHookFunctions): Promise { - let responseData; const webhookData = this.getWorkflowStaticData('node'); - const endpoint = `/webhooks/${webhookData.webhookId}/`; try { - responseData = await zendeskApiRequest.call(this, 'DELETE', endpoint); + await zendeskApiRequest.call(this, 'DELETE', `/triggers/${webhookData.webhookId}`); + await zendeskApiRequest.call(this, 'DELETE', `/targets/${webhookData.targetId}`); } catch(error) { return false; } - if (!responseData.success) { - return false; - } delete webhookData.webhookId; + delete webhookData.targetId return true; }, }, From 0d4a7f54085b526e6ae70c093ec4246039b78863 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Mon, 6 Jan 2020 19:30:40 -0500 Subject: [PATCH 005/389] done --- .../nodes/Zendesk/GenericFunctions.ts | 30 +- .../nodes/Zendesk/ZendeskTrigger.node.ts | 773 ++++++++++++++++-- 2 files changed, 753 insertions(+), 50 deletions(-) diff --git a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts index 9677f5652f..392c1426e3 100644 --- a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts @@ -18,7 +18,7 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions method, qs, body, - uri: uri ||`${credentials.domain}/api/v2${resource}.json`, + uri: uri ||`${credentials.url}/api/v2${resource}.json`, json: true }; options = Object.assign({}, options, option); @@ -29,9 +29,33 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions return await this.helpers.request!(options); } catch (err) { let errorMessage = ''; - if (err.error && err.description) { - errorMessage = err.description; + if (err.message && err.error) { + errorMessage = err.message; } throw new Error(errorMessage); } } + +/** + * Make an API request to paginated flow endpoint + * and return all results + */ +export async function zendeskApiRequestAllItems(this: IHookFunctions | IExecuteFunctions| ILoadOptionsFunctions, propertyName: string, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + let uri: string | undefined; + + do { + responseData = await zendeskApiRequest.call(this, method, resource, body, query, uri); + uri = responseData.next_page + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.next_page !== undefined && + responseData.next_page !== null + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts index 0859dd9db1..e86a6ddd72 100644 --- a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts +++ b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts @@ -8,10 +8,13 @@ import { INodeType, IWebhookResponseData, IDataObject, + INodePropertyOptions, + ILoadOptionsFunctions, } from 'n8n-workflow'; import { zendeskApiRequest, + zendeskApiRequestAllItems, } from './GenericFunctions'; export class ZendeskTrigger implements INodeType { @@ -24,7 +27,7 @@ export class ZendeskTrigger implements INodeType { description: 'Handle Zendesk events via webhooks', defaults: { name: 'Zendesk Trigger', - color: '#559922', + color: '#13353c', }, inputs: [], outputs: ['main'], @@ -58,9 +61,9 @@ export class ZendeskTrigger implements INodeType { description: '', }, { - displayName: 'Events', - name: 'events', - type: 'options', + displayName: 'Title', + name: 'title', + type: 'string', displayOptions: { show: { service: [ @@ -68,19 +71,669 @@ export class ZendeskTrigger implements INodeType { ] } }, - options: [ - { - name: 'ticket.created', - value: 'ticket.created', - }, - ], required: true, default: '', description: '', }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + displayOptions: { + show: { + service: [ + 'support' + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + default: [], + options: [ + { + name: 'Title', + value: 'ticket.title', + description: `Ticket's subject`, + }, + { + name: 'Description', + value: 'ticket.description', + description: `Ticket's description`, + }, + { + name: 'URL', + value: 'ticket.url', + description: `Ticket's URL`, + }, + { + name: 'ID', + value: 'ticket.id', + description: `Ticket's ID`, + }, + { + name: 'External ID', + value: 'ticket.external_id', + description: `Ticket's external ID`, + }, + { + name: 'Via', + value: 'ticket.via', + description: `Ticket's source` + }, + { + name: 'Status', + value: 'ticket.status', + description: `Ticket's status`, + }, + { + name: 'Priority', + value: 'ticket.priority', + description: `Ticket's priority`, + }, + { + name: 'Type', + value: 'ticket.ticket_type', + description: `Ticket's type`, + }, + { + name: 'Group Name', + value: 'ticket.group.name', + description: `Ticket's assigned group`, + }, + { + name: 'Brand Name', + value: 'ticket.brand.name', + description: `Ticket's brand`, + }, + { + name: 'Due Date', + value: 'ticket.due_date', + description: `Ticket's due date (relevant for tickets of type Task)`, + }, + { + name: 'Account', + value: 'ticket.account', + description: `This Zendesk Support's account name`, + }, + { + name: 'Assignee Email', + value: 'ticket.assignee.email', + description: `Ticket assignee email (if any)`, + }, + { + name: 'Assignee Name', + value: 'ticket.assignee.name', + description: `Assignee's full name`, + }, + { + name: 'Assignee First Name', + value: 'ticket.assignee.first_name', + description: `Assignee's first name`, + }, + { + name: 'Assignee Last Name', + value: 'ticket.assignee.last_name', + description: `Assignee's last name`, + }, + { + name: 'Requester Full Name', + value: 'ticket.requester.name', + description: `Requester's full name`, + }, + { + name: 'Requester First Name', + value: 'ticket.requester.first_name', + description: `Requester's first name`, + }, + { + name: 'Requester Last Name', + value: 'ticket.requester.last_name', + description: `Requester's last name`, + }, + { + name: 'Requester Email', + value: 'ticket.requester.email', + description: `Requester's email`, + }, + { + name: 'Requester Language', + value: 'ticket.requester.language', + description: `Requester's language`, + }, + { + name: 'Requester Phone', + value: 'ticket.requester.phone', + description: `Requester's phone number`, + }, + { + name: 'Requester External ID', + value: 'ticket.requester.external_id', + description: `Requester's external ID`, + }, + { + name: 'Requester Field', + value: 'ticket.requester.requester_field', + description: `Name or email`, + }, + { + name: 'Requester Details', + value: 'ticket.requester.details', + description: `Detailed information about the ticket's requester`, + }, + { + name: 'Requester Organization', + value: 'ticket.organization.name', + description: `Requester's organization`, + }, + { + name: `Ticket's Organization External ID`, + value: 'ticket.organization.external_id', + description: `Ticket's organization external ID`, + }, + { + name: `Organization details`, + value: 'ticket.organization.details', + description: `The details about the organization of the ticket's requester`, + }, + { + name: `Organization Note`, + value: 'ticket.organization.notes', + description: `The notes about the organization of the ticket's requester`, + }, + { + name: `Ticket's CCs`, + value: 'ticket.ccs', + description: `Ticket's CCs`, + }, + { + name: `Ticket's CCs names`, + value: 'ticket.cc_names', + description: `Ticket's CCs names`, + }, + { + name: `Ticket's tags`, + value: 'ticket.tags', + description: `Ticket's tags`, + }, + { + name: `Current Holiday Name`, + value: 'ticket.current_holiday_name', + description: `Displays the name of the current holiday on the ticket's schedule`, + }, + { + name: `Current User Name `, + value: 'current_user.name', + description: `Your full name`, + }, + { + name: `Current User First Name `, + value: 'current_user.first_name', + description: 'Your first name', + }, + { + name: `Current User Email `, + value: 'current_user.email', + description: 'Your primary email', + }, + { + name: `Current User Organization Name `, + value: 'current_user.organization.name', + description: 'Your default organization', + }, + { + name: `Current User Organization Details `, + value: 'current_user.organization.details', + description: `Your default organization's details`, + }, + { + name: `Current User Organization Notes `, + value: 'current_user.organization.notes', + description: `Your default organization's note`, + }, + { + name: `Current User Language `, + value: 'current_user.language', + description: `Your chosen language`, + }, + { + name: `Current User External ID `, + value: 'current_user.external_id', + description: 'Your external ID', + }, + { + name: `Current User Notes `, + value: 'current_user.notes', + description: 'Your notes, stored in your profile', + }, + { + name: `Satisfation Current Rating `, + value: 'satisfaction.current_rating', + description: 'The text of the current satisfaction rating', + }, + { + name: `Satisfation Current Comment `, + value: 'satisfaction.current_comment', + description: 'The text of the current satisfaction rating comment``', + }, + ], + }, + ], + placeholder: 'Add Option', + }, + { + displayName: 'Conditions', + name: 'conditions', + placeholder: 'Add Condition', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + service: [ + 'support' + ], + } + }, + description: 'The condition to set.', + default: {}, + options: [ + { + name: 'all', + displayName: 'All', + values: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Ticket', + value: 'ticket', + }, + ], + default: 'ticket', + description: '', + }, + { + displayName: 'Field', + name: 'field', + type: 'options', + displayOptions: { + show: { + 'resource': [ + 'ticket' + ] + } + }, + options: [ + { + name: 'Status', + value: 'status', + }, + { + name: 'Type', + value: 'type', + }, + { + name: 'Priority', + value: 'priority', + }, + { + name: 'Group', + value: 'group', + }, + { + name: 'Assignee', + value: 'assignee', + }, + ], + default: 'status', + description: '', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Is', + value: 'is', + }, + { + name: 'Is Not', + value: 'is_not', + }, + { + name: 'Less Than', + value: 'less_than', + }, + { + name: 'Greater Than', + value: 'greater_than', + }, + { + name: 'Changed', + value: 'changed', + }, + { + name: 'Changed To', + value: 'value', + }, + { + name: 'Changed From', + value: 'value_previous', + }, + { + name: 'Not Changed', + value: 'not_changed', + }, + { + name: 'Not Changed To', + value: 'not_value', + }, + { + name: 'Not Changed From', + value: 'not_value_previous', + }, + ], + displayOptions: { + hide: { + field: [ + 'assignee', + ] + } + }, + default: 'is', + description: '', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Is', + value: 'is', + }, + { + name: 'Is Not', + value: 'is_not', + }, + { + name: 'Changed', + value: 'changed', + }, + { + name: 'Changed To', + value: 'value', + }, + { + name: 'Changed From', + value: 'value_previous', + }, + { + name: 'Not Changed', + value: 'not_changed', + }, + { + name: 'Not Changed To', + value: 'not_value', + }, + { + name: 'Not Changed From', + value: 'not_value_previous', + }, + ], + displayOptions: { + show: { + field: [ + 'assignee', + ] + } + }, + default: 'is', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + displayOptions: { + show: { + field: [ + 'status' + ], + }, + hide: { + operation:[ + 'changed', + 'not_changed', + ], + field: [ + 'assignee', + 'group', + 'priority', + 'type', + ], + } + }, + options: [ + { + name: 'Open', + value: 'open', + }, + { + name: 'New', + value: 'new', + }, + { + name: 'Pending', + value: 'pending', + }, + { + name: 'Solved', + value: 'solved', + }, + { + name: 'Closed', + value: 'closed', + }, + ], + default: 'open', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + displayOptions: { + show: { + field: [ + 'type' + ], + }, + hide: { + operation:[ + 'changed', + 'not_changed', + ], + field: [ + 'assignee', + 'group', + 'priority', + 'status', + ], + } + }, + options: [ + { + name: 'Question', + value: 'question', + }, + { + name: 'Incident', + value: 'incident', + }, + { + name: 'Problem', + value: 'problem', + }, + { + name: 'Task', + value: 'task', + }, + ], + default: 'question', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + displayOptions: { + show: { + field: [ + 'priority' + ], + }, + hide: { + operation:[ + 'changed', + 'not_changed', + ], + field: [ + 'assignee', + 'group', + 'type', + 'status', + ], + } + }, + options: [ + { + name: 'Low', + value: 'low', + }, + { + name: 'Normal', + value: 'normal', + }, + { + name: 'High', + value: 'high', + }, + { + name: 'Urgent', + value: 'urgent', + }, + ], + default: 'low', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getGroups', + }, + displayOptions: { + show: { + field: [ + 'group' + ], + }, + hide: { + field: [ + 'assignee', + 'priority', + 'type', + 'status', + ], + }, + }, + default: '', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + displayOptions: { + show: { + field: [ + 'assignee' + ], + }, + hide: { + field: [ + 'group', + 'priority', + 'type', + 'status', + ], + }, + }, + default: '', + description: '', + }, + ] + } + ], + }, ], }; + methods = { + loadOptions: { + // Get all the groups to display them to user so that he can + // select them easily + async getGroups(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const groups = await zendeskApiRequestAllItems.call(this, 'groups', 'GET', '/groups'); + for (const group of groups) { + const groupName = group.name; + const groupId = group.id; + returnData.push({ + name: groupName, + value: groupId, + }); + } + return returnData; + }, + // Get all the users to display them to user so that he can + // select them easily + async getUsers(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const users = await zendeskApiRequestAllItems.call(this, 'users', 'GET', '/users'); + for (const user of users) { + const userName = user.name; + const userId = user.id; + returnData.push({ + name: userName, + value: userId, + }); + } + returnData.push({ + name: 'Current User', + value: 'current_user', + }) + returnData.push({ + name: 'Requester', + value: 'requester_id', + }) + return returnData; + }, + } + }; // @ts-ignore webhookMethods = { default: { @@ -98,47 +751,73 @@ export class ZendeskTrigger implements INodeType { return true; }, async create(this: IHookFunctions): Promise { - let condition: IDataObject = {}; const webhookUrl = this.getNodeWebhookUrl('default'); const webhookData = this.getWorkflowStaticData('node'); - const event = this.getNodeParameter('event') as string; - if (event === 'ticket.created') { - condition = { - all: [ - { - field: 'status', - value: 'open', - }, - ], + const service = this.getNodeParameter('service') as string; + if (service === 'support') { + const aux: IDataObject = {}; + const message: IDataObject = {}; + const resultAll = []; + const title = this.getNodeParameter('title') as string; + const conditions = this.getNodeParameter('conditions') as IDataObject; + const options = this.getNodeParameter('options') as IDataObject; + if (Object.keys(conditions).length === 0) { + throw new Error('You must have at least one condition'); } + console.log(options) + if (options.fields) { + // @ts-ignore + for (let field of options.fields) { + // @ts-ignore + message[field] = `{{${field}}}`; + } + } else { + message['ticket.id'] = '{{ticket.id}}' + } + const conditionsAll = conditions.all as [IDataObject]; + for (let conditionAll of conditionsAll) { + aux.field = conditionAll.field; + aux.operator = conditionAll.operation; + if (conditionAll.operation !== 'changed' + && conditionAll.operation !== 'not_changed') { + aux.value = conditionAll.value; + } else { + aux.value = null; + } + resultAll.push(aux) + } + const bodyTrigger: IDataObject = { + trigger: { + title, + conditions: { + all: resultAll, + any: [], + }, + actions: [ + { + field: 'notification_target', + value: [], + } + ] + }, + } + const bodyTarget: IDataObject = { + target: { + title: 'N8N webhook', + type: 'http_target', + target_url: webhookUrl, + method: 'POST', + active: true, + content_type: 'application/json', + }, + }; + const { target } = await zendeskApiRequest.call(this, 'POST', '/targets', bodyTarget); + // @ts-ignore + bodyTrigger.trigger.actions[0].value = [target.id, JSON.stringify(message)]; + const { trigger } = await zendeskApiRequest.call(this, 'POST', '/triggers', bodyTrigger); + webhookData.webhookId = trigger.id; + webhookData.targetId = target.id; } - const bodyTrigger: IDataObject = { - trigger: { - conditions: { ...condition }, - actions: [ - { - field: 'notification_target', - value: [], - } - ] - }, - } - const bodyTarget: IDataObject = { - target: { - title: 'N8N webhook', - type: 'http_target', - target_url: webhookUrl, - method: 'POST', - active: true, - content_type: 'application/json', - }, - } - const { target } = await zendeskApiRequest.call(this, 'POST', '/targets', bodyTarget); - // @ts-ignore - bodyTrigger.trigger.actions[0].value = [target.id, '']; - const { trigger } = await zendeskApiRequest.call(this, 'POST', '/triggers', bodyTrigger); - webhookData.webhookId = trigger.id; - webhookData.targetId = target.id; return true; }, async delete(this: IHookFunctions): Promise { From 0cb7965101f6b3ac2790de95e89077e2a3e9b591 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Mon, 6 Jan 2020 19:52:37 -0500 Subject: [PATCH 006/389] :sparkles: zendesk trigger --- .../nodes/Zendesk/ConditionDescription.ts | 343 +++++++++++++++ .../nodes/Zendesk/ZendeskTrigger.node.ts | 391 ++---------------- 2 files changed, 382 insertions(+), 352 deletions(-) create mode 100644 packages/nodes-base/nodes/Zendesk/ConditionDescription.ts diff --git a/packages/nodes-base/nodes/Zendesk/ConditionDescription.ts b/packages/nodes-base/nodes/Zendesk/ConditionDescription.ts new file mode 100644 index 0000000000..802ba004f9 --- /dev/null +++ b/packages/nodes-base/nodes/Zendesk/ConditionDescription.ts @@ -0,0 +1,343 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const conditionFields = [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Ticket', + value: 'ticket', + }, + ], + default: 'ticket', + description: '', + }, + { + displayName: 'Field', + name: 'field', + type: 'options', + displayOptions: { + show: { + 'resource': [ + 'ticket' + ] + } + }, + options: [ + { + name: 'Status', + value: 'status', + }, + { + name: 'Type', + value: 'type', + }, + { + name: 'Priority', + value: 'priority', + }, + { + name: 'Group', + value: 'group', + }, + { + name: 'Assignee', + value: 'assignee', + }, + ], + default: 'status', + description: '', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Is', + value: 'is', + }, + { + name: 'Is Not', + value: 'is_not', + }, + { + name: 'Less Than', + value: 'less_than', + }, + { + name: 'Greater Than', + value: 'greater_than', + }, + { + name: 'Changed', + value: 'changed', + }, + { + name: 'Changed To', + value: 'value', + }, + { + name: 'Changed From', + value: 'value_previous', + }, + { + name: 'Not Changed', + value: 'not_changed', + }, + { + name: 'Not Changed To', + value: 'not_value', + }, + { + name: 'Not Changed From', + value: 'not_value_previous', + }, + ], + displayOptions: { + hide: { + field: [ + 'assignee', + ] + } + }, + default: 'is', + description: '', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Is', + value: 'is', + }, + { + name: 'Is Not', + value: 'is_not', + }, + { + name: 'Changed', + value: 'changed', + }, + { + name: 'Changed To', + value: 'value', + }, + { + name: 'Changed From', + value: 'value_previous', + }, + { + name: 'Not Changed', + value: 'not_changed', + }, + { + name: 'Not Changed To', + value: 'not_value', + }, + { + name: 'Not Changed From', + value: 'not_value_previous', + }, + ], + displayOptions: { + show: { + field: [ + 'assignee', + ] + } + }, + default: 'is', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + displayOptions: { + show: { + field: [ + 'status' + ], + }, + hide: { + operation:[ + 'changed', + 'not_changed', + ], + field: [ + 'assignee', + 'group', + 'priority', + 'type', + ], + } + }, + options: [ + { + name: 'Open', + value: 'open', + }, + { + name: 'New', + value: 'new', + }, + { + name: 'Pending', + value: 'pending', + }, + { + name: 'Solved', + value: 'solved', + }, + { + name: 'Closed', + value: 'closed', + }, + ], + default: 'open', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + displayOptions: { + show: { + field: [ + 'type' + ], + }, + hide: { + operation:[ + 'changed', + 'not_changed', + ], + field: [ + 'assignee', + 'group', + 'priority', + 'status', + ], + } + }, + options: [ + { + name: 'Question', + value: 'question', + }, + { + name: 'Incident', + value: 'incident', + }, + { + name: 'Problem', + value: 'problem', + }, + { + name: 'Task', + value: 'task', + }, + ], + default: 'question', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + displayOptions: { + show: { + field: [ + 'priority' + ], + }, + hide: { + operation:[ + 'changed', + 'not_changed', + ], + field: [ + 'assignee', + 'group', + 'type', + 'status', + ], + } + }, + options: [ + { + name: 'Low', + value: 'low', + }, + { + name: 'Normal', + value: 'normal', + }, + { + name: 'High', + value: 'high', + }, + { + name: 'Urgent', + value: 'urgent', + }, + ], + default: 'low', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getGroups', + }, + displayOptions: { + show: { + field: [ + 'group' + ], + }, + hide: { + field: [ + 'assignee', + 'priority', + 'type', + 'status', + ], + }, + }, + default: '', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + displayOptions: { + show: { + field: [ + 'assignee' + ], + }, + hide: { + field: [ + 'group', + 'priority', + 'type', + 'status', + ], + }, + }, + default: '', + description: '', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts index e86a6ddd72..a380fa07be 100644 --- a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts +++ b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts @@ -16,6 +16,9 @@ import { zendeskApiRequest, zendeskApiRequestAllItems, } from './GenericFunctions'; +import { + conditionFields + } from './ConditionDescription'; export class ZendeskTrigger implements INodeType { description: INodeTypeDescription = { @@ -346,347 +349,16 @@ export class ZendeskTrigger implements INodeType { name: 'all', displayName: 'All', values: [ - { - displayName: 'Resource', - name: 'resource', - type: 'options', - options: [ - { - name: 'Ticket', - value: 'ticket', - }, - ], - default: 'ticket', - description: '', - }, - { - displayName: 'Field', - name: 'field', - type: 'options', - displayOptions: { - show: { - 'resource': [ - 'ticket' - ] - } - }, - options: [ - { - name: 'Status', - value: 'status', - }, - { - name: 'Type', - value: 'type', - }, - { - name: 'Priority', - value: 'priority', - }, - { - name: 'Group', - value: 'group', - }, - { - name: 'Assignee', - value: 'assignee', - }, - ], - default: 'status', - description: '', - }, - { - displayName: 'Operation', - name: 'operation', - type: 'options', - options: [ - { - name: 'Is', - value: 'is', - }, - { - name: 'Is Not', - value: 'is_not', - }, - { - name: 'Less Than', - value: 'less_than', - }, - { - name: 'Greater Than', - value: 'greater_than', - }, - { - name: 'Changed', - value: 'changed', - }, - { - name: 'Changed To', - value: 'value', - }, - { - name: 'Changed From', - value: 'value_previous', - }, - { - name: 'Not Changed', - value: 'not_changed', - }, - { - name: 'Not Changed To', - value: 'not_value', - }, - { - name: 'Not Changed From', - value: 'not_value_previous', - }, - ], - displayOptions: { - hide: { - field: [ - 'assignee', - ] - } - }, - default: 'is', - description: '', - }, - { - displayName: 'Operation', - name: 'operation', - type: 'options', - options: [ - { - name: 'Is', - value: 'is', - }, - { - name: 'Is Not', - value: 'is_not', - }, - { - name: 'Changed', - value: 'changed', - }, - { - name: 'Changed To', - value: 'value', - }, - { - name: 'Changed From', - value: 'value_previous', - }, - { - name: 'Not Changed', - value: 'not_changed', - }, - { - name: 'Not Changed To', - value: 'not_value', - }, - { - name: 'Not Changed From', - value: 'not_value_previous', - }, - ], - displayOptions: { - show: { - field: [ - 'assignee', - ] - } - }, - default: 'is', - description: '', - }, - { - displayName: 'Value', - name: 'value', - type: 'options', - displayOptions: { - show: { - field: [ - 'status' - ], - }, - hide: { - operation:[ - 'changed', - 'not_changed', - ], - field: [ - 'assignee', - 'group', - 'priority', - 'type', - ], - } - }, - options: [ - { - name: 'Open', - value: 'open', - }, - { - name: 'New', - value: 'new', - }, - { - name: 'Pending', - value: 'pending', - }, - { - name: 'Solved', - value: 'solved', - }, - { - name: 'Closed', - value: 'closed', - }, - ], - default: 'open', - description: '', - }, - { - displayName: 'Value', - name: 'value', - type: 'options', - displayOptions: { - show: { - field: [ - 'type' - ], - }, - hide: { - operation:[ - 'changed', - 'not_changed', - ], - field: [ - 'assignee', - 'group', - 'priority', - 'status', - ], - } - }, - options: [ - { - name: 'Question', - value: 'question', - }, - { - name: 'Incident', - value: 'incident', - }, - { - name: 'Problem', - value: 'problem', - }, - { - name: 'Task', - value: 'task', - }, - ], - default: 'question', - description: '', - }, - { - displayName: 'Value', - name: 'value', - type: 'options', - displayOptions: { - show: { - field: [ - 'priority' - ], - }, - hide: { - operation:[ - 'changed', - 'not_changed', - ], - field: [ - 'assignee', - 'group', - 'type', - 'status', - ], - } - }, - options: [ - { - name: 'Low', - value: 'low', - }, - { - name: 'Normal', - value: 'normal', - }, - { - name: 'High', - value: 'high', - }, - { - name: 'Urgent', - value: 'urgent', - }, - ], - default: 'low', - description: '', - }, - { - displayName: 'Value', - name: 'value', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getGroups', - }, - displayOptions: { - show: { - field: [ - 'group' - ], - }, - hide: { - field: [ - 'assignee', - 'priority', - 'type', - 'status', - ], - }, - }, - default: '', - description: '', - }, - { - displayName: 'Value', - name: 'value', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getUsers', - }, - displayOptions: { - show: { - field: [ - 'assignee' - ], - }, - hide: { - field: [ - 'group', - 'priority', - 'type', - 'status', - ], - }, - }, - default: '', - description: '', - }, + ...conditionFields, ] - } + }, + { + name: 'any', + displayName: 'Any', + values: [ + ...conditionFields, + ] + }, ], }, ], @@ -757,14 +429,13 @@ export class ZendeskTrigger implements INodeType { if (service === 'support') { const aux: IDataObject = {}; const message: IDataObject = {}; - const resultAll = []; + const resultAll = [], resultAny = []; const title = this.getNodeParameter('title') as string; const conditions = this.getNodeParameter('conditions') as IDataObject; const options = this.getNodeParameter('options') as IDataObject; if (Object.keys(conditions).length === 0) { throw new Error('You must have at least one condition'); } - console.log(options) if (options.fields) { // @ts-ignore for (let field of options.fields) { @@ -775,23 +446,39 @@ export class ZendeskTrigger implements INodeType { message['ticket.id'] = '{{ticket.id}}' } const conditionsAll = conditions.all as [IDataObject]; - for (let conditionAll of conditionsAll) { - aux.field = conditionAll.field; - aux.operator = conditionAll.operation; - if (conditionAll.operation !== 'changed' - && conditionAll.operation !== 'not_changed') { - aux.value = conditionAll.value; - } else { - aux.value = null; + if (conditionsAll) { + for (let conditionAll of conditionsAll) { + aux.field = conditionAll.field; + aux.operator = conditionAll.operation; + if (conditionAll.operation !== 'changed' + && conditionAll.operation !== 'not_changed') { + aux.value = conditionAll.value; + } else { + aux.value = null; + } + resultAll.push(aux) + } + } + const conditionsAny = conditions.any as [IDataObject]; + if (conditionsAny) { + for (let conditionAny of conditionsAny) { + aux.field = conditionAny.field; + aux.operator = conditionAny.operation; + if (conditionAny.operation !== 'changed' + && conditionAny.operation !== 'not_changed') { + aux.value = conditionAny.value; + } else { + aux.value = null; + } + resultAny.push(aux) } - resultAll.push(aux) } const bodyTrigger: IDataObject = { trigger: { title, conditions: { all: resultAll, - any: [], + any: resultAny, }, actions: [ { From 3448575a206dc0c98f3d00e8ce7302d75cdb0d5e Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Tue, 7 Jan 2020 14:56:42 -0500 Subject: [PATCH 007/389] :sparkles: added zendesk node --- .../nodes/Zendesk/GenericFunctions.ts | 6 +- .../nodes/Zendesk/TicketDescription.ts | 503 ++++++++++++++++++ .../nodes/Zendesk/TicketInterface.ts | 16 + .../nodes-base/nodes/Zendesk/Zendesk.node.ts | 238 +++++++++ packages/nodes-base/package.json | 3 +- 5 files changed, 760 insertions(+), 6 deletions(-) create mode 100644 packages/nodes-base/nodes/Zendesk/TicketDescription.ts create mode 100644 packages/nodes-base/nodes/Zendesk/TicketInterface.ts create mode 100644 packages/nodes-base/nodes/Zendesk/Zendesk.node.ts diff --git a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts index 392c1426e3..0c87d87738 100644 --- a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts @@ -28,11 +28,7 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions try { return await this.helpers.request!(options); } catch (err) { - let errorMessage = ''; - if (err.message && err.error) { - errorMessage = err.message; - } - throw new Error(errorMessage); + throw new Error(err); } } diff --git a/packages/nodes-base/nodes/Zendesk/TicketDescription.ts b/packages/nodes-base/nodes/Zendesk/TicketDescription.ts new file mode 100644 index 0000000000..601beb71fe --- /dev/null +++ b/packages/nodes-base/nodes/Zendesk/TicketDescription.ts @@ -0,0 +1,503 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const ticketOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'ticket', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a ticket', + }, + { + name: 'Update', + value: 'update', + description: 'Update a ticket' + }, + { + name: 'Get', + value: 'get', + description: 'Get a ticket' + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all tickets' + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a ticket' + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const ticketFields = [ + +/* -------------------------------------------------------------------------- */ +/* ticket:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Description', + name: 'description', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + description: 'The first comment on the ticket', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'External ID', + name: 'externalId', + type: 'string', + default: '', + description: 'An id you can use to link Zendesk Support tickets to local records', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + default: '', + description: 'The value of the subject field for this ticket', + }, + { + displayName: 'Recipient', + name: 'recipient', + type: 'string', + default: '', + description: 'The original recipient e-mail address of the ticket', + }, + { + displayName: 'Group', + name: 'group', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getGroups', + }, + default: '', + description: 'The group this ticket is assigned to', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: [], + description: 'The array of tags applied to this ticket', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Question', + value: 'question', + }, + { + name: 'Incident', + value: 'incident', + }, + { + name: 'Problem', + value: 'problem', + }, + { + name: 'Task', + value: 'task', + }, + ], + default: '', + description: 'The type of this ticket', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Open', + value: 'open', + }, + { + name: 'New', + value: 'new', + }, + { + name: 'Pending', + value: 'pending', + }, + { + name: 'Solved', + value: 'solved', + }, + { + name: 'Closed', + value: 'closed', + }, + ], + default: '', + description: 'The state of the ticket', + } + ], + }, +/* -------------------------------------------------------------------------- */ +/* ticket:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'update', + ], + }, + }, + description: 'Ticket ID', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'External ID', + name: 'externalId', + type: 'string', + default: '', + description: 'An id you can use to link Zendesk Support tickets to local records', + }, + { + displayName: 'Subject', + name: 'subject', + type: 'string', + default: '', + description: 'The value of the subject field for this ticket', + }, + { + displayName: 'Recipient', + name: 'recipient', + type: 'string', + default: '', + description: 'The original recipient e-mail address of the ticket', + }, + { + displayName: 'Group', + name: 'group', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getGroups', + }, + default: '', + description: 'The group this ticket is assigned to', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: [], + description: 'The array of tags applied to this ticket', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Question', + value: 'question', + }, + { + name: 'Incident', + value: 'incident', + }, + { + name: 'Problem', + value: 'problem', + }, + { + name: 'Task', + value: 'task', + }, + ], + default: '', + description: 'The type of this ticket', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Open', + value: 'open', + }, + { + name: 'New', + value: 'new', + }, + { + name: 'Pending', + value: 'pending', + }, + { + name: 'Solved', + value: 'solved', + }, + { + name: 'Closed', + value: 'closed', + }, + ], + default: '', + description: 'The state of the ticket', + } + ], + }, +/* -------------------------------------------------------------------------- */ +/* ticket:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'get', + ], + }, + }, + description: 'Ticket ID', + }, +/* -------------------------------------------------------------------------- */ +/* ticket:getAll */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'ticket', + ], + 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: [ + 'ticket', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Open', + value: 'open', + }, + { + name: 'New', + value: 'new', + }, + { + name: 'Pending', + value: 'pending', + }, + { + name: 'Solved', + value: 'solved', + }, + { + name: 'Closed', + value: 'closed', + }, + ], + default: '', + description: 'The state of the ticket', + }, + { + displayName: 'Sort By', + name: 'sortBy', + type: 'options', + options: [ + { + name: 'Updated At', + value: 'updated_at', + }, + { + name: 'Created At', + value: 'created_at', + }, + { + name: 'Priority', + value: 'priority', + }, + { + name: 'Status', + value: 'status', + }, + { + name: 'Ticket Type', + value: 'ticket_type', + }, + ], + default: 'updated_at', + description: 'Defaults to sorting by relevance', + }, + { + displayName: 'Sort Order', + name: 'sortOrder', + type: 'options', + options: [ + { + name: 'Asc', + value: 'asc', + }, + { + name: 'Desc', + value: 'desc', + }, + ], + default: 'desc', + description: 'Sort order', + } + ], + }, + +/* -------------------------------------------------------------------------- */ +/* ticket:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'ticket', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'Ticket ID', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zendesk/TicketInterface.ts b/packages/nodes-base/nodes/Zendesk/TicketInterface.ts new file mode 100644 index 0000000000..fc4eb75697 --- /dev/null +++ b/packages/nodes-base/nodes/Zendesk/TicketInterface.ts @@ -0,0 +1,16 @@ +import { IDataObject } from "n8n-workflow"; + +export interface ITicket { + subject?: string; + comment?: IComment; + type?: string; + group?: string; + external_id?: string; + tags?: string[]; + status?: string; + recipient?: string; +} + +export interface IComment { + body?: string; +} diff --git a/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts new file mode 100644 index 0000000000..49f91ceb56 --- /dev/null +++ b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts @@ -0,0 +1,238 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; +import { + IDataObject, + INodeTypeDescription, + INodeExecutionData, + INodeType, + ILoadOptionsFunctions, + INodePropertyOptions, +} from 'n8n-workflow'; +import { + zendeskApiRequest, + zendeskApiRequestAllItems, +} from './GenericFunctions'; +import { + ticketFields, + ticketOperations +} from './TicketDescription'; +import { + ITicket, + IComment, + } from './TicketInterface'; + +export class Zendesk implements INodeType { + description: INodeTypeDescription = { + displayName: 'Zendesk', + name: 'zendesk', + icon: 'file:zendesk.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Zendesk API', + defaults: { + name: 'Zendesk', + color: '#13353c', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'zendeskApi', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Ticket', + value: 'ticket', + description: 'Tickets are the means through which your end users (customers) communicate with agents in Zendesk Support.', + }, + ], + default: 'ticket', + description: 'Resource to consume.', + }, + ...ticketOperations, + ...ticketFields, + ], + }; + + methods = { + loadOptions: { + // Get all the groups to display them to user so that he can + // select them easily + async getGroups(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const groups = await zendeskApiRequestAllItems.call(this, 'groups', 'GET', '/groups'); + for (const group of groups) { + const groupName = group.name; + const groupId = group.id; + returnData.push({ + name: groupName, + value: groupId, + }); + } + return returnData; + }, + // Get all the tags to display them to user so that he can + // select them easily + async getTags(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const tags = await zendeskApiRequestAllItems.call(this, 'tags', 'GET', '/tags'); + for (const tag of tags) { + const tagName = tag.name; + const tagId = tag.name; + returnData.push({ + name: tagName, + value: tagId, + }); + } + return returnData; + }, + } + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + let qs: IDataObject = {}; + let responseData; + for (let i = 0; i < length; i++) { + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + //https://developer.zendesk.com/rest_api/docs/support/introduction + if (resource === 'ticket') { + //https://developer.zendesk.com/rest_api/docs/support/tickets + if (operation === 'create') { + const description = this.getNodeParameter('description', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const comment: IComment = { + body: description, + }; + const body: ITicket = { + comment, + }; + if (additionalFields.type) { + body.type = additionalFields.type as string; + } + if (additionalFields.externalId) { + body.external_id = additionalFields.externalId as string; + } + if (additionalFields.subject) { + body.subject = additionalFields.subject as string; + } + if (additionalFields.status) { + body.status = additionalFields.status as string; + } + if (additionalFields.recipient) { + body.recipient = additionalFields.recipient as string; + } + if (additionalFields.group) { + body.group = additionalFields.group as string; + } + if (additionalFields.tags) { + body.tags = additionalFields.tags as string[]; + } + try { + responseData = await zendeskApiRequest.call(this, 'POST', '/tickets', { ticket: body }); + responseData = responseData.ticket; + } catch (err) { + throw new Error(`Zendesk Error: ${err}`); + } + } + //https://developer.zendesk.com/rest_api/docs/support/tickets#update-ticket + if (operation === 'update') { + const ticketId = this.getNodeParameter('id', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const body: ITicket = {}; + if (updateFields.type) { + body.type = updateFields.type as string; + } + if (updateFields.externalId) { + body.external_id = updateFields.externalId as string; + } + if (updateFields.subject) { + body.subject = updateFields.subject as string; + } + if (updateFields.status) { + body.status = updateFields.status as string; + } + if (updateFields.recipient) { + body.recipient = updateFields.recipient as string; + } + if (updateFields.group) { + body.group = updateFields.group as string; + } + if (updateFields.tags) { + body.tags = updateFields.tags as string[]; + } + try { + responseData = await zendeskApiRequest.call(this, 'PUT', `/tickets/${ticketId}`, { ticket: body }); + responseData = responseData.ticket; + } catch (err) { + throw new Error(`Zendesk Error: ${err}`); + } + } + //https://developer.zendesk.com/rest_api/docs/support/tickets#show-ticket + if (operation === 'get') { + const ticketId = this.getNodeParameter('id', i) as string; + try { + responseData = await zendeskApiRequest.call(this, 'GET', `/tickets/${ticketId}`, {}); + responseData = responseData.ticket; + } catch (err) { + throw new Error(`Zendesk Error: ${err}`); + } + } + //https://developer.zendesk.com/rest_api/docs/support/search#list-search-results + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const options = this.getNodeParameter('options', i) as IDataObject; + qs.query = 'type:ticket' + if (options.status) { + qs.query += ` status:${options.status}` + } + if (options.sortBy) { + qs.sort_by = options.sortBy; + } + if (options.sortOrder) { + qs.sort_order = options.sortOrder; + } + try { + if (returnAll) { + responseData = await zendeskApiRequestAllItems.call(this, 'results', 'GET', `/search`, {}, qs); + } else { + const limit = this.getNodeParameter('limit', i) as number; + qs.per_page = limit; + responseData = await zendeskApiRequest.call(this, 'GET', `/search`, {}, qs); + responseData = responseData.results; + } + } catch (err) { + throw new Error(`Zendesk Error: ${err}`); + } + } + //https://developer.zendesk.com/rest_api/docs/support/tickets#delete-ticket + if (operation === 'delete') { + const ticketId = this.getNodeParameter('id', i) as string; + try { + responseData = await zendeskApiRequest.call(this, 'DELETE', `/tickets/${ticketId}`, {}); + } catch (err) { + throw new Error(`Zendesk Error: ${err}`); + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as IDataObject); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index ade1417d8a..a4d4236129 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -171,7 +171,8 @@ "dist/nodes/Webhook.node.js", "dist/nodes/Wordpress/Wordpress.node.js", "dist/nodes/Xml.node.js", - "dist/nodes/Zendesk/ZendeskTrigger.node.js" + "dist/nodes/Zendesk/ZendeskTrigger.node.js", + "dist/nodes/Zendesk/Zendesk.node.js" ] }, "devDependencies": { From d169a5617fcef2b5ea5834c04d0193578201b8bf Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 20 Dec 2019 16:35:00 -0600 Subject: [PATCH 008/389] :zap: Small improvements to Msg91-Node --- .../nodes/Msg91/GenericFunctions.ts | 14 ++--- packages/nodes-base/nodes/Msg91/Msg91.node.ts | 54 +++++++++---------- 2 files changed, 31 insertions(+), 37 deletions(-) diff --git a/packages/nodes-base/nodes/Msg91/GenericFunctions.ts b/packages/nodes-base/nodes/Msg91/GenericFunctions.ts index 59a5831bf4..893996fc8a 100644 --- a/packages/nodes-base/nodes/Msg91/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Msg91/GenericFunctions.ts @@ -24,19 +24,15 @@ export async function msg91ApiRequest(this: IHookFunctions | IExecuteFunctions, if (query === undefined) { query = {}; - } - - query.authkey = credentials.authkey as string; + } + + query.authkey = credentials.authkey as string; const options = { method, form: body, qs: query, - uri: `https://api.msg91.com/api/sendhttp.php`, - auth: { - user: '', - pass: '', - }, + uri: `https://api.msg91.com/api${endpoint}`, json: true }; @@ -61,4 +57,4 @@ export async function msg91ApiRequest(this: IHookFunctions | IExecuteFunctions, // If that data does not exist for some reason return the actual error throw error; } -} \ No newline at end of file +} diff --git a/packages/nodes-base/nodes/Msg91/Msg91.node.ts b/packages/nodes-base/nodes/Msg91/Msg91.node.ts index f59dd49ece..240a3623e0 100644 --- a/packages/nodes-base/nodes/Msg91/Msg91.node.ts +++ b/packages/nodes-base/nodes/Msg91/Msg91.node.ts @@ -46,7 +46,6 @@ export class Msg91 implements INodeType { default: 'sms', description: 'The resource to operate on.', }, - { displayName: 'Operation', name: 'operation', @@ -69,8 +68,27 @@ export class Msg91 implements INodeType { description: 'The operation to perform.', }, { - displayName: 'Sender', - name: 'sender', + displayName: 'From', + name: 'from', + type: 'string', + default: '', + placeholder: '4155238886', + required: true, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'sms', + ], + }, + }, + description: 'The number from which to send the message.', + }, + { + displayName: 'To', + name: 'to', type: 'string', default: '', placeholder: '+14155238886', @@ -85,26 +103,7 @@ export class Msg91 implements INodeType { ], }, }, - description: 'The number from which to send the message', - }, - { - displayName: 'To', - name: 'mobiles', - type: 'string', - default: '', - placeholder: 'Mobile Number With Country Code', - required: true, - displayOptions: { - show: { - operation: [ - 'send', - ], - resource: [ - 'sms', - ], - }, - }, - description: 'The number to which to send the message', + description: 'The number, with coutry code, to which to send the message.', }, { displayName: 'Message', @@ -145,7 +144,6 @@ export class Msg91 implements INodeType { let endpoint: string; for (let i = 0; i < items.length; i++) { - requestMethod = 'GET'; endpoint = ''; body = {}; qs = {}; @@ -160,12 +158,12 @@ export class Msg91 implements INodeType { // ---------------------------------- requestMethod = 'GET'; - endpoint = 'https://api.msg91.com/api/sendhttp.php'; + endpoint = '/sendhttp.php'; qs.route = 4; qs.country = 0; - qs.sender = this.getNodeParameter('sender', i) as string; - qs.mobiles = this.getNodeParameter('mobiles', i) as string; + qs.sender = this.getNodeParameter('from', i) as string; + qs.mobiles = this.getNodeParameter('to', i) as string; qs.message = this.getNodeParameter('message', i) as string; } else { @@ -177,7 +175,7 @@ export class Msg91 implements INodeType { const responseData = await msg91ApiRequest.call(this, requestMethod, endpoint, body, qs); - returnData.push(responseData as IDataObject); + returnData.push({ requestId: responseData }); } return [this.helpers.returnJsonArray(returnData)]; From 97cc3af4c320c8a74df83b25971a8f428c41c2ee Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 7 Jan 2020 21:15:37 -0600 Subject: [PATCH 009/389] :zap: Small fixes and improvements on Zendesk-Node --- .../nodes/Zendesk/ConditionDescription.ts | 9 ---- .../nodes/Zendesk/GenericFunctions.ts | 6 +-- .../nodes/Zendesk/TicketDescription.ts | 8 +-- .../nodes/Zendesk/TicketInterface.ts | 2 - .../nodes-base/nodes/Zendesk/Zendesk.node.ts | 6 +-- .../nodes/Zendesk/ZendeskTrigger.node.ts | 50 ++++++++----------- 6 files changed, 30 insertions(+), 51 deletions(-) diff --git a/packages/nodes-base/nodes/Zendesk/ConditionDescription.ts b/packages/nodes-base/nodes/Zendesk/ConditionDescription.ts index 802ba004f9..99a1429b0c 100644 --- a/packages/nodes-base/nodes/Zendesk/ConditionDescription.ts +++ b/packages/nodes-base/nodes/Zendesk/ConditionDescription.ts @@ -12,7 +12,6 @@ export const conditionFields = [ }, ], default: 'ticket', - description: '', }, { displayName: 'Field', @@ -48,7 +47,6 @@ export const conditionFields = [ }, ], default: 'status', - description: '', }, { displayName: 'Operation', @@ -104,7 +102,6 @@ export const conditionFields = [ } }, default: 'is', - description: '', }, { displayName: 'Operation', @@ -152,7 +149,6 @@ export const conditionFields = [ } }, default: 'is', - description: '', }, { displayName: 'Value', @@ -200,7 +196,6 @@ export const conditionFields = [ }, ], default: 'open', - description: '', }, { displayName: 'Value', @@ -244,7 +239,6 @@ export const conditionFields = [ }, ], default: 'question', - description: '', }, { displayName: 'Value', @@ -288,7 +282,6 @@ export const conditionFields = [ }, ], default: 'low', - description: '', }, { displayName: 'Value', @@ -313,7 +306,6 @@ export const conditionFields = [ }, }, default: '', - description: '', }, { displayName: 'Value', @@ -338,6 +330,5 @@ export const conditionFields = [ }, }, default: '', - description: '', }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts index 0c87d87738..5126c69495 100644 --- a/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Zendesk/GenericFunctions.ts @@ -1,9 +1,9 @@ import { OptionsWithUri } from 'request'; import { IExecuteFunctions, + IExecuteSingleFunctions, IHookFunctions, ILoadOptionsFunctions, - IExecuteSingleFunctions, } from 'n8n-core'; import { IDataObject } from 'n8n-workflow'; @@ -12,7 +12,7 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions if (credentials === undefined) { throw new Error('No credentials got returned!'); } - const base64Key = Buffer.from(`${credentials.email}/token:${credentials.apiToken}`).toString('base64') + const base64Key = Buffer.from(`${credentials.email}/token:${credentials.apiToken}`).toString('base64'); let options: OptionsWithUri = { headers: { 'Authorization': `Basic ${base64Key}`}, method, @@ -46,7 +46,7 @@ export async function zendeskApiRequestAllItems(this: IHookFunctions | IExecuteF do { responseData = await zendeskApiRequest.call(this, method, resource, body, query, uri); - uri = responseData.next_page + uri = responseData.next_page; returnData.push.apply(returnData, responseData[propertyName]); } while ( responseData.next_page !== undefined && diff --git a/packages/nodes-base/nodes/Zendesk/TicketDescription.ts b/packages/nodes-base/nodes/Zendesk/TicketDescription.ts index 601beb71fe..477a8074e4 100644 --- a/packages/nodes-base/nodes/Zendesk/TicketDescription.ts +++ b/packages/nodes-base/nodes/Zendesk/TicketDescription.ts @@ -21,22 +21,22 @@ export const ticketOperations = [ { name: 'Update', value: 'update', - description: 'Update a ticket' + description: 'Update a ticket', }, { name: 'Get', value: 'get', - description: 'Get a ticket' + description: 'Get a ticket', }, { name: 'Get All', value: 'getAll', - description: 'Get all tickets' + description: 'Get all tickets', }, { name: 'Delete', value: 'delete', - description: 'Delete a ticket' + description: 'Delete a ticket', }, ], default: 'create', diff --git a/packages/nodes-base/nodes/Zendesk/TicketInterface.ts b/packages/nodes-base/nodes/Zendesk/TicketInterface.ts index fc4eb75697..5ef968381b 100644 --- a/packages/nodes-base/nodes/Zendesk/TicketInterface.ts +++ b/packages/nodes-base/nodes/Zendesk/TicketInterface.ts @@ -1,5 +1,3 @@ -import { IDataObject } from "n8n-workflow"; - export interface ITicket { subject?: string; comment?: IComment; diff --git a/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts index 49f91ceb56..91d055dae8 100644 --- a/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts +++ b/packages/nodes-base/nodes/Zendesk/Zendesk.node.ts @@ -102,7 +102,7 @@ export class Zendesk implements INodeType { const items = this.getInputData(); const returnData: IDataObject[] = []; const length = items.length as unknown as number; - let qs: IDataObject = {}; + const qs: IDataObject = {}; let responseData; for (let i = 0; i < length; i++) { const resource = this.getNodeParameter('resource', 0) as string; @@ -194,9 +194,9 @@ export class Zendesk implements INodeType { if (operation === 'getAll') { const returnAll = this.getNodeParameter('returnAll', i) as boolean; const options = this.getNodeParameter('options', i) as IDataObject; - qs.query = 'type:ticket' + qs.query = 'type:ticket'; if (options.status) { - qs.query += ` status:${options.status}` + qs.query += ` status:${options.status}`; } if (options.sortBy) { qs.sort_by = options.sortBy; diff --git a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts index a380fa07be..e242141d92 100644 --- a/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts +++ b/packages/nodes-base/nodes/Zendesk/ZendeskTrigger.node.ts @@ -1,3 +1,7 @@ +import { + parse as urlParse, +} from 'url'; + import { IHookFunctions, IWebhookFunctions, @@ -23,7 +27,7 @@ import { export class ZendeskTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'Zendesk Trigger', - name: 'zendesk', + name: 'zendeskTrigger', icon: 'file:zendesk.png', group: ['trigger'], version: 1, @@ -63,21 +67,6 @@ export class ZendeskTrigger implements INodeType { default: 'support', description: '', }, - { - displayName: 'Title', - name: 'title', - type: 'string', - displayOptions: { - show: { - service: [ - 'support' - ] - } - }, - required: true, - default: '', - description: '', - }, { displayName: 'Options', name: 'options', @@ -94,6 +83,7 @@ export class ZendeskTrigger implements INodeType { { displayName: 'Fields', name: 'fields', + description: 'The fields to return the values of.', type: 'multiOptions', default: [], options: [ @@ -397,11 +387,11 @@ export class ZendeskTrigger implements INodeType { returnData.push({ name: 'Current User', value: 'current_user', - }) + }); returnData.push({ name: 'Requester', value: 'requester_id', - }) + }); return returnData; }, } @@ -423,14 +413,13 @@ export class ZendeskTrigger implements INodeType { return true; }, async create(this: IHookFunctions): Promise { - const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookUrl = this.getNodeWebhookUrl('default') as string; const webhookData = this.getWorkflowStaticData('node'); const service = this.getNodeParameter('service') as string; if (service === 'support') { const aux: IDataObject = {}; const message: IDataObject = {}; const resultAll = [], resultAny = []; - const title = this.getNodeParameter('title') as string; const conditions = this.getNodeParameter('conditions') as IDataObject; const options = this.getNodeParameter('options') as IDataObject; if (Object.keys(conditions).length === 0) { @@ -438,16 +427,16 @@ export class ZendeskTrigger implements INodeType { } if (options.fields) { // @ts-ignore - for (let field of options.fields) { + for (const field of options.fields) { // @ts-ignore message[field] = `{{${field}}}`; } } else { - message['ticket.id'] = '{{ticket.id}}' + message['ticket.id'] = '{{ticket.id}}'; } const conditionsAll = conditions.all as [IDataObject]; if (conditionsAll) { - for (let conditionAll of conditionsAll) { + for (const conditionAll of conditionsAll) { aux.field = conditionAll.field; aux.operator = conditionAll.operation; if (conditionAll.operation !== 'changed' @@ -456,12 +445,12 @@ export class ZendeskTrigger implements INodeType { } else { aux.value = null; } - resultAll.push(aux) + resultAll.push(aux); } } const conditionsAny = conditions.any as [IDataObject]; if (conditionsAny) { - for (let conditionAny of conditionsAny) { + for (const conditionAny of conditionsAny) { aux.field = conditionAny.field; aux.operator = conditionAny.operation; if (conditionAny.operation !== 'changed' @@ -470,12 +459,13 @@ export class ZendeskTrigger implements INodeType { } else { aux.value = null; } - resultAny.push(aux) + resultAny.push(aux); } } + const urlParts = urlParse(webhookUrl); const bodyTrigger: IDataObject = { trigger: { - title, + title: `n8n-webhook:${urlParts.path}`, conditions: { all: resultAll, any: resultAny, @@ -487,10 +477,10 @@ export class ZendeskTrigger implements INodeType { } ] }, - } + }; const bodyTarget: IDataObject = { target: { - title: 'N8N webhook', + title: 'n8n webhook', type: 'http_target', target_url: webhookUrl, method: 'POST', @@ -516,7 +506,7 @@ export class ZendeskTrigger implements INodeType { return false; } delete webhookData.webhookId; - delete webhookData.targetId + delete webhookData.targetId; return true; }, }, From dfc3cb962ce2b1aa2e98cce59a1cdbe935091f30 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Tue, 7 Jan 2020 23:23:43 -0600 Subject: [PATCH 010/389] :shirt: Fix lint issue --- packages/editor-ui/src/components/mixins/restApi.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/src/components/mixins/restApi.ts b/packages/editor-ui/src/components/mixins/restApi.ts index a2cdbd6584..72616e83d4 100644 --- a/packages/editor-ui/src/components/mixins/restApi.ts +++ b/packages/editor-ui/src/components/mixins/restApi.ts @@ -261,7 +261,7 @@ export const restApi = Vue.extend({ OAuth2Callback: (code: string, state: string): Promise => { const sendData = { 'code': code, - 'state': state + 'state': state, }; return self.restApi().makeRestApiRequest('POST', `/oauth2-credential/callback`, sendData); From e9a9b58afbb24ccf187e076cdfe5cb65ee2743f4 Mon Sep 17 00:00:00 2001 From: Florian GAULTIER Date: Wed, 8 Jan 2020 11:27:39 +0100 Subject: [PATCH 011/389] Add expire option to redis set --- packages/nodes-base/nodes/Redis/Redis.node.ts | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/packages/nodes-base/nodes/Redis/Redis.node.ts b/packages/nodes-base/nodes/Redis/Redis.node.ts index f3a574a522..beb3bc42dc 100644 --- a/packages/nodes-base/nodes/Redis/Redis.node.ts +++ b/packages/nodes-base/nodes/Redis/Redis.node.ts @@ -250,6 +250,35 @@ export class Redis implements INodeType { default: 'automatic', description: 'The type of the key to set.', }, + + { + displayName: 'Expire', + name: 'expire', + type: 'boolean', + default: false, + description: 'Set a timeout on key ?', + }, + + { + displayName: 'TTL', + name: 'ttl', + type: 'number', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + operation: [ + 'set' + ], + expire: [ + true, + ], + }, + }, + default: 60, + description: 'Number of seconds before key expiration.', + } ] }; @@ -319,7 +348,7 @@ export class Redis implements INodeType { } - async function setValue(client: redis.RedisClient, keyName: string, value: string | number | object | string[] | number[], type?: string) { + async function setValue(client: redis.RedisClient, keyName: string, value: string | number | object | string[] | number[], expire: boolean, ttl: number, type?: string) { if (type === undefined || type === 'automatic') { // Request the type first if (typeof value === 'string') { @@ -335,20 +364,24 @@ export class Redis implements INodeType { if (type === 'string') { const clientSet = util.promisify(client.set).bind(client); - return await clientSet(keyName, value.toString()); + await clientSet(keyName, value.toString()); } else if (type === 'hash') { const clientHset = util.promisify(client.hset).bind(client); for (const key of Object.keys(value)) { await clientHset(keyName, key, (value as IDataObject)[key]!.toString()); } - return; } else if (type === 'list') { const clientLset = util.promisify(client.lset).bind(client); for (let index = 0; index < (value as string[]).length; index++) { await clientLset(keyName, index, (value as IDataObject)[index]!.toString()); } - return; } + + if (expire === true) { + const clientExpire = util.promisify(client.expire).bind(client); + await clientExpire(keyName, ttl); + } + return; } @@ -434,8 +467,10 @@ export class Redis implements INodeType { const keySet = this.getNodeParameter('key', itemIndex) as string; const value = this.getNodeParameter('value', itemIndex) as string; const keyType = this.getNodeParameter('keyType', itemIndex) as string; + const expire = this.getNodeParameter('expire', itemIndex, false) as boolean; + const ttl = this.getNodeParameter('ttl', itemIndex, -1) as number; - await setValue(client, keySet, value, keyType); + await setValue(client, keySet, value, expire, ttl, keyType); returnItems.push(items[itemIndex]); } } From e6b11c9dcccf6130781d15bb49c41019e1961116 Mon Sep 17 00:00:00 2001 From: Florian GAULTIER Date: Wed, 8 Jan 2020 12:22:18 +0100 Subject: [PATCH 012/389] Print expire only when set --- packages/nodes-base/nodes/Redis/Redis.node.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/nodes-base/nodes/Redis/Redis.node.ts b/packages/nodes-base/nodes/Redis/Redis.node.ts index beb3bc42dc..337e0c5537 100644 --- a/packages/nodes-base/nodes/Redis/Redis.node.ts +++ b/packages/nodes-base/nodes/Redis/Redis.node.ts @@ -255,6 +255,13 @@ export class Redis implements INodeType { displayName: 'Expire', name: 'expire', type: 'boolean', + displayOptions: { + show: { + operation: [ + 'set' + ], + }, + }, default: false, description: 'Set a timeout on key ?', }, From 5ecc9553386351d984b030d90b9663bee08be452 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Wed, 8 Jan 2020 11:06:28 -0500 Subject: [PATCH 013/389] fixed issue with url --- packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts b/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts index b9d0763f4b..ef3fc8be7b 100644 --- a/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts @@ -20,14 +20,12 @@ export async function rocketchatApiRequest(this: IHookFunctions | IExecuteFuncti headers: headerWithAuthentication, method, body, - uri: `${credentials.domain}${resource}.${operation}`, + uri: `${credentials.domain}/api/v1${resource}.${operation}`, json: true }; - if (Object.keys(options.body).length === 0) { delete options.body; } - try { return await this.helpers.request!(options); } catch (error) { From 7f21f3eee7041b79d1f639f53d5ad6fc2b27ad57 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 8 Jan 2020 13:25:35 -0600 Subject: [PATCH 014/389] :bug: Fix bug that TextEdit did not display existing values --- packages/editor-ui/src/components/TextEdit.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/editor-ui/src/components/TextEdit.vue b/packages/editor-ui/src/components/TextEdit.vue index 11fa9059bb..5e6fb7de6b 100644 --- a/packages/editor-ui/src/components/TextEdit.vue +++ b/packages/editor-ui/src/components/TextEdit.vue @@ -47,6 +47,9 @@ export default Vue.extend({ return false; }, }, + mounted () { + this.tempValue = this.value as string; + }, watch: { dialogVisible () { if (this.dialogVisible === true) { From 1d1b580200f68a637b0516a99f90272daabda8f7 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 8 Jan 2020 13:38:20 -0600 Subject: [PATCH 015/389] :bookmark: Release n8n-nodes-base@0.40.0 --- packages/nodes-base/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index d90cf83627..a91be93fb6 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-base", - "version": "0.39.0", + "version": "0.40.0", "description": "Base nodes of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From cba55d4f868f2f038c23d86e06dddbd0bceb2d1c Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 8 Jan 2020 13:39:50 -0600 Subject: [PATCH 016/389] :bookmark: Release n8n-editor-ui@0.32.0 --- packages/editor-ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index abbdab8353..ea58983883 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "n8n-editor-ui", - "version": "0.31.0", + "version": "0.32.0", "description": "Workflow Editor UI for n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From a91e66ad169ed051aa00b44efd9a213133124d2d Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 8 Jan 2020 13:41:39 -0600 Subject: [PATCH 017/389] :arrow_up: Set n8n-editor-ui@0.32.0 and n8n-nodes-base@0.40.0 on n8n --- packages/cli/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 220cf6a24b..8a39473ad9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -95,8 +95,8 @@ "lodash.get": "^4.4.2", "mongodb": "^3.2.3", "n8n-core": "~0.20.0", - "n8n-editor-ui": "~0.31.0", - "n8n-nodes-base": "~0.39.0", + "n8n-editor-ui": "~0.32.0", + "n8n-nodes-base": "~0.40.0", "n8n-workflow": "~0.20.0", "open": "^7.0.0", "pg": "^7.11.0", From c78916af5ff06b41b9ff7b83310c3b8511e9be3c Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 8 Jan 2020 13:42:25 -0600 Subject: [PATCH 018/389] :bookmark: Release n8n@0.45.0 --- packages/cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 8a39473ad9..f3fdb87bcd 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "0.44.0", + "version": "0.45.0", "description": "n8n Workflow Automation Tool", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From c174f6cc70b9fd5a352a6a1037b30535e6197945 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 9 Jan 2020 20:05:57 -0600 Subject: [PATCH 019/389] :zap: Make it possible to soft-delete and restore Mattermost-Channels --- .../nodes/Mattermost/Mattermost.node.ts | 83 ++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts b/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts index 1a8262d8d8..fd936e9276 100644 --- a/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts +++ b/packages/nodes-base/nodes/Mattermost/Mattermost.node.ts @@ -85,6 +85,16 @@ export class Mattermost implements INodeType { value: 'create', description: 'Create a new channel', }, + { + name: 'Delete', + value: 'delete', + description: 'Soft-deletes a channel', + }, + { + name: 'Restore', + value: 'restore', + description: 'Restores a soft-deleted channel', + }, { name: 'Statistics', value: 'statistics', @@ -219,6 +229,56 @@ export class Mattermost implements INodeType { }, + // ---------------------------------- + // channel:delete + // ---------------------------------- + { + displayName: 'Channel ID', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + options: [], + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'delete' + ], + resource: [ + 'channel', + ], + }, + }, + description: 'The ID of the channel to soft-delete.', + }, + + + // ---------------------------------- + // channel:restore + // ---------------------------------- + { + displayName: 'Channel ID', + name: 'channelId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'restore' + ], + resource: [ + 'channel', + ], + }, + }, + description: 'The ID of the channel to restore.', + }, + + // ---------------------------------- // channel:addUser // ---------------------------------- @@ -266,6 +326,8 @@ export class Mattermost implements INodeType { }, description: 'The ID of the user to invite into channel.', }, + + // ---------------------------------- // channel:statistics // ---------------------------------- @@ -629,8 +691,7 @@ export class Mattermost implements INodeType { methods = { loadOptions: { - // Get all the available workspaces to display them to user so that he can - // select them easily + // Get all the available channels async getChannels(this: ILoadOptionsFunctions): Promise { const endpoint = 'channels'; const responseData = await apiRequest.call(this, 'GET', endpoint, {}); @@ -754,6 +815,24 @@ export class Mattermost implements INodeType { const type = this.getNodeParameter('type', i) as string; body.type = type === 'public' ? 'O' : 'P'; + } else if (operation === 'delete') { + // ---------------------------------- + // channel:delete + // ---------------------------------- + + requestMethod = 'DELETE'; + const channelId = this.getNodeParameter('channelId', i) as string; + endpoint = `channels/${channelId}`; + + } else if (operation === 'restore') { + // ---------------------------------- + // channel:restore + // ---------------------------------- + + requestMethod = 'POST'; + const channelId = this.getNodeParameter('channelId', i) as string; + endpoint = `channels/${channelId}/restore`; + } else if (operation === 'addUser') { // ---------------------------------- // channel:addUser From 9a2188f22fdcca8076d893cddc2e95833fef4b4d Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 9 Jan 2020 21:23:47 -0600 Subject: [PATCH 020/389] :zip: Do not allow credentials without name --- packages/cli/src/Server.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index f5a608331a..9eecf67062 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -657,6 +657,10 @@ class App { throw new Error('No encryption key got found to encrypt the credentials!'); } + if (incomingData.name === '') { + throw new Error('Credentials have to have a name set!'); + } + // Check if credentials with the same name and type exist already const findQuery = { where: { @@ -696,6 +700,10 @@ class App { const id = req.params.id; + if (incomingData.name === '') { + throw new Error('Credentials have to have a name set!'); + } + // Add the date for newly added node access permissions for (const nodeAccess of incomingData.nodesAccess) { if (!nodeAccess.date) { From 29633cfd1f3baecbb156e61d46fa6004bfd15e36 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 9 Jan 2020 21:53:26 -0600 Subject: [PATCH 021/389] :zap: Add "About n8n" to Help in sidebar --- packages/editor-ui/src/components/About.vue | 88 +++++++++++++++++++ .../editor-ui/src/components/MainSidebar.vue | 37 +++++--- 2 files changed, 113 insertions(+), 12 deletions(-) create mode 100644 packages/editor-ui/src/components/About.vue diff --git a/packages/editor-ui/src/components/About.vue b/packages/editor-ui/src/components/About.vue new file mode 100644 index 0000000000..62563a1c25 --- /dev/null +++ b/packages/editor-ui/src/components/About.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index cbf0664e78..71388c2cd3 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -1,5 +1,6 @@ + + + @@ -168,6 +169,7 @@ import { IWorkflowDataUpdate, } from '../Interface'; +import About from '@/components/About.vue'; import CredentialsEdit from '@/components/CredentialsEdit.vue'; import CredentialsList from '@/components/CredentialsList.vue'; import ExecutionsList from '@/components/ExecutionsList.vue'; @@ -196,6 +198,7 @@ export default mixins( .extend({ name: 'MainHeader', components: { + About, CredentialsEdit, CredentialsList, ExecutionsList, @@ -204,6 +207,7 @@ export default mixins( }, data () { return { + aboutDialogVisible: false, isCollapsed: true, credentialNewDialogVisible: false, credentialOpenDialogVisible: false, @@ -251,9 +255,6 @@ export default mixins( currentWorkflow (): string { return this.$route.params.name; }, - versionCli (): string { - return this.$store.getters.versionCli; - }, workflowExecution (): IExecutionResponse | null { return this.$store.getters.getWorkflowExecution; }, @@ -269,6 +270,9 @@ export default mixins( this.$store.commit('setWorkflowExecutionData', null); this.updateNodesExecutionIssues(); }, + closeAboutDialog () { + this.aboutDialogVisible = false; + }, closeWorkflowOpenDialog () { this.workflowOpenDialogVisible = false; }, @@ -434,6 +438,8 @@ export default mixins( this.saveCurrentWorkflow(); } else if (key === 'workflow-save-as') { this.saveCurrentWorkflow(true); + } else if (key === 'help-about') { + this.aboutDialogVisible = true; } else if (key === 'workflow-settings') { this.workflowSettingsDialogVisible = true; } else if (key === 'workflow-new') { @@ -466,6 +472,9 @@ export default mixins(