diff --git a/packages/nodes-base/nodes/Asana/Asana.node.ts b/packages/nodes-base/nodes/Asana/Asana.node.ts index 507a2c44de..be5269fa7e 100644 --- a/packages/nodes-base/nodes/Asana/Asana.node.ts +++ b/packages/nodes-base/nodes/Asana/Asana.node.ts @@ -1,17 +1,19 @@ import { IExecuteFunctions, } from 'n8n-core'; + import { IDataObject, ILoadOptionsFunctions, - INodePropertyOptions, - INodeTypeDescription, INodeExecutionData, + INodePropertyOptions, INodeType, + INodeTypeDescription, } from 'n8n-workflow'; import { asanaApiRequest, + asanaApiRequestAllItems, } from './GenericFunctions'; export class Asana implements INodeType { @@ -22,10 +24,10 @@ export class Asana implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', - description: 'Access and edit Asana tasks', + description: 'Consume Asana REST API', defaults: { name: 'Asana', - color: '#339922', + color: '#FC636B', }, inputs: ['main'], outputs: ['main'], @@ -33,7 +35,7 @@ export class Asana implements INodeType { { name: 'asanaApi', required: true, - } + }, ], properties: [ { @@ -41,10 +43,22 @@ export class Asana implements INodeType { name: 'resource', type: 'options', options: [ + { + name: 'Project', + value: 'project', + }, { name: 'Task', value: 'task', }, + { + name: 'Task Comment', + value: 'taskComment', + }, + { + name: 'Task Tag', + value: 'taskTag', + }, { name: 'User', value: 'user', @@ -54,8 +68,6 @@ export class Asana implements INodeType { description: 'The resource to operate on.', }, - - // ---------------------------------- // task // ---------------------------------- @@ -84,18 +96,23 @@ export class Asana implements INodeType { { name: 'Get', value: 'get', - description: 'Get data of a task', + description: 'Get a task', }, { - name: 'Update', - value: 'update', - description: 'Update a task', + name: 'Move', + value: 'move', + description: 'Move a task', }, { name: 'Search', value: 'search', description: 'Search for tasks', }, + { + name: 'Update', + value: 'update', + description: 'Update a task', + }, ], default: 'create', description: 'The operation to perform.', @@ -146,7 +163,7 @@ export class Asana implements INodeType { }, // ---------------------------------- - // delete + // task:delete // ---------------------------------- { displayName: 'Task ID', @@ -168,7 +185,7 @@ export class Asana implements INodeType { }, // ---------------------------------- - // get + // task:get // ---------------------------------- { displayName: 'Task ID', @@ -190,7 +207,77 @@ export class Asana implements INodeType { }, // ---------------------------------- - // update + // task:move + // ---------------------------------- + + { + displayName: 'Task ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'move', + ], + resource: [ + 'task', + ], + }, + }, + description: 'The ID of the task to be moved.', + }, + { + displayName: 'Project', + name: 'projectId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getProjects', + }, + options: [], + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'move', + ], + resource: [ + 'task', + ], + }, + }, + description: 'Project to show the sections of.', + }, + { + displayName: 'Section', + name: 'section', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getSections', + loadOptionsDependsOn: [ + 'projectId', + ], + }, + options: [], + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'move', + ], + resource: [ + 'task', + ], + }, + }, + description: 'The Section to move the task to', + }, + + // ---------------------------------- + // task:update // ---------------------------------- { displayName: 'Task ID', @@ -211,9 +298,8 @@ export class Asana implements INodeType { description: 'The ID of the task to update the data of.', }, - // ---------------------------------- - // search + // task:search // ---------------------------------- { displayName: 'Workspace', @@ -235,10 +321,10 @@ export class Asana implements INodeType { ], }, }, - description: 'The workspace to create the task in', + description: 'The workspace in which the task is searched', }, { - displayName: 'Search Properties', + displayName: 'Filters', name: 'searchTaskProperties', type: 'collection', displayOptions: { @@ -253,9 +339,15 @@ export class Asana implements INodeType { }, default: {}, description: 'Properties to search for', - placeholder: 'Add Search Property', + placeholder: 'Add Filter', options: [ - // TODO: Add "assignee" and "assignee_status" + { + displayName: 'Completed', + name: 'completed', + type: 'boolean', + default: false, + description: 'If the task is marked completed.', + }, { displayName: 'Text', name: 'text', @@ -267,21 +359,14 @@ export class Asana implements INodeType { default: '', description: 'Text to search for in name or notes.', }, - { - displayName: 'Completed', - name: 'completed', - type: 'boolean', - default: false, - description: 'If the task is marked completed.', - }, ], }, // ---------------------------------- - // create/update + // task:create/update // ---------------------------------- { - displayName: 'Other Properties', + displayName: 'Additional Fields', name: 'otherProperties', type: 'collection', displayOptions: { @@ -296,34 +381,42 @@ export class Asana implements INodeType { }, }, default: {}, - description: 'Other properties to set', - placeholder: 'Add Property', + placeholder: 'Add Field', options: [ { - displayName: 'Name', - name: 'name', - type: 'string', - default: '', - displayOptions: { - show: { - '/operation': [ - 'update', - ], - }, - }, - description: 'The new name of the task', - }, - // TODO: Add "assignee" and "assignee_status" - { - displayName: 'Notes', - name: 'notes', - type: 'string', + displayName: 'Assignee', + name: 'assignee', + type: 'options', typeOptions: { - alwaysOpenEditWindow: true, - rows: 5, + loadOptionsMethod: 'getUsers', }, default: '', - description: 'The task notes', + description: 'Set Assignee on the task', + }, + { + displayName: 'Assignee Status', + name: 'assignee_status', + type: 'options', + options: [ + { + name: 'Inbox', + value: 'inbox', + }, + { + name: 'Today', + value: 'today', + }, + { + name: 'Upcoming', + value: 'upcoming', + }, + { + name: 'Later', + value: 'later', + }, + ], + default: 'inbox', + description: 'Set Assignee status on the task (requires Assignee)', }, { displayName: 'Completed', @@ -339,6 +432,20 @@ export class Asana implements INodeType { default: '', description: 'Date on which the time is due.', }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + displayOptions: { + show: { + '/operation': [ + 'update', + ], + }, + }, + description: 'The new name of the task', + }, { displayName: 'Liked', name: 'liked', @@ -346,10 +453,307 @@ export class Asana implements INodeType { default: false, description: 'If the task is liked by the authorized user.', }, + { + displayName: 'Notes', + name: 'notes', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + rows: 5, + }, + default: '', + description: 'The task notes', + }, ], }, + // ---------------------------------- + // taskComment + // ---------------------------------- + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'taskComment', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add a comment to a task', + }, + { + name: 'Remove', + value: 'remove', + description: 'Remove a comment from a task', + }, + ], + default: 'add', + description: 'The operation to perform.', + }, + + // ---------------------------------- + // taskComment:add + // ---------------------------------- + + { + displayName: 'Task ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'taskComment', + ], + }, + }, + description: 'The ID of the task to add the comment to', + }, + { + displayName: 'Is Text HTML', + name: 'isTextHtml', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'taskComment', + ], + }, + }, + default: false, + description: 'If body is HTML or simple text.', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + required: true, + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'taskComment', + ], + isTextHtml: [ + false, + ], + }, + }, + description: 'The plain text of the comment to add', + }, + { + displayName: 'HTML Text', + name: 'html_text', + type: 'string', + default: '', + required: true, + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'taskComment', + ], + isTextHtml: [ + true, + ], + }, + }, + description: 'Comment as HTML string. Do not use together with plain text.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'taskComment', + ], + }, + }, + default: {}, + description: 'Properties of the task comment', + placeholder: 'Add Field', + options: [ + { + displayName: 'Pinned', + name: 'is_pinned', + type: 'boolean', + default: false, + description: 'Pin the comment.', + }, + ], + }, + + // ---------------------------------- + // taskComment:remove + // ---------------------------------- + + { + displayName: 'Comment ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'remove', + ], + resource: [ + 'taskComment', + ], + }, + }, + description: 'The ID of the comment to be removed', + }, + + // ---------------------------------- + // taskTag + // ---------------------------------- + + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'taskTag', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add a tag to a task', + }, + { + name: 'Remove', + value: 'remove', + description: 'Remove a tag from a task', + }, + ], + default: 'add', + description: 'The operation to perform.', + }, + + // ---------------------------------- + // taskTag:add + // ---------------------------------- + + { + displayName: 'Task ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'taskTag', + ], + }, + }, + description: 'The ID of the task to add the tag to', + }, + { + displayName: 'Tags', + name: 'tag', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'taskTag', + ], + }, + }, + description: 'The tag that should be added', + }, + + // ---------------------------------- + // taskTag:remove + // ---------------------------------- + + { + displayName: 'Task ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'remove', + ], + resource: [ + 'taskTag', + ], + }, + }, + description: 'The ID of the task to add the tag to', + }, + { + displayName: 'Tags', + name: 'tag', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'remove', + ], + resource: [ + 'taskTag', + ], + }, + }, + description: 'The tag that should be added', + }, // ---------------------------------- // user @@ -366,15 +770,15 @@ export class Asana implements INodeType { }, }, options: [ - { - name: 'Get All', - value: 'getAll', - description: 'Get data of all users', - }, { name: 'Get', value: 'get', - description: 'Get data of a user', + description: 'Get a user', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all users', }, ], default: 'get', @@ -385,7 +789,7 @@ export class Asana implements INodeType { // user:get // ---------------------------------- { - displayName: 'Id', + displayName: 'User ID', name: 'userId', type: 'string', default: '', @@ -428,6 +832,165 @@ export class Asana implements INodeType { }, description: 'The workspace in which to get users.', }, + + // ---------------------------------- + // Project + // ---------------------------------- + + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'project', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get a project', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all projects', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, + + // ---------------------------------- + // project:get + // ---------------------------------- + { + displayName: 'Project ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'project', + ], + }, + }, + }, + + // ---------------------------------- + // project:getAll + // ---------------------------------- + { + displayName: 'Workspace', + name: 'workspace', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkspaces', + }, + options: [], + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'project', + ], + }, + }, + description: 'The workspace in which to get users.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'project', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'project', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + displayOptions: { + show: { + resource: [ + 'project', + ], + operation: [ + 'getAll', + ], + }, + }, + default: {}, + description: 'Other properties to set', + placeholder: 'Add Property', + options: [ + { + displayName: 'Archived', + name: 'archived', + type: 'boolean', + default: false, + description: 'Only return projects whose archived field takes on the value of this parameter.', + }, + { + displayName: 'Teams', + name: 'team', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'workspace', + ], + loadOptionsMethod: 'getTeams', + }, + default: '', + description: 'The new name of the task', + }, + ], + }, ], }; @@ -436,15 +999,11 @@ export class Asana implements INodeType { // Get all the available workspaces to display them to user so that he can // select them easily async getWorkspaces(this: ILoadOptionsFunctions): Promise { - const endpoint = 'workspaces'; - const responseData = await asanaApiRequest.call(this, 'GET', endpoint, {}); - - if (responseData.data === undefined) { - throw new Error('No data got returned'); - } + const endpoint = '/workspaces'; + const responseData = await asanaApiRequestAllItems.call(this, 'GET', endpoint, {}); const returnData: INodePropertyOptions[] = []; - for (const workspaceData of responseData.data) { + for (const workspaceData of responseData) { if (workspaceData.resource_type !== 'workspace') { // Not sure if for some reason also ever other resources // get returned but just in case filter them out @@ -457,12 +1016,155 @@ export class Asana implements INodeType { }); } + return returnData; + }, + + // Get all the available projects to display them to user so that they can be + // selected easily + async getProjects(this: ILoadOptionsFunctions): Promise { + const endpoint = '/projects'; + const responseData = await asanaApiRequest.call(this, 'GET', endpoint, {}); + + if (responseData.data === undefined) { + throw new Error('No data got returned'); + } + + const returnData: INodePropertyOptions[] = []; + for (const projectData of responseData.data) { + if (projectData.resource_type !== 'project') { + // Not sure if for some reason also ever other resources + // get returned but just in case filter them out + continue; + } + const projectName = projectData.name; + const projectId = projectData.gid; + + returnData.push({ + name: projectName, + value: projectId, + }); + } + + return returnData; + }, + // Get all the available sections in a project to display them to user so that they + // can be selected easily + async getSections(this: ILoadOptionsFunctions): Promise { + const projectId = this.getNodeParameter('projectId') as string; + const endpoint = `/projects/${projectId}/sections`; + const responseData = await asanaApiRequest.call(this, 'GET', endpoint, {}); + + if (responseData.data === undefined) { + throw new Error('No data got returned'); + } + + const returnData: INodePropertyOptions[] = []; + for (const sectionData of responseData.data) { + if (sectionData.resource_type !== 'section') { + // Not sure if for some reason also ever other resources + // get returned but just in case filter them out + continue; + } + + returnData.push({ + name: sectionData.name, + value: sectionData.gid, + }); + } + + return returnData; + }, + + // Get all the available teams to display them to user so that he can + // select them easily + async getTeams(this: ILoadOptionsFunctions): Promise { + const workspaceId = this.getCurrentNodeParameter('workspace'); + + const workspace = await asanaApiRequest.call(this, 'GET', `/workspaces/${workspaceId}`, {}); + + // if the workspace selected it's not an organization then error as they endpoint + // to retrieve the teams from an organization just work with workspaces that are an organization + + if (workspace.is_organization === false) { + throw Error('To filter by team, the workspace selected has to be an organization'); + } + + const endpoint = `/organizations/${workspaceId}/teams`; + + const responseData = await asanaApiRequestAllItems.call(this, 'GET', endpoint, {}); + + const returnData: INodePropertyOptions[] = []; + for (const teamData of responseData) { + if (teamData.resource_type !== 'team') { + // Not sure if for some reason also ever other resources + // get returned but just in case filter them out + continue; + } + + returnData.push({ + name: teamData.name, + value: teamData.gid, + }); + } + + return returnData; + }, + + // Get all tags to display them to user so that they can be selected easily + // See: https://developers.asana.com/docs/get-multiple-tags + async getTags(this: ILoadOptionsFunctions): Promise { + const endpoint = '/tags'; + const responseData = await asanaApiRequest.call(this, 'GET', endpoint, {}); + + if (responseData.data === undefined) { + throw new Error('No data got returned'); + } + + const returnData: INodePropertyOptions[] = []; + for (const tagData of responseData.data) { + if (tagData.resource_type !== 'tag') { + // Not sure if for some reason also ever other resources + // get returned but just in case filter them out + continue; + } + + returnData.push({ + name: tagData.name, + value: tagData.gid, + }); + } + + return returnData; + }, + // Get all users to display them to user so that they can be selected easily + // See: https://developers.asana.com/docs/get-multiple-users + async getUsers(this: ILoadOptionsFunctions): Promise { + const endpoint = `/users`; + const responseData = await asanaApiRequest.call(this, 'GET', endpoint, {}); + + if (responseData.data === undefined) { + throw new Error('No data got returned'); + } + + const returnData: INodePropertyOptions[] = []; + for (const userData of responseData.data) { + if (userData.resource_type !== 'user') { + // Not sure if for some reason also ever other resources + // get returned but just in case filter them out + continue; + } + + returnData.push({ + name: userData.name, + value: userData.gid, + }); + } + return returnData; } }, }; - async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: IDataObject[] = []; @@ -481,6 +1183,7 @@ export class Asana implements INodeType { let body: IDataObject; let qs: IDataObject; + let responseData; for (let i = 0; i < items.length; i++) { body = {}; @@ -489,11 +1192,11 @@ export class Asana implements INodeType { if (resource === 'task') { if (operation === 'create') { // ---------------------------------- - // create + // task:create // ---------------------------------- requestMethod = 'POST'; - endpoint = 'tasks'; + endpoint = '/tasks'; body.name = this.getNodeParameter('name', i) as string; // body.notes = this.getNodeParameter('taskNotes', 0) as string; @@ -502,50 +1205,177 @@ export class Asana implements INodeType { const otherProperties = this.getNodeParameter('otherProperties', i) as IDataObject; Object.assign(body, otherProperties); + responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.data; + } else if (operation === 'delete') { // ---------------------------------- - // delete + // task:delete // ---------------------------------- requestMethod = 'DELETE'; - endpoint = 'tasks/' + this.getNodeParameter('id', i) as string; + + endpoint = '/tasks/' + this.getNodeParameter('id', i) as string; + + responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.data; } else if (operation === 'get') { // ---------------------------------- - // get + // task:get // ---------------------------------- requestMethod = 'GET'; - endpoint = 'tasks/' + this.getNodeParameter('id', i) as string; + + endpoint = '/tasks/' + this.getNodeParameter('id', i) as string; + + responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.data; + + } else if (operation === 'move') { + // ---------------------------------- + // task:move + // ---------------------------------- + + const sectionId = this.getNodeParameter('section', i) as string; + + requestMethod = 'POST'; + + endpoint = `/sections/${sectionId}/addTask`; + + body.task = this.getNodeParameter('id', i) as string; + + Object.assign(body); + + responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = { success: true }; } else if (operation === 'update') { // ---------------------------------- - // update + // task:update // ---------------------------------- requestMethod = 'PUT'; - endpoint = 'tasks/' + this.getNodeParameter('id', i) as string; + endpoint = '/tasks/' + this.getNodeParameter('id', i) as string; const otherProperties = this.getNodeParameter('otherProperties', i) as IDataObject; Object.assign(body, otherProperties); + responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.data; + } else if (operation === 'search') { // ---------------------------------- - // search + // tasksearch // ---------------------------------- const workspaceId = this.getNodeParameter('workspace', i) as string; requestMethod = 'GET'; - endpoint = `workspaces/${workspaceId}/tasks/search`; + endpoint = `/workspaces/${workspaceId}/tasks/search`; const searchTaskProperties = this.getNodeParameter('searchTaskProperties', i) as IDataObject; Object.assign(qs, searchTaskProperties); - } else { - throw new Error(`The operation "${operation}" is not known!`); + responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.data; } - } else if (resource === 'user') { + } + if (resource === 'taskComment') { + if (operation === 'add') { + // ---------------------------------- + // taskComment:add + // ---------------------------------- + + const taskId = this.getNodeParameter('id', i) as string; + + const isTextHtml = this.getNodeParameter('isTextHtml', i) as boolean; + + if (!isTextHtml) { + + body.text = this.getNodeParameter('text', i) as string; + + } else { + + body.html_text = this.getNodeParameter('html_text', i) as string; + } + + requestMethod = 'POST'; + + endpoint = `/tasks/${taskId}/stories`; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + Object.assign(body, additionalFields); + + responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.data; + } + + if (operation === 'remove') { + // ---------------------------------- + // taskComment:remove + // ---------------------------------- + + const commentId = this.getNodeParameter('id', i) as string; + + requestMethod = 'DELETE'; + + endpoint = `/stories/${commentId}`; + + responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = { success: true }; + } + } + if (resource === 'taskTag') { + if (operation === 'add') { + + // ---------------------------------- + // taskTag:add + // ---------------------------------- + + const taskId = this.getNodeParameter('id', i) as string; + + requestMethod = 'POST'; + + endpoint = `/tasks/${taskId}/addTag`; + + body.tag = this.getNodeParameter('tag', i) as string; + + responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = { success: true }; + } + + if (operation === 'remove') { + + // ---------------------------------- + // taskTag:remove + // ---------------------------------- + + const taskId = this.getNodeParameter('id', i) as string; + + requestMethod = 'POST'; + + endpoint = `/tasks/${taskId}/removeTag`; + + body.tag = this.getNodeParameter('tag', i) as string; + + responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = { success: true }; + } + } + + if (resource === 'user') { if (operation === 'get') { // ---------------------------------- // get @@ -554,7 +1384,10 @@ export class Asana implements INodeType { const userId = this.getNodeParameter('userId', i) as string; requestMethod = 'GET'; - endpoint = `users/${userId}`; + endpoint = `/users/${userId}`; + + responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body, qs); + responseData = responseData.data; } else if (operation === 'getAll') { // ---------------------------------- @@ -564,17 +1397,72 @@ export class Asana implements INodeType { const workspaceId = this.getNodeParameter('workspace', i) as string; requestMethod = 'GET'; - endpoint = `workspaces/${workspaceId}/users`; + endpoint = `/workspaces/${workspaceId}/users`; + + responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body, qs); + responseData = responseData.data; - } else { - throw new Error(`The operation "${operation}" is not known!`); } - } else { - throw new Error(`The resource "${resource}" is not known!`); } - const responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body); + if (resource === 'project') { - returnData.push(responseData.data as IDataObject); + if (operation === 'get') { + // ---------------------------------- + // project:get + // ---------------------------------- + const projectId = this.getNodeParameter('id', i) as string; + + requestMethod = 'GET'; + + endpoint = `/projects/${projectId}`; + + responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.data; + + } + + if (operation === 'getAll') { + // ---------------------------------- + // project:getAll + // ---------------------------------- + const workspaceId = this.getNodeParameter('workspace', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + requestMethod = 'GET'; + endpoint = `/projects`; + + if (additionalFields.team) { + qs.team = additionalFields.team; + } else { + qs.workspace = workspaceId; + } + + if (additionalFields.archived) { + qs.archived = additionalFields.archived as boolean; + } + + if (returnAll) { + + responseData = await asanaApiRequestAllItems.call(this, requestMethod, endpoint, body, qs); + + } else { + + qs.limit = this.getNodeParameter('limit', i) as boolean; + + responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.data; + } + } + } + + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData); + } } return [this.helpers.returnJsonArray(returnData)]; diff --git a/packages/nodes-base/nodes/Asana/GenericFunctions.ts b/packages/nodes-base/nodes/Asana/GenericFunctions.ts index 83bdfe01a2..c8a0beb1b4 100644 --- a/packages/nodes-base/nodes/Asana/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Asana/GenericFunctions.ts @@ -4,8 +4,17 @@ import { ILoadOptionsFunctions, } from 'n8n-core'; -import { OptionsWithUri } from 'request'; +import { + OptionsWithUri, +} from 'request'; +import { + IDataObject, +} from 'n8n-workflow'; + +import { + get, +} from 'lodash'; /** * Make an API request to Asana @@ -16,7 +25,7 @@ import { OptionsWithUri } from 'request'; * @param {object} body * @returns {Promise} */ -export async function asanaApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, query?: object): Promise { // tslint:disable-line:no-any +export async function asanaApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, query?: object, uri?: string | undefined): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('asanaApi'); if (credentials === undefined) { @@ -30,7 +39,7 @@ export async function asanaApiRequest(this: IHookFunctions | IExecuteFunctions | method, body: { data: body }, qs: query, - uri: `https://app.asana.com/api/1.0/${endpoint}`, + uri: uri || `https://app.asana.com/api/1.0${endpoint}`, json: true, }; @@ -54,3 +63,22 @@ export async function asanaApiRequest(this: IHookFunctions | IExecuteFunctions | throw error; } } + +export async function asanaApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + let uri: string | undefined; + query.limit = 100; + + do { + responseData = await asanaApiRequest.call(this, method, endpoint, body, query, uri); + uri = get(responseData, 'next_page.uri'); + returnData.push.apply(returnData, responseData['data']); + } while ( + responseData['next_page'] !== null + ); + + return returnData; +}