🔀 Merge branch 'RicardoE105-feature/click-up-trigger'

This commit is contained in:
Jan Oberhauser 2020-01-18 20:37:26 -06:00
commit a8271be768
8 changed files with 1112 additions and 0 deletions

View file

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

View file

@ -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<INodePropertyOptions[]> {
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<INodePropertyOptions[]> {
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<INodePropertyOptions[]> {
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<INodePropertyOptions[]> {
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<INodePropertyOptions[]> {
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<INodePropertyOptions[]> {
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<INodePropertyOptions[]> {
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<INodeExecutionData[][]> {
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)];
}
}

View file

@ -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<INodePropertyOptions[]> {
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<boolean> {
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<boolean> {
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<boolean> {
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<IWebhookResponseData> {
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),
],
};
}
}

View file

@ -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<any> { // 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);
}
}

View file

@ -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[];

View file

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -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",