From 7d2e8576137b07587c15d86e750b8af2d00e8b84 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sun, 5 Jan 2020 13:34:09 -0500 Subject: [PATCH 1/5] 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 zcmY*cXEYqz79PathUn!*OY|{Hl$q#V41%jhnbCV0y+!XLhUh|wkf@U(qD4f^Ac7#G zjT&7dA&j0p?!E86_s&}ToW1w?&iCzoew?*V>K#KJI%-a8002Ozr>kjlu_|5~CGcW~ zolT=I7LouH9d$s%F!$y~K;@@v6#xLxFkKo6ps0lX0$}C-hh?Cp0UYM&>n(wF@^x^Q z2=(^6KwmJ0!Y*8I=RhQHsJE9-04!9A|4#($!oN&I_<8??1bQm*TN>Qq)$m0*^U6xx zl#t|Crsn14RYWZDe=1n2KvDukdTlNi4YkHUz96E3JQfnB&8wJ(%=gO zI3Ua?5E%;g2@v>;P@ob&|E17BUS?rND0k-z1s{P?c{>|Du#a4%-6dJJey5ksQKbu7d7J1I{SQEUGyd7stx1wGB#48#@^Vo0#5s?A|slZV)WbCeUu|4$q*o z%t@1T$3~1m1MusG0XuU6Bl8i5_sETd7k7>e3J)K-u02|hT2~V5^{{#rJHcW3&vfKF z6ayE|t9Y|upcwKy_&i*II`Q#szv$?yt+rxNdb)|%Tn%oe(lFhC)M)ZtMbYAi9(trB z`1AMnh@8E12$hbLrf?*m|71OXV(f=&BDe+8sDl_`)+n*?+0vIaA-hz_4?!Q-pO`r# z9z9PIbibW7NiiW5KfT%K7&gYj2C+t1y=gYQ!- zE8m#~ZF*~cc3kN`Li^9i?rHbF*OKF+HtO<;{Oyu4O!lybWT$kk5@STCWi(DM1LK1ibXs@V)5M8^BU?8%qCL1 zQ1&2%Ez{8mV&NY76t|R$5oYzIpg_W=+bw}oeOIPDUOtwa3-O$B8zxLbWMW8uL$j87 z?FEXOyui1Heca8niW3syw2&P*S4VjURf^bGn{|jR{SIzHJ#ZVzl5N`$Fm0^B9adsz zbRplBun^WuQn5xTN52-7YRS)M;FC5_yHZ z*HX>ttH>mr$9(~!vZHv4!>&g~a_?>JrRB3uUm+nLyJjl@GNP^1E_?RMF1}&lm6(WE5_kjF=Vn_NxfZ>vi=T( zhu^;5*Kc$9R})s~LD}oMSnMa3sP6vjA`21HCB?ra>N5LgHn_7q2pcb0y7i!Zs)A)X zhy5j;k9##oLx9@XfKwweFZ8EEpbYj{Td_lgQ`SQw$kCa{FfhYPW!EKf)QU6S!DiSCvm%6k3xXr3$m@{VQm%mR}pQ%cPWMBlK&Iy6uwO{fX{_Gn>b;=%W_u%{X323&u(@h5P)mQq~sW14h3EefF>h5Rq3# zdU$S<*i9CL>pF-|VL$c^)pn_xk`}05nduw0DCpv-tpz3@#@bxN@rK21BKQq@`(vYN zDUOrB3ZI@9X1Nujy%#=}4FeqCjLCQ0(oRZ18zDzbI(?)y;dHoC0)w2j_OFP?jCiaj z^m&V3OEtV!SM}?)ZR~aVWX@(bTQK%A1#^|a~>(_%*xDOwJyuhf1zz# zV2?!(-G9%_Q|DCn_X{_C=hnP~iESVw6B2l39rJmQC6it*m*^ku(#|f0x`FjHbD1N4 z0|a{sQ=vT&D}6mf`vQrkJ^Z^D|5 zXhZIjf6ySM8fc9duvawtPH{%;3~F z!ZPn#HhCORP{l<79;-KgHRG#}Bk40bp9RxL(YBTK`2(t-nG1RAy_QP3wF*|$+MJ#P zQA&ZoZq39~kQNRM2upwu``06Sd(>I|+eQdnb`ab?ZDd6}T;p&;5h0CQ`sm=&w?Vv9 zX{65Ugl~xU7nuw`+1eE>iLlT@zx#=?z!*mKdp|4vme=hv`q}M z-hV==LX=uCuy}T0pN0Qzs1UGUZ0HyG60a4{w3?3xWS)cBA zY6FdwAa5pKS^lAI0X5PXNf~1C7?L~luj8g^)Et)DP2=Xen*4#D{18as;rPQ?R`yo4F- z;{-cmvI6%^!W3lZgZ%>>4dy+oCnu&kcOy1huCP~p4ybO~T$P$Ew#%{n3XrYw#OikRoGg=M^*1&Jp2*lpg@O+p;r9qro1( zA0(murSsKsUFn}+Y2f7U&1x;lZ08TriQN@vm0Vq3Q`#6zd>LcK7?qnmZV@lQLt?7z zy1<7;Zu>9;byJ3qeD&F0Qp@4|F)1o_PZztIi{`}CZ*r;Q7@L!L-w_v;f4*lrIm!V;e83`C!SVrCR9z> y8ik~Ng?4C?HLOD<+U)ZdZQXG97OhPO#}R1Q6fYSH$&R`F|LAEMYBs1N;{FZy6;1B| 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 2/5] 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 3/5] 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 4/5] :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 5/5] :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": {