diff --git a/packages/nodes-base/credentials/ClickUpApi.credentials.ts b/packages/nodes-base/credentials/ClickUpApi.credentials.ts new file mode 100644 index 0000000000..7003b784b3 --- /dev/null +++ b/packages/nodes-base/credentials/ClickUpApi.credentials.ts @@ -0,0 +1,17 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class ClickUpApi implements ICredentialType { + name = 'clickUpApi'; + displayName = 'ClickUp API'; + properties = [ + { + displayName: 'Access Token', + name: 'accessToken', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/ClickUp/ClickUp.node.ts b/packages/nodes-base/nodes/ClickUp/ClickUp.node.ts new file mode 100644 index 0000000000..20ee32b65d --- /dev/null +++ b/packages/nodes-base/nodes/ClickUp/ClickUp.node.ts @@ -0,0 +1,298 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; +import { + clickupApiRequest, +} from './GenericFunctions'; +import { + taskFields, + taskOperations +} from './TaskDescription'; +import { + ITask, + } from './TaskInterface'; + +export class ClickUp implements INodeType { + description: INodeTypeDescription = { + displayName: 'ClickUp', + name: 'clickUp', + icon: 'file:clickup.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"] + ":" + $parameter["resource"]}}', + description: 'Consume ClickUp API (Beta)', + defaults: { + name: 'ClickUp', + color: '#7B68EE', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'clickUpApi', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Task', + value: 'task', + }, + ], + default: 'task', + description: 'Resource to consume.', + }, + ...taskOperations, + ...taskFields, + ], + }; + + methods = { + loadOptions: { + // Get all the available teams to display them to user so that he can + // select them easily + async getTeams(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const { teams } = await clickupApiRequest.call(this, 'GET', '/team'); + for (const team of teams) { + const teamName = team.name; + const teamId = team.id; + returnData.push({ + name: teamName, + value: teamId, + }); + } + return returnData; + }, + // Get all the available spaces to display them to user so that he can + // select them easily + async getSpaces(this: ILoadOptionsFunctions): Promise { + const teamId = this.getCurrentNodeParameter('team') as string; + const returnData: INodePropertyOptions[] = []; + const { spaces } = await clickupApiRequest.call(this, 'GET', `/team/${teamId}/space`); + for (const space of spaces) { + const spaceName = space.name; + const spaceId = space.id; + returnData.push({ + name: spaceName, + value: spaceId, + }); + } + return returnData; + }, + // Get all the available folders to display them to user so that he can + // select them easily + async getFolders(this: ILoadOptionsFunctions): Promise { + const spaceId = this.getCurrentNodeParameter('space') as string; + const returnData: INodePropertyOptions[] = []; + const { folders } = await clickupApiRequest.call(this, 'GET', `/space/${spaceId}/folder`); + for (const folder of folders) { + const folderName = folder.name; + const folderId = folder.id; + returnData.push({ + name: folderName, + value: folderId, + }); + } + return returnData; + }, + // Get all the available lists to display them to user so that he can + // select them easily + async getLists(this: ILoadOptionsFunctions): Promise { + const folderId = this.getCurrentNodeParameter('folder') as string; + const returnData: INodePropertyOptions[] = []; + const { lists } = await clickupApiRequest.call(this, 'GET', `/folder/${folderId}/list`); + for (const list of lists) { + const listName = list.name; + const listId = list.id; + returnData.push({ + name: listName, + value: listId, + }); + } + return returnData; + }, + // Get all the available assignees to display them to user so that he can + // select them easily + async getAssignees(this: ILoadOptionsFunctions): Promise { + const listId = this.getCurrentNodeParameter('list') as string; + const returnData: INodePropertyOptions[] = []; + const { members } = await clickupApiRequest.call(this, 'GET', `/list/${listId}/member`); + for (const member of members) { + const memberName = member.username; + const menberId = member.id; + returnData.push({ + name: memberName, + value: menberId, + }); + } + return returnData; + }, + // Get all the available tags to display them to user so that he can + // select them easily + async getTags(this: ILoadOptionsFunctions): Promise { + const spaceId = this.getCurrentNodeParameter('space') as string; + const returnData: INodePropertyOptions[] = []; + const { tags } = await clickupApiRequest.call(this, 'GET', `/space/${spaceId}/tag`); + for (const tag of tags) { + const tagName = tag.name; + const tagId = tag.id; + returnData.push({ + name: tagName, + value: tagId, + }); + } + return returnData; + }, + // Get all the available tags to display them to user so that he can + // select them easily + async getStatuses(this: ILoadOptionsFunctions): Promise { + const listId = this.getCurrentNodeParameter('list') as string; + const returnData: INodePropertyOptions[] = []; + const { statuses } = await clickupApiRequest.call(this, 'GET', `/list/${listId}`); + for (const status of statuses) { + const statusName = status.status; + const statusId = status.status; + returnData.push({ + name: statusName, + value: statusId, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length as unknown as number; + const qs: IDataObject = {}; + let responseData; + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + for (let i = 0; i < length; i++) { + if (resource === 'task') { + if (operation === 'create') { + const listId = this.getNodeParameter('list', i) as string; + const name = this.getNodeParameter('name', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: ITask = { + name, + }; + if (additionalFields.content) { + body.content = additionalFields.content as string; + } + if (additionalFields.assignees) { + body.assignees = additionalFields.assignees as string[]; + } + if (additionalFields.tags) { + body.tags = additionalFields.tags as string[]; + } + if (additionalFields.status) { + body.status = additionalFields.status as string; + } + if (additionalFields.priority) { + body.priority = additionalFields.priority as number; + } + if (additionalFields.dueDate) { + body.due_date = new Date(additionalFields.dueDate as string).getTime(); + } + if (additionalFields.dueDateTime) { + body.due_date_time = additionalFields.dueDateTime as boolean; + } + if (additionalFields.timeEstimate) { + body.time_estimate = (additionalFields.timeEstimate as number) * 6000; + } + if (additionalFields.startDate) { + body.start_date = new Date(additionalFields.startDate as string).getTime(); + } + if (additionalFields.startDateTime) { + body.start_date_time = additionalFields.startDateTime as boolean; + } + if (additionalFields.notifyAll) { + body.notify_all = additionalFields.notifyAll as boolean; + } + if (additionalFields.parentId) { + body.parent = additionalFields.parentId as string; + } + if (additionalFields.markdownContent) { + delete body.content; + body.markdown_content = additionalFields.content as string; + } + + responseData = await clickupApiRequest.call(this, 'POST', `/list/${listId}/task`, body); + } + if (operation === 'update') { + const taskId = this.getNodeParameter('id', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const body: ITask = {}; + if (updateFields.content) { + body.content = updateFields.content as string; + } + if (updateFields.priority) { + body.priority = updateFields.priority as number; + } + if (updateFields.dueDate) { + body.due_date = new Date(updateFields.dueDate as string).getTime(); + } + if (updateFields.dueDateTime) { + body.due_date_time = updateFields.dueDateTime as boolean; + } + if (updateFields.timeEstimate) { + body.time_estimate = (updateFields.timeEstimate as number) * 6000; + } + if (updateFields.startDate) { + body.start_date = new Date(updateFields.startDate as string).getTime(); + } + if (updateFields.startDateTime) { + body.start_date_time = updateFields.startDateTime as boolean; + } + if (updateFields.notifyAll) { + body.notify_all = updateFields.notifyAll as boolean; + } + if (updateFields.name) { + body.name = updateFields.name as string; + } + if (updateFields.parentId) { + body.parent = updateFields.parentId as string; + } + if (updateFields.markdownContent) { + delete body.content; + body.markdown_content = updateFields.content as string; + } + responseData = await clickupApiRequest.call(this, 'PUT', `/task/${taskId}`, body); + } + if (operation === 'get') { + const taskId = this.getNodeParameter('id', i) as string; + responseData = await clickupApiRequest.call(this, 'GET', `/task/${taskId}`); + } + if (operation === 'delete') { + const taskId = this.getNodeParameter('id', i) as string; + responseData = await clickupApiRequest.call(this, 'DELETE', `/task/${taskId}`, {}); + } + } + 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/nodes/ClickUp/ClickUpTrigger.node.ts b/packages/nodes-base/nodes/ClickUp/ClickUpTrigger.node.ts new file mode 100644 index 0000000000..36d523313e --- /dev/null +++ b/packages/nodes-base/nodes/ClickUp/ClickUpTrigger.node.ts @@ -0,0 +1,317 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodePropertyOptions, + INodeType, + INodeTypeDescription, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + clickupApiRequest, +} from './GenericFunctions'; + +import { createHmac } from 'crypto'; + +export class ClickUpTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'ClickUp Trigger', + name: 'clickUpTrigger', + icon: 'file:clickup.png', + group: ['trigger'], + version: 1, + description: 'Handle ClickUp events via webhooks (Beta)', + defaults: { + name: 'ClickUp Trigger', + color: '#7B68EE', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'clickUpApi', + required: true, + } + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Team', + name: 'team', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTeams', + }, + required: true, + default: '', + }, + { + displayName: 'Events', + name: 'events', + type: 'multiOptions', + required: true, + default: [], + options: [ + { + name: '*', + value: '*', + }, + { + name: 'folder.created', + value: 'folderCreated', + }, + { + name: 'folder.deleted', + value: 'folderDeleted', + }, + { + name: 'folder.updated', + value: 'folderUpdated', + }, + { + name: 'goal.created', + value: 'goalCreated', + }, + { + name: 'goal.updated', + value: 'goalUpdated', + }, + { + name: 'goal.deleted', + value: 'goalDeleted', + }, + { + name: 'keyResult.created', + value: 'keyResultCreated', + }, + { + name: 'keyResult.deleted', + value: 'keyResultDelete', + }, + { + name: 'keyResult.updated', + value: 'keyResultUpdated', + }, + { + name: 'list.created', + value: 'listCreated', + }, + { + name: 'list.deleted', + value: 'listDeleted', + }, + { + name: 'list.updated', + value: 'listUpdated', + }, + { + name: 'space.created', + value: 'spaceCreated', + }, + { + name: 'space.deleted', + value: 'spaceDeleted', + }, + { + name: 'space.updated', + value: 'spaceUpdated', + }, + { + name: 'task.assignee.updated', + value: 'taskAssigneeUpdated', + }, + { + name: 'task.comment.posted', + value: 'taskCommentPosted', + }, + { + name: 'task.comment.updated', + value: 'taskCommentUpdated', + }, + { + name: 'task.created', + value: 'taskCreated', + }, + { + name: 'task.deleted', + value: 'taskDeleted', + }, + { + name: 'task.dueDate.updated', + value: 'taskDueDateUpdated', + }, + { + name: 'task.moved', + value: 'taskMoved', + }, + { + name: 'task.status.updated', + value: 'taskStatusUpdated', + }, + { + name: 'task.tag.updated', + value: 'taskTagUpdated', + }, + { + name: 'task.timeEstimate.updated', + value: 'taskTimeEstimateUpdated', + }, + { + name: 'task.timeTracked.updated', + value: 'taskTimeTrackedUpdated', + }, + { + name: 'task.updated', + value: 'taskUpdated', + }, + ], + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Folder ID', + name: 'folderId', + type: 'string', + default: '', + }, + { + displayName: 'List ID', + name: 'listId', + type: 'string', + default: '', + }, + { + displayName: 'Space ID', + name: 'spaceId', + type: 'string', + default: '', + }, + { + displayName: 'Task ID', + name: 'taskId', + type: 'string', + default: '', + }, + ], + }, + ], + }; + + methods = { + loadOptions: { + // Get all the available teams to display them to user so that he can + // select them easily + async getTeams(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const { teams } = await clickupApiRequest.call(this, 'GET', '/team'); + for (const team of teams) { + const teamName = team.name; + const teamId = team.id; + returnData.push({ + name: teamName, + value: teamId, + }); + } + return returnData; + }, + }, + }; + // @ts-ignore + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const teamId = this.getNodeParameter('team') as string; + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId === undefined) { + return false; + } + const endpoint = `/team/${teamId}/webhook`; + const { webhooks } = await clickupApiRequest.call(this, 'GET', endpoint); + if (Array.isArray(webhooks)) { + for (const webhook of webhooks) { + if (webhook.id === webhookData.webhookId) { + return true; + } + } + } + return false; + }, + async create(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const filters = this.getNodeParameter('filters') as IDataObject; + const teamId = this.getNodeParameter('team') as string; + const events = this.getNodeParameter('events') as string[]; + const endpoint = `/team/${teamId}/webhook`; + const body: IDataObject = { + endpoint: webhookUrl, + events, + }; + if (events.includes('*')) { + body.events = '*'; + } + if (filters.listId) { + body.list_id = (filters.listId as string).replace('#',''); + } + if (filters.taskId) { + body.task_id = (filters.taskId as string).replace('#',''); + } + if (filters.spaceId) { + body.space_id = (filters.spaceId as string).replace('#',''); + } + if (filters.folderId) { + body.folder_id = (filters.folderId as string).replace('#',''); + } + const { webhook } = await clickupApiRequest.call(this, 'POST', endpoint, body); + webhookData.webhookId = webhook.id; + webhookData.secret = webhook.secret; + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const endpoint = `/webhook/${webhookData.webhookId}`; + try { + await clickupApiRequest.call(this, 'DELETE', endpoint); + } catch(error) { + return false; + } + delete webhookData.webhookId; + delete webhookData.secret; + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const headerData = this.getHeaderData() as IDataObject; + const req = this.getRequestObject(); + const computedSignature = createHmac('sha256', webhookData.secret as string).update(JSON.stringify(req.body)).digest('hex'); + if (headerData['x-signature'] !== computedSignature) { + // Signature is not valid so ignore call + return {}; + } + return { + workflowData: [ + this.helpers.returnJsonArray(req.body), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/ClickUp/GenericFunctions.ts b/packages/nodes-base/nodes/ClickUp/GenericFunctions.ts new file mode 100644 index 0000000000..08375c5317 --- /dev/null +++ b/packages/nodes-base/nodes/ClickUp/GenericFunctions.ts @@ -0,0 +1,37 @@ +import { OptionsWithUri } from 'request'; +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IWebhookFunctions, +} from 'n8n-core'; +import { IDataObject } from 'n8n-workflow'; + +export async function clickupApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('clickUpApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const options: OptionsWithUri = { + headers: { + Authorization: credentials.accessToken, + 'Content-Type': 'application/json', + }, + method, + qs, + body, + uri: uri ||`https://api.clickup.com/api/v2${resource}`, + json: true + }; + try { + return await this.helpers.request!(options); + } catch (error) { + let errorMessage = error; + if (error.err) { + errorMessage = error.err; + } + throw new Error('ClickUp Error: ' + errorMessage); + } +} diff --git a/packages/nodes-base/nodes/ClickUp/TaskDescription.ts b/packages/nodes-base/nodes/ClickUp/TaskDescription.ts new file mode 100644 index 0000000000..b3883040c3 --- /dev/null +++ b/packages/nodes-base/nodes/ClickUp/TaskDescription.ts @@ -0,0 +1,424 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const taskOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'task', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a task', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a task', + }, + { + name: 'Get', + value: 'get', + description: 'Get a task', + }, + { + name: 'Update', + value: 'update', + description: 'Update a task', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const taskFields = [ + +/* -------------------------------------------------------------------------- */ +/* task:create */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Team', + name: 'team', + type: 'options', + default: '', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create', + ], + }, + }, + typeOptions: { + loadOptionsMethod: 'getTeams', + }, + required: true, + }, + { + displayName: 'Space', + name: 'space', + type: 'options', + default: '', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create', + ], + }, + }, + typeOptions: { + loadOptionsMethod: 'getSpaces', + loadOptionsDependsOn: [ + 'team', + ] + }, + required: true, + }, + { + displayName: 'Folder', + name: 'folder', + type: 'options', + default: '', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create', + ], + }, + }, + typeOptions: { + loadOptionsMethod: 'getFolders', + loadOptionsDependsOn: [ + 'space', + ] + }, + required: true, + }, + { + displayName: 'List', + name: 'list', + type: 'options', + default: '', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create', + ], + }, + }, + typeOptions: { + loadOptionsMethod: 'getLists', + loadOptionsDependsOn: [ + 'folder', + ] + }, + required: true, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create', + ], + }, + }, + required: true, + description: 'The first name on the task', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Assignees', + name: 'assignees', + type: 'multiOptions', + loadOptionsDependsOn: [ + 'list', + ], + typeOptions: { + loadOptionsMethod: 'getAssignees', + }, + + default: [], + }, + { + displayName: 'Content', + name: 'content', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + }, + { + displayName: 'Due Date', + name: 'dueDate', + type: 'dateTime', + default: '', + }, + { + displayName: 'Due Date Time', + name: 'dueDateTime', + type: 'boolean', + default: false, + }, + { + displayName: 'Is Markdown Content', + name: 'markdownContent', + type: 'boolean', + default: false, + }, + { + displayName: 'Notify All', + name: 'notifyAll', + type: 'boolean', + default: false, + }, + { + displayName: 'Parent ID', + name: 'parentId', + type: 'string', + default: '', + }, + { + displayName: 'Priority', + name: 'priority', + type: 'number', + typeOptions: { + maxValue: 4, + }, + description: 'Integer mapping as 1 : Urgent, 2 : High, 3 : Normal, 4 : Low', + default: 3, + }, + { + displayName: 'Start Date Time', + name: 'startDateTime', + type: 'boolean', + default: false, + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + loadOptionsDependsOn: [ + 'list', + ], + typeOptions: { + loadOptionsMethod: 'getStatuses', + }, + default: '', + }, + { + displayName: 'Tags', + name: 'tags', + type: 'multiOptions', + loadOptionsDependsOn: [ + 'space', + ], + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: [], + description: 'The array of tags applied to this task', + }, + { + displayName: 'Time Estimate', + name: 'timeEstimate', + type: 'number', + description: 'time estimate in minutes', + default: 1, + }, + ], + }, +/* -------------------------------------------------------------------------- */ +/* task:update */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Task ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'update', + ], + }, + }, + description: 'Task ID', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Content', + name: 'content', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + }, + { + displayName: 'Due Date', + name: 'dueDate', + type: 'dateTime', + default: '', + }, + { + displayName: 'Due Date Time', + name: 'dueDateTime', + type: 'boolean', + default: false, + }, + { + displayName: 'Is Markdown Content', + name: 'markdownContent', + type: 'boolean', + default: false, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + { + displayName: 'Notify All', + name: 'notifyAll', + type: 'boolean', + default: false, + }, + { + displayName: 'Parent ID', + name: 'parentId', + type: 'string', + default: '', + }, + { + displayName: 'Priority', + name: 'priority', + type: 'number', + typeOptions: { + maxValue: 4, + }, + description: 'Integer mapping as 1 : Urgent, 2 : High, 3 : Normal, 4 : Low', + default: 3, + }, + { + displayName: 'Start Date Time', + name: 'startDateTime', + type: 'boolean', + default: false, + }, + { + displayName: 'Time Estimate', + name: 'timeEstimate', + type: 'number', + description: 'time estimate in minutes', + default: 1, + }, + ], + + }, +/* -------------------------------------------------------------------------- */ +/* task:get */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'Task ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'get', + ], + }, + }, + description: 'Task ID', + }, +/* -------------------------------------------------------------------------- */ +/* task:delete */ +/* -------------------------------------------------------------------------- */ + { + displayName: 'ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'task ID', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ClickUp/TaskInterface.ts b/packages/nodes-base/nodes/ClickUp/TaskInterface.ts new file mode 100644 index 0000000000..70a3899e15 --- /dev/null +++ b/packages/nodes-base/nodes/ClickUp/TaskInterface.ts @@ -0,0 +1,16 @@ +export interface ITask { + name?: string; + content?: string; + assignees?: string[]; + tags?: string[]; + status?: string; + priority?: number; + due_date?: number; + due_date_time?: boolean; + time_estimate?: number; + start_date?: number; + start_date_time?: boolean; + markdown_content?: string; + notify_all?: boolean; + parent?: string; +} diff --git a/packages/nodes-base/nodes/ClickUp/clickup.png b/packages/nodes-base/nodes/ClickUp/clickup.png new file mode 100755 index 0000000000..905deb1c37 Binary files /dev/null and b/packages/nodes-base/nodes/ClickUp/clickup.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index e72da6de67..3b2dea916b 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -34,6 +34,7 @@ "dist/credentials/Aws.credentials.js", "dist/credentials/BitbucketApi.credentials.js", "dist/credentials/ChargebeeApi.credentials.js", + "dist/credentials/ClickUpApi.credentials.js", "dist/credentials/CodaApi.credentials.js", "dist/credentials/CopperApi.credentials.js", "dist/credentials/DropboxApi.credentials.js", @@ -97,6 +98,8 @@ "dist/nodes/Bitbucket/BitbucketTrigger.node.js", "dist/nodes/Chargebee/Chargebee.node.js", "dist/nodes/Chargebee/ChargebeeTrigger.node.js", + "dist/nodes/ClickUp/ClickUp.node.js", + "dist/nodes/ClickUp/ClickUpTrigger.node.js", "dist/nodes/Coda/Coda.node.js", "dist/nodes/Copper/CopperTrigger.node.js", "dist/nodes/Cron.node.js",