From d8870ecbff88819c93e829d145579b7fc9157fc8 Mon Sep 17 00:00:00 2001 From: Cristobal Schlaubitz Garcia <57994942+CxGarcia@users.noreply.github.com> Date: Sun, 15 May 2022 19:48:17 +0200 Subject: [PATCH] feat(Trello Node) Add support for board members and credential tests (#3201) * adds support for trello board member operations: inviteMemberByEmail, addMember, removeMember, getMembers * lintfix * format fixes * remove unnecessary variable and assign to qs on same line * fix description * Moved Board Members to their own resource * Removed members from board resource... * Added return all limits to get members * adds info about Trello premium feature in description * Improvements from internal review * :zap: Improvements * Changed credentials to use new system and implemented test * :zap: Improvements * fix(core): Fix issue with fixedCollection having all default values * :shirt: Fix lint issue Co-authored-by: Jonathan Bennetts Co-authored-by: ricardo Co-authored-by: Jan Oberhauser --- .../credentials/TrelloApi.credentials.ts | 23 +- .../nodes/Pipedrive/Pipedrive.node.ts | 2 +- .../nodes/Trello/BoardDescription.ts | 1 - .../nodes/Trello/BoardMemberDescription.ts | 337 ++++++++++++++++++ .../nodes/Trello/GenericFunctions.ts | 17 +- .../nodes-base/nodes/Trello/Trello.node.ts | 72 ++++ .../nodes/Trello/TrelloTrigger.node.ts | 6 +- 7 files changed, 440 insertions(+), 18 deletions(-) create mode 100644 packages/nodes-base/nodes/Trello/BoardMemberDescription.ts diff --git a/packages/nodes-base/credentials/TrelloApi.credentials.ts b/packages/nodes-base/credentials/TrelloApi.credentials.ts index 0d50dc8d86..f110f1528a 100644 --- a/packages/nodes-base/credentials/TrelloApi.credentials.ts +++ b/packages/nodes-base/credentials/TrelloApi.credentials.ts @@ -1,9 +1,11 @@ import { + ICredentialDataDecryptedObject, + ICredentialTestRequest, ICredentialType, + IHttpRequestOptions, INodeProperties, } from 'n8n-workflow'; - export class TrelloApi implements ICredentialType { name = 'trelloApi'; displayName = 'Trello API'; @@ -13,19 +15,36 @@ export class TrelloApi implements ICredentialType { displayName: 'API Key', name: 'apiKey', type: 'string', + required: true, default: '', }, { displayName: 'API Token', name: 'apiToken', type: 'string', + required: true, default: '', }, { displayName: 'OAuth Secret', name: 'oauthSecret', - type: 'string', + type: 'hidden', default: '', }, ]; + + async authenticate(credentials: ICredentialDataDecryptedObject, requestOptions: IHttpRequestOptions): Promise { + requestOptions.qs = { + ...requestOptions.qs, + 'key': credentials.apiKey, + 'token': credentials.apiToken, + }; + return requestOptions; + } + test: ICredentialTestRequest = { + request: { + baseURL: 'https://api.trello.com', + url: '=/1/tokens/{{$credentials.apiToken}}/member', + }, + }; } diff --git a/packages/nodes-base/nodes/Pipedrive/Pipedrive.node.ts b/packages/nodes-base/nodes/Pipedrive/Pipedrive.node.ts index c24dee7665..8ded7998df 100644 --- a/packages/nodes-base/nodes/Pipedrive/Pipedrive.node.ts +++ b/packages/nodes-base/nodes/Pipedrive/Pipedrive.node.ts @@ -3663,7 +3663,7 @@ export class Pipedrive implements INodeType { loadOptionsMethod: 'getFilters', }, default: '', - description: 'ID of the filter to use.', + description: 'ID of the filter to use', }, ], }, diff --git a/packages/nodes-base/nodes/Trello/BoardDescription.ts b/packages/nodes-base/nodes/Trello/BoardDescription.ts index a33440a68f..45f7688c8f 100644 --- a/packages/nodes-base/nodes/Trello/BoardDescription.ts +++ b/packages/nodes-base/nodes/Trello/BoardDescription.ts @@ -455,5 +455,4 @@ export const boardFields: INodeProperties[] = [ }, ], }, - ]; diff --git a/packages/nodes-base/nodes/Trello/BoardMemberDescription.ts b/packages/nodes-base/nodes/Trello/BoardMemberDescription.ts new file mode 100644 index 0000000000..081097e3a0 --- /dev/null +++ b/packages/nodes-base/nodes/Trello/BoardMemberDescription.ts @@ -0,0 +1,337 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const boardMemberOperations: INodeProperties[] = [ + // ---------------------------------- + // boardMember + // ---------------------------------- + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'boardMember', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add member to board using member ID', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all members of a board', + }, + { + name: 'Invite', + value: 'invite', + description: 'Invite a new member to a board via email', + }, + { + name: 'Remove', + value: 'remove', + description: 'Remove member from board using member ID', + }, + ], + default: 'add', + description: 'The operation to perform.', + }, +]; + +export const boardMemberFields: INodeProperties[] = [ + // ---------------------------------- + // boardMember:getAll + // ---------------------------------- + { + displayName: 'Board ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'boardMember', + ], + }, + }, + description: 'The ID of the board to get members from', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'boardMember', + ], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + description: 'Max number of results to return', + default: 20, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'boardMember', + ], + returnAll: [ + false, + ], + }, + }, + }, + + // ---------------------------------- + // boardMember:add + // ---------------------------------- + { + displayName: 'Board ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'boardMember', + ], + }, + }, + description: 'The ID of the board to add member to', + }, + { + displayName: 'Member ID', + name: 'idMember', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'boardMember', + ], + }, + }, + description: 'The ID of the member to add to the board', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + required: true, + default: 'normal', + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'boardMember', + ], + }, + }, + options: [ + { + name: 'Normal', + value: 'normal', + description: 'Invite as normal member', + }, + { + name: 'Admin', + value: 'admin', + description: 'Invite as admin', + }, + { + name: 'Observer', + value: 'observer', + description: 'Invite as observer (Trello premium feature)', + }, + ], + description: 'Determines the type of membership the user being added should have', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'boardMember', + ], + }, + }, + options: [ + { + displayName: 'Allow Billable Guest', + name: 'allowBillableGuest', + type: 'boolean', + default: false, + description: 'Allows organization admins to add multi-board guests onto a board', + }, + ], + }, + + // ---------------------------------- + // boardMember:invite + // ---------------------------------- + { + displayName: 'Board ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'invite', + ], + resource: [ + 'boardMember', + ], + }, + }, + description: 'The ID of the board to invite member to', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'invite', + ], + resource: [ + 'boardMember', + ], + }, + }, + description: 'The ID of the board to update', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'invite', + ], + resource: [ + 'boardMember', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + default: 'normal', + options: [ + { + name: 'Normal', + value: 'normal', + description: 'Invite as normal member', + }, + { + name: 'Admin', + value: 'admin', + description: 'Invite as admin', + }, + { + name: 'Observer', + value: 'observer', + description: 'Invite as observer (Trello premium feature)', + }, + ], + description: 'Determines the type of membership the user being added should have', + }, + { + displayName: 'Full Name', + name: 'fullName', + type: 'string', + default: '', + description: 'The full name of the user to add as a member of the board. Must have a length of at least 1 and cannot begin nor end with a space.', + }, + ], + }, + + // ---------------------------------- + // boardMember:remove + // ---------------------------------- + { + displayName: 'Board ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'remove', + ], + resource: [ + 'boardMember', + ], + }, + }, + description: 'The ID of the board to remove member from', + }, + { + displayName: 'Member ID', + name: 'idMember', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'remove', + ], + resource: [ + 'boardMember', + ], + }, + }, + description: 'The ID of the member to remove from the board', + }, +]; diff --git a/packages/nodes-base/nodes/Trello/GenericFunctions.ts b/packages/nodes-base/nodes/Trello/GenericFunctions.ts index 4556cba88b..daee29cb49 100644 --- a/packages/nodes-base/nodes/Trello/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Trello/GenericFunctions.ts @@ -9,7 +9,9 @@ import { } from 'request'; import { - IDataObject, NodeApiError, NodeOperationError, + IDataObject, + JsonObject, + NodeApiError, } from 'n8n-workflow'; /** @@ -22,16 +24,9 @@ import { * @returns {Promise} */ export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, query?: IDataObject): Promise { // tslint:disable-line:no-any - const credentials = await this.getCredentials('trelloApi'); - query = query || {}; - query.key = credentials.apiKey; - query.token = credentials.apiToken; - const options: OptionsWithUri = { - headers: { - }, method, body, qs: query, @@ -40,9 +35,9 @@ export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoa }; try { - return await this.helpers.request!(options); - } catch (error) { - throw new NodeApiError(this.getNode(), error); + return await this.helpers.requestWithAuthentication.call(this, 'trelloApi', options); + } catch(error) { + throw new NodeApiError(this.getNode(), error as JsonObject); } } diff --git a/packages/nodes-base/nodes/Trello/Trello.node.ts b/packages/nodes-base/nodes/Trello/Trello.node.ts index 7eb3a20ed7..2467c97153 100644 --- a/packages/nodes-base/nodes/Trello/Trello.node.ts +++ b/packages/nodes-base/nodes/Trello/Trello.node.ts @@ -25,6 +25,11 @@ import { boardOperations, } from './BoardDescription'; +import { + boardMemberFields, + boardMemberOperations, +} from './BoardMemberDescription'; + import { cardFields, cardOperations, @@ -84,6 +89,10 @@ export class Trello implements INodeType { name: 'Board', value: 'board', }, + { + name: 'Board Member', + value: 'boardMember', + }, { name: 'Card', value: 'card', @@ -114,6 +123,7 @@ export class Trello implements INodeType { // ---------------------------------- ...attachmentOperations, ...boardOperations, + ...boardMemberOperations, ...cardOperations, ...cardCommentOperations, ...checklistOperations, @@ -125,6 +135,7 @@ export class Trello implements INodeType { // ---------------------------------- ...attachmentFields, ...boardFields, + ...boardMemberFields, ...cardFields, ...cardCommentFields, ...checklistFields, @@ -216,7 +227,68 @@ export class Trello implements INodeType { } else { throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known!`); } + } else if (resource === 'boardMember') { + if (operation === 'getAll') { + // ---------------------------------- + // getAll + // ---------------------------------- + requestMethod = 'GET'; + + const id = this.getNodeParameter('id', i) as string; + returnAll = this.getNodeParameter('returnAll', i) as boolean; + if (returnAll === false) { + qs.limit = this.getNodeParameter('limit', i) as number; + } + + endpoint = `boards/${id}/members`; + + } else if (operation === 'add') { + // ---------------------------------- + // add + // ---------------------------------- + + requestMethod = 'PUT'; + + const id = this.getNodeParameter('id', i) as string; + const idMember = this.getNodeParameter('idMember', i) as string; + + endpoint = `boards/${id}/members/${idMember}`; + + qs.type = this.getNodeParameter('type', i) as string; + qs.allowBillableGuest = this.getNodeParameter('additionalFields.allowBillableGuest', i, false) as boolean; + + } else if (operation === 'invite') { + // ---------------------------------- + // invite + // ---------------------------------- + + requestMethod = 'PUT'; + + const id = this.getNodeParameter('id', i) as string; + + endpoint = `boards/${id}/members`; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + qs.email = this.getNodeParameter('email', i) as string; + qs.type = additionalFields.type as string; + body.fullName = additionalFields.fullName as string; + } else if (operation === 'remove') { + // ---------------------------------- + // remove + // ---------------------------------- + + requestMethod = 'DELETE'; + + const id = this.getNodeParameter('id', i) as string; + const idMember = this.getNodeParameter('idMember', i) as string; + + endpoint = `boards/${id}/members/${idMember}`; + + } else { + throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not known!`); + } } else if (resource === 'card') { if (operation === 'create') { diff --git a/packages/nodes-base/nodes/Trello/TrelloTrigger.node.ts b/packages/nodes-base/nodes/Trello/TrelloTrigger.node.ts index 04e1423543..2c3f40461c 100644 --- a/packages/nodes-base/nodes/Trello/TrelloTrigger.node.ts +++ b/packages/nodes-base/nodes/Trello/TrelloTrigger.node.ts @@ -159,10 +159,10 @@ export class TrelloTrigger implements INodeType { const bodyData = this.getBodyData(); - const credentials = await this.getCredentials('trelloApi'); - // TODO: Check why that does not work as expected even though it gets done as described - // https://developers.trello.com/page/webhooks + // https://developers.trello.com/page/webhooks + + //const credentials = await this.getCredentials('trelloApi'); // // Check if the request is valid // const headerData = this.getHeaderData() as IDataObject; // const webhookUrl = this.getNodeWebhookUrl('default');