From 9de91cae70d4175a1f21327d29fcfc5984779397 Mon Sep 17 00:00:00 2001 From: ricardo Date: Tue, 22 Dec 2020 17:44:31 -0500 Subject: [PATCH 1/3] :zap: Add task:getAll and subtask:create --- packages/nodes-base/nodes/Asana/Asana.node.ts | 363 ++++++++++++++++++ 1 file changed, 363 insertions(+) diff --git a/packages/nodes-base/nodes/Asana/Asana.node.ts b/packages/nodes-base/nodes/Asana/Asana.node.ts index f308cba65b..aa0ac83d56 100644 --- a/packages/nodes-base/nodes/Asana/Asana.node.ts +++ b/packages/nodes-base/nodes/Asana/Asana.node.ts @@ -17,6 +17,8 @@ import { getWorkspaces, } from './GenericFunctions'; +import * as moment from 'moment-timezone'; + export class Asana implements INodeType { description: INodeTypeDescription = { displayName: 'Asana', @@ -83,6 +85,10 @@ export class Asana implements INodeType { name: 'Project', value: 'project', }, + { + name: 'Subtask', + value: 'subtask', + }, { name: 'Task', value: 'task', @@ -103,6 +109,166 @@ export class Asana implements INodeType { default: 'task', description: 'The resource to operate on.', }, + // ---------------------------------- + // subtask + // ---------------------------------- + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'subtask', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a subtask', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, + + // ---------------------------------- + // subtask:create + // ---------------------------------- + { + displayName: 'Parent Task ID', + name: 'taskId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'subtask', + ], + }, + }, + description: 'The task to operate on.', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'subtask', + ], + }, + }, + description: 'The name of the subtask to create', + }, + { + displayName: 'Additional Fields', + name: 'otherProperties', + type: 'collection', + displayOptions: { + show: { + resource: [ + 'subtask', + ], + operation: [ + 'create', + ], + }, + }, + default: {}, + placeholder: 'Add Field', + options: [ + { + displayName: 'Assignee', + name: 'assignee', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'Set Assignee on the subtask', + }, + { + 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 subtask (requires Assignee)', + }, + { + displayName: 'Completed', + name: 'completed', + type: 'boolean', + default: false, + description: 'If the subtask should be marked completed.', + }, + { + displayName: 'Due On', + name: 'due_on', + type: 'dateTime', + default: '', + description: 'Date on which the time is due.', + }, + { + displayName: 'Liked', + name: 'liked', + type: 'boolean', + 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', + }, + { + displayName: 'Workspace', + name: 'workspace', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkspaces', + }, + default: '', + description: 'The workspace to create the subtask in', + }, + ], + }, // ---------------------------------- // task @@ -134,6 +300,11 @@ export class Asana implements INodeType { value: 'get', description: 'Get a task', }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all tasks', + }, { name: 'Move', value: 'move', @@ -241,6 +412,138 @@ export class Asana implements INodeType { }, description: 'The ID of the task to get the data of.', }, + // ---------------------------------- + // task:getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'task', + ], + }, + }, + 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: [ + 'task', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'task', + ], + }, + }, + default: {}, + description: 'Properties to search for', + placeholder: 'Add Filter', + options: [ + { + displayName: 'Assignee', + name: 'assignee', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'The assignee to filter tasks on. Note: If you specify assignee, you must also specify the workspace to filter on.', + }, + { + displayName: 'Fields', + name: 'opt_fields', + type: 'string', + default: '', + description: 'Defines fields to return. Multiple can be set separated by comma.', + }, + { + displayName: 'Pretty', + name: 'opt_pretty', + type: 'boolean', + default: false, + description: 'Provides “pretty” output.', + }, + { + displayName: 'Project', + name: 'project', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getProjects', + }, + default: '', + description: 'The project to filter tasks on.', + }, + { + displayName: 'Section', + name: 'section', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getSections', + }, + default: '', + description: 'The section to filter tasks on.', + }, + { + displayName: 'workspace', + name: 'workspace', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkspaces', + }, + default: '', + description: 'The workspace to filter tasks on. Note: If you specify workspace, you must also specify the assignee to filter on.', + }, + { + displayName: 'Completed Since', + name: 'completed_since', + type: 'dateTime', + default: '', + description: 'Only return tasks that are either incomplete or that have been completed since this time.', + }, + { + displayName: 'Modified Since', + name: 'modified_since', + type: 'dateTime', + default: '', + description: 'Only return tasks that have been modified since the given time.', + }, + ], + }, // ---------------------------------- // task:move @@ -1225,6 +1528,7 @@ export class Asana implements INodeType { async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: IDataObject[] = []; + const timezone = this.getTimezone(); const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; @@ -1240,6 +1544,27 @@ export class Asana implements INodeType { body = {}; qs = {}; + if (resource === 'subtask') { + if (operation === 'create') { + // ---------------------------------- + // subtask:create + // ---------------------------------- + + const taskId = this.getNodeParameter('taskId', i) as string; + + requestMethod = 'POST'; + endpoint = `/tasks/${taskId}/subtasks`; + + body.name = this.getNodeParameter('name', 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; + } + } if (resource === 'task') { if (operation === 'create') { // ---------------------------------- @@ -1285,7 +1610,45 @@ export class Asana implements INodeType { responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body, qs); responseData = responseData.data; + + } else if (operation === 'getAll') { + // ---------------------------------- + // task:getAll + // ---------------------------------- + const filters = this.getNodeParameter('filters', i) as IDataObject; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + requestMethod = 'GET'; + endpoint = `/tasks`; + + Object.assign(qs, filters); + + if (qs.modified_since) { + qs.modified_since = moment.tz(qs.modified_since as string, timezone).format(); + } + + if (qs.completed_since) { + qs.completed_since = moment.tz(qs.completed_since as string, timezone).format(); + } + + if (qs.fields) { + qs.fields = (qs.fields as string).split(','); + } + + 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; + } + } else if (operation === 'move') { // ---------------------------------- // task:move From c6ef0ea0792b3013d7323b94e6387e1046696ebe Mon Sep 17 00:00:00 2001 From: ricardo Date: Fri, 25 Dec 2020 14:41:48 -0500 Subject: [PATCH 2/3] :zap: Improvements --- packages/nodes-base/nodes/Asana/Asana.node.ts | 189 +++++++++++++++++- .../nodes/Asana/GenericFunctions.ts | 37 ++++ 2 files changed, 215 insertions(+), 11 deletions(-) diff --git a/packages/nodes-base/nodes/Asana/Asana.node.ts b/packages/nodes-base/nodes/Asana/Asana.node.ts index aa0ac83d56..81f669f35d 100644 --- a/packages/nodes-base/nodes/Asana/Asana.node.ts +++ b/packages/nodes-base/nodes/Asana/Asana.node.ts @@ -14,11 +14,16 @@ import { import { asanaApiRequest, asanaApiRequestAllItems, + getTaskFields, getWorkspaces, } from './GenericFunctions'; import * as moment from 'moment-timezone'; +import { + snakeCase, +} from 'change-case'; + export class Asana implements INodeType { description: INodeTypeDescription = { displayName: 'Asana', @@ -129,6 +134,11 @@ export class Asana implements INodeType { value: 'create', description: 'Create a subtask', }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all substasks', + }, ], default: 'create', description: 'The operation to perform.', @@ -269,7 +279,108 @@ export class Asana implements INodeType { }, ], }, - + // ---------------------------------- + // subtask:getAll + // ---------------------------------- + { + displayName: 'Parent Task ID', + name: 'taskId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'subtask', + ], + }, + }, + description: 'The task to operate on.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'subtask', + ], + }, + }, + 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: [ + 'subtask', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'subtask', + ], + }, + }, + default: {}, + placeholder: 'Add Field', + options: [ + { + displayName: 'Fields', + name: 'opt_fields', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTaskFields', + }, + default: [ + 'gid', + 'name', + 'resource_type', + ], + description: 'Defines fields to return.', + }, + { + displayName: 'Pretty', + name: 'opt_pretty', + type: 'boolean', + default: false, + description: 'Provides “pretty” output.', + }, + ], + }, // ---------------------------------- // task // ---------------------------------- @@ -487,9 +598,16 @@ export class Asana implements INodeType { { displayName: 'Fields', name: 'opt_fields', - type: 'string', - default: '', - description: 'Defines fields to return. Multiple can be set separated by comma.', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTaskFields', + }, + default: [ + 'gid', + 'name', + 'resource_type', + ], + description: 'Defines fields to return.', }, { displayName: 'Pretty', @@ -519,7 +637,7 @@ export class Asana implements INodeType { description: 'The section to filter tasks on.', }, { - displayName: 'workspace', + displayName: 'Workspace', name: 'workspace', type: 'options', typeOptions: { @@ -1522,6 +1640,18 @@ export class Asana implements INodeType { return returnData; }, + async getTaskFields(this: ILoadOptionsFunctions): Promise { + + const returnData: INodePropertyOptions[] = []; + for (const field of getTaskFields()) { + const value = snakeCase(field); + returnData.push({ + name: field, + value: (value === '') ? '*' : value, + }); + } + return returnData; + }, }, }; @@ -1564,6 +1694,40 @@ export class Asana implements INodeType { responseData = responseData.data; } + + if (operation === 'getAll') { + // ---------------------------------- + // subtask:getAll + // ---------------------------------- + const taskId = this.getNodeParameter('taskId', i) as string; + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const options = this.getNodeParameter('options', i) as IDataObject; + + requestMethod = 'GET'; + endpoint = `/tasks/${taskId}/subtasks`; + + Object.assign(qs, options); + + if (qs.opt_fields) { + const fields = qs.opt_fields as string[]; + if (fields.includes('*')) { + qs.opt_fields = getTaskFields().map((e) => snakeCase(e)).join(','); + } else { + qs.opt_fields = (qs.opt_fields as string[]).join(','); + } + } + + responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.data; + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as boolean; + responseData = responseData.splice(0, limit); + } + } } if (resource === 'task') { if (operation === 'create') { @@ -1624,6 +1788,15 @@ export class Asana implements INodeType { Object.assign(qs, filters); + if (qs.opt_fields) { + const fields = qs.opt_fields as string[]; + if (fields.includes('*')) { + qs.opt_fields = getTaskFields().map((e) => snakeCase(e)).join(','); + } else { + qs.opt_fields = (qs.opt_fields as string[]).join(','); + } + } + if (qs.modified_since) { qs.modified_since = moment.tz(qs.modified_since as string, timezone).format(); } @@ -1631,17 +1804,11 @@ export class Asana implements INodeType { if (qs.completed_since) { qs.completed_since = moment.tz(qs.completed_since as string, timezone).format(); } - - if (qs.fields) { - qs.fields = (qs.fields as string).split(','); - } 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); diff --git a/packages/nodes-base/nodes/Asana/GenericFunctions.ts b/packages/nodes-base/nodes/Asana/GenericFunctions.ts index 44a04123e6..db0b978a04 100644 --- a/packages/nodes-base/nodes/Asana/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Asana/GenericFunctions.ts @@ -121,3 +121,40 @@ export async function getWorkspaces(this: ILoadOptionsFunctions): Promise < INod return returnData; } + +export function getTaskFields() { + return [ + '*', + 'GID', + 'Resource Type', + 'name', + 'Approval Status', + 'Assignee Status', + 'Completed', + 'Completed At', + 'Completed By', + 'Created At', + 'Dependencies', + 'Dependents', + 'Due At', + 'Due On', + 'External', + 'HTML Notes', + 'Liked', + 'Likes', + 'Memberships', + 'Modified At', + 'Notes', + 'Num Likes', + 'Resource Subtype', + 'Start On', + 'Assignee', + 'Custom Fields', + 'Followers', + 'Parent', + 'Permalink URL', + 'Projects', + 'Tags', + 'Workspace', + ]; +} From ef53eddb6cb48ec1c9782d821501b29fb40fbc44 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 28 Dec 2020 08:48:57 +0100 Subject: [PATCH 3/3] :zap: Small improvements to Asana Node --- packages/nodes-base/nodes/Asana/Asana.node.ts | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/packages/nodes-base/nodes/Asana/Asana.node.ts b/packages/nodes-base/nodes/Asana/Asana.node.ts index 81f669f35d..92e6f75fa1 100644 --- a/packages/nodes-base/nodes/Asana/Asana.node.ts +++ b/packages/nodes-base/nodes/Asana/Asana.node.ts @@ -20,7 +20,7 @@ import { import * as moment from 'moment-timezone'; -import { +import { snakeCase, } from 'change-case'; @@ -1709,7 +1709,7 @@ export class Asana implements INodeType { endpoint = `/tasks/${taskId}/subtasks`; Object.assign(qs, options); - + if (qs.opt_fields) { const fields = qs.opt_fields as string[]; if (fields.includes('*')) { @@ -1774,48 +1774,48 @@ export class Asana implements INodeType { responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body, qs); responseData = responseData.data; - + } else if (operation === 'getAll') { - // ---------------------------------- - // task:getAll - // ---------------------------------- + // ---------------------------------- + // task:getAll + // ---------------------------------- - const filters = this.getNodeParameter('filters', i) as IDataObject; - const returnAll = this.getNodeParameter('returnAll', i) as boolean; - - requestMethod = 'GET'; - endpoint = `/tasks`; + const filters = this.getNodeParameter('filters', i) as IDataObject; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; - Object.assign(qs, filters); + requestMethod = 'GET'; + endpoint = `/tasks`; - if (qs.opt_fields) { - const fields = qs.opt_fields as string[]; - if (fields.includes('*')) { - qs.opt_fields = getTaskFields().map((e) => snakeCase(e)).join(','); - } else { - qs.opt_fields = (qs.opt_fields as string[]).join(','); - } - } + Object.assign(qs, filters); - if (qs.modified_since) { - qs.modified_since = moment.tz(qs.modified_since as string, timezone).format(); - } - - if (qs.completed_since) { - qs.completed_since = moment.tz(qs.completed_since as string, timezone).format(); - } - - if (returnAll) { - responseData = await asanaApiRequestAllItems.call(this, requestMethod, endpoint, body, qs); - + if (qs.opt_fields) { + const fields = qs.opt_fields as string[]; + if (fields.includes('*')) { + qs.opt_fields = getTaskFields().map((e) => snakeCase(e)).join(','); } else { - qs.limit = this.getNodeParameter('limit', i) as boolean; - - responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body, qs); - - responseData = responseData.data; + qs.opt_fields = (qs.opt_fields as string[]).join(','); } - + } + + if (qs.modified_since) { + qs.modified_since = moment.tz(qs.modified_since as string, timezone).format(); + } + + if (qs.completed_since) { + qs.completed_since = moment.tz(qs.completed_since as string, timezone).format(); + } + + 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; + } + } else if (operation === 'move') { // ---------------------------------- // task:move