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

*  Improvements

* Changed credentials to use new system and implemented test

*  Improvements

* fix(core): Fix issue with fixedCollection having all default values

* 👕 Fix lint issue

Co-authored-by: Jonathan Bennetts <jonathan.bennetts@gmail.com>
Co-authored-by: ricardo <ricardoespinoza105@gmail.com>
Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
Cristobal Schlaubitz Garcia 2022-05-15 19:48:17 +02:00 committed by GitHub
parent 7ced65484f
commit d8870ecbff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 440 additions and 18 deletions

View file

@ -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<IHttpRequestOptions> {
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',
},
};
}

View file

@ -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',
},
],
},

View file

@ -455,5 +455,4 @@ export const boardFields: INodeProperties[] = [
},
],
},
];

View file

@ -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',
},
];

View file

@ -9,7 +9,9 @@ import {
} from 'request';
import {
IDataObject, NodeApiError, NodeOperationError,
IDataObject,
JsonObject,
NodeApiError,
} from 'n8n-workflow';
/**
@ -22,16 +24,9 @@ import {
* @returns {Promise<any>}
*/
export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, query?: IDataObject): Promise<any> { // 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);
}
}

View file

@ -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') {

View file

@ -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
//const credentials = await this.getCredentials('trelloApi');
// // Check if the request is valid
// const headerData = this.getHeaderData() as IDataObject;
// const webhookUrl = this.getNodeWebhookUrl('default');