From 3a73493aebb206087e628307b4ba92831636236f Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sun, 4 Oct 2020 10:28:05 -0400 Subject: [PATCH] :sparkles: Add Clockify Node (#997) * Added pull.yml back after reset * Added Clockify Entry * Created ClockifyWriter * :zap: Improvements to #988 * :zap: Improvements * :zap: Improvements Co-authored-by: Ethan Sowell Co-authored-by: Mark Horninger --- .../nodes/ActiveCampaign/GenericFunctions.ts | 1 + .../nodes/Clockify/Clockify.node.ts | 593 ++++++++++++++++++ .../nodes/Clockify/ClockifyTrigger.node.ts | 9 +- .../nodes-base/nodes/Clockify/CommonDtos.ts | 13 + .../nodes/Clockify/GenericFunctions.ts | 57 +- .../nodes/Clockify/ProjectDescription.ts | 528 ++++++++++++++++ .../nodes/Clockify/ProjectInterfaces.ts | 56 ++ .../nodes/Clockify/TagDescription.ts | 242 +++++++ .../nodes/Clockify/TimeEntryDescription.ts | 384 ++++++++++++ .../nodes/Clockify/TimeEntryInterfaces.ts | 35 ++ .../nodes-base/nodes/Clockify/UserDtos.ts | 2 +- .../nodes/Clockify/WorkpaceInterfaces.ts | 6 + packages/nodes-base/package.json | 1 + 13 files changed, 1915 insertions(+), 12 deletions(-) create mode 100644 packages/nodes-base/nodes/Clockify/Clockify.node.ts create mode 100644 packages/nodes-base/nodes/Clockify/ProjectDescription.ts create mode 100644 packages/nodes-base/nodes/Clockify/ProjectInterfaces.ts create mode 100644 packages/nodes-base/nodes/Clockify/TagDescription.ts create mode 100644 packages/nodes-base/nodes/Clockify/TimeEntryDescription.ts create mode 100644 packages/nodes-base/nodes/Clockify/TimeEntryInterfaces.ts diff --git a/packages/nodes-base/nodes/ActiveCampaign/GenericFunctions.ts b/packages/nodes-base/nodes/ActiveCampaign/GenericFunctions.ts index 97f174065b..33728debc2 100644 --- a/packages/nodes-base/nodes/ActiveCampaign/GenericFunctions.ts +++ b/packages/nodes-base/nodes/ActiveCampaign/GenericFunctions.ts @@ -187,3 +187,4 @@ export function activeCampaignDefaultGetAllProperties(resource: string, operatio }, ]; } + diff --git a/packages/nodes-base/nodes/Clockify/Clockify.node.ts b/packages/nodes-base/nodes/Clockify/Clockify.node.ts new file mode 100644 index 0000000000..5dab674fa4 --- /dev/null +++ b/packages/nodes-base/nodes/Clockify/Clockify.node.ts @@ -0,0 +1,593 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, + IDataObject, +} from 'n8n-workflow'; + +import { + clockifyApiRequest, + clockifyApiRequestAllItems, +} from './GenericFunctions'; + +import { + IClientDto, + IWorkspaceDto, +} from './WorkpaceInterfaces'; + +import { + IUserDto, +} from './UserDtos'; + +import { + IProjectDto, +} from './ProjectInterfaces'; + +import { + projectOperations, + projectFields, +} from './ProjectDescription'; + +import { + tagOperations, + tagFields, +} from './TagDescription'; + +import { + timeEntryFields, + timeEntryOperations, +} from './TimeEntryDescription'; + +import * as moment from 'moment-timezone'; +import { boardColumnFields } from '../MondayCom/BoardColumnDescription'; + +export class Clockify implements INodeType { + description: INodeTypeDescription = { + displayName: 'Clockify', + name: 'clockify', + icon: 'file:clockify.png', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Clockify REST API', + defaults: { + name: 'Clockify', + color: '#000000', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'clockifyApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Project', + value: 'project' + }, + { + name: 'Tag', + value: 'tag' + }, + { + name: 'Time Entry', + value: 'timeEntry' + }, + ], + default: 'project', + description: 'The resource to operate on.', + }, + ...projectOperations, + ...tagOperations, + ...timeEntryOperations, + { + displayName: 'Workspace ID', + name: 'workspaceId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'listWorkspaces', + }, + required: true, + default: [], + }, + ...projectFields, + ...tagFields, + ...timeEntryFields, + ], + }; + + methods = { + loadOptions: { + async listWorkspaces(this: ILoadOptionsFunctions): Promise { + const rtv: INodePropertyOptions[] = []; + const workspaces: IWorkspaceDto[] = await clockifyApiRequest.call(this, 'GET', 'workspaces'); + if (undefined !== workspaces) { + workspaces.forEach(value => { + rtv.push( + { + name: value.name, + value: value.id, + }); + }); + } + return rtv; + }, + async loadUsersForWorkspace(this: ILoadOptionsFunctions): Promise { + const rtv: INodePropertyOptions[] = []; + const workspaceId = this.getCurrentNodeParameter('workspaceId'); + if (undefined !== workspaceId) { + const resource = `workspaces/${workspaceId}/users`; + const users: IUserDto[] = await clockifyApiRequest.call(this, 'GET', resource); + if (undefined !== users) { + users.forEach(value => { + rtv.push( + { + name: value.name, + value: value.id, + }); + }); + } + } + return rtv; + }, + async loadClientsForWorkspace(this: ILoadOptionsFunctions): Promise { + const rtv: INodePropertyOptions[] = []; + const workspaceId = this.getCurrentNodeParameter('workspaceId'); + if (undefined !== workspaceId) { + const resource = `workspaces/${workspaceId}/clients`; + const clients: IClientDto[] = await clockifyApiRequest.call(this, 'GET', resource); + if (undefined !== clients) { + clients.forEach(value => { + rtv.push( + { + name: value.name, + value: value.id, + }); + }); + } + } + return rtv; + }, + async loadProjectsForWorkspace(this: ILoadOptionsFunctions): Promise { + const rtv: INodePropertyOptions[] = []; + const workspaceId = this.getCurrentNodeParameter('workspaceId'); + if (undefined !== workspaceId) { + const resource = `workspaces/${workspaceId}/projects`; + const users: IProjectDto[] = await clockifyApiRequest.call(this, 'GET', resource); + if (undefined !== users) { + users.forEach(value => { + rtv.push( + { + name: value.name, + value: value.id, + }); + }); + } + } + return rtv; + }, + async loadTagsForWorkspace(this: ILoadOptionsFunctions): Promise { + const rtv: INodePropertyOptions[] = []; + const workspaceId = this.getCurrentNodeParameter('workspaceId'); + if (undefined !== workspaceId) { + const resource = `workspaces/${workspaceId}/tags`; + const users: IProjectDto[] = await clockifyApiRequest.call(this, 'GET', resource); + if (undefined !== users) { + users.forEach(value => { + rtv.push( + { + name: value.name, + value: value.id, + }); + }); + } + } + return rtv; + }, + async loadCustomFieldsForWorkspace(this: ILoadOptionsFunctions): Promise { + const rtv: INodePropertyOptions[] = []; + const workspaceId = this.getCurrentNodeParameter('workspaceId'); + if (undefined !== workspaceId) { + const resource = `workspaces/${workspaceId}/custom-fields`; + const customFields = await clockifyApiRequest.call(this, 'GET', resource); + for (const customField of customFields) { + rtv.push( + { + name: customField.name, + value: customField.id, + }); + } + } + return rtv; + }, + }, + }; + + 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 === 'project') { + + if (operation === 'create') { + + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + + const name = this.getNodeParameter('name', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + name, + }; + + Object.assign(body, additionalFields); + + if (body.estimateUi) { + + body.estimate = (body.estimateUi as IDataObject).estimateValues; + + delete body.estimateUi; + } + + responseData = await clockifyApiRequest.call( + this, + 'POST', + `/workspaces/${workspaceId}/projects`, + body, + qs + ); + } + + if (operation === 'delete') { + + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + + const projectId = this.getNodeParameter('projectId', i) as string; + + responseData = await clockifyApiRequest.call( + this, + 'DELETE', + `/workspaces/${workspaceId}/projects/${projectId}`, + {}, + qs + ); + + responseData = { success: true }; + } + + if (operation === 'get') { + + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + + const projectId = this.getNodeParameter('projectId', i) as string; + + responseData = await clockifyApiRequest.call( + this, + 'GET', + `/workspaces/${workspaceId}/projects/${projectId}`, + {}, + qs + ); + } + + if (operation === 'getAll') { + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + Object.assign(qs, additionalFields); + + if (returnAll) { + responseData = await clockifyApiRequestAllItems.call( + this, + 'GET', + `/workspaces/${workspaceId}/projects`, + {}, + qs + ); + + } else { + + qs.limit = this.getNodeParameter('limit', i) as number; + + responseData = await clockifyApiRequestAllItems.call( + this, + 'GET', + `/workspaces/${workspaceId}/projects`, + {}, + qs + ); + + responseData = responseData.splice(0, qs.limit); + } + } + + if (operation === 'update') { + + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + + const projectId = this.getNodeParameter('projectId', i) as string; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + const body: IDataObject = {}; + + Object.assign(body, updateFields); + + if (body.estimateUi) { + + body.estimate = (body.estimateUi as IDataObject).estimateValues; + + delete body.estimateUi; + } + + responseData = await clockifyApiRequest.call( + this, + 'PUT', + `/workspaces/${workspaceId}/projects/${projectId}`, + body, + qs + ); + + responseData = { success: true }; + } + } + + if (resource === 'tag') { + + if (operation === 'add') { + + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + + const name = this.getNodeParameter('name', i) as string; + + const body: IDataObject = { + name, + }; + + responseData = await clockifyApiRequest.call( + this, + 'POST', + `/workspaces/${workspaceId}/tags`, + body, + qs + ); + } + + if (operation === 'delete') { + + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + + const tagId = this.getNodeParameter('tagId', i) as string; + + responseData = await clockifyApiRequest.call( + this, + 'DELETE', + `/workspaces/${workspaceId}/tags/${tagId}`, + {}, + qs + ); + + responseData = { success: true }; + } + + if (operation === 'getAll') { + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + Object.assign(qs, additionalFields); + + if (returnAll) { + responseData = await clockifyApiRequestAllItems.call( + this, + 'GET', + `/workspaces/${workspaceId}/tags`, + {}, + qs + ); + + } else { + + qs.limit = this.getNodeParameter('limit', i) as number; + + responseData = await clockifyApiRequestAllItems.call( + this, + 'GET', + `/workspaces/${workspaceId}/tags`, + {}, + qs + ); + + responseData = responseData.splice(0, qs.limit); + } + } + + if (operation === 'update') { + + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + + const tagId = this.getNodeParameter('tagId', i) as string; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + const body: IDataObject = {}; + + Object.assign(body, updateFields); + + responseData = await clockifyApiRequest.call( + this, + 'PUT', + `/workspaces/${workspaceId}/tags/${tagId}`, + body, + qs + ); + + responseData = { success: true }; + } + } + + if (resource === 'timeEntry') { + + if (operation === 'create') { + + const timezone = this.getTimezone(); + + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + + const start = this.getNodeParameter('start', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + start: moment.tz(start, timezone).utc().format(), + }; + + Object.assign(body, additionalFields); + + if (body.end) { + body.end = moment.tz(body.end, timezone).utc().format(); + } + + if (body.customFieldsUi) { + + const customFields = (body.customFieldsUi as IDataObject).customFieldsValues as IDataObject[]; + + body.customFields = customFields; + } + + responseData = await clockifyApiRequest.call( + this, + 'POST', + `/workspaces/${workspaceId}/time-entries`, + body, + qs + ); + } + + if (operation === 'delete') { + + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + + const timeEntryId = this.getNodeParameter('timeEntryId', i) as string; + + responseData = await clockifyApiRequest.call( + this, + 'DELETE', + `/workspaces/${workspaceId}/time-entries/${timeEntryId}`, + {}, + qs + ); + + responseData = { success: true }; + } + + if (operation === 'get') { + + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + + const timeEntryId = this.getNodeParameter('timeEntryId', i) as string; + + responseData = await clockifyApiRequest.call( + this, + 'GET', + `/workspaces/${workspaceId}/time-entries/${timeEntryId}`, + {}, + qs + ); + } + + if (operation === 'update') { + + const timezone = this.getTimezone(); + + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + + const timeEntryId = this.getNodeParameter('timeEntryId', i) as string; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + const body: IDataObject = {}; + + Object.assign(body, updateFields); + + if (body.end) { + body.end = moment.tz(body.end, timezone).utc().format(); + } + + if (body.start) { + body.start = moment.tz(body.start, timezone).utc().format(); + } else { + // even if you do not want to update the start time, it always has to be set + // to make it more simple to the user, if he did not set a start time look for the current start time + // and set it + const { timeInterval: { start } } = await clockifyApiRequest.call( + this, + 'GET', + `/workspaces/${workspaceId}/time-entries/${timeEntryId}`, + {}, + qs + ); + + body.start = start; + } + + responseData = await clockifyApiRequest.call( + this, + 'PUT', + `/workspaces/${workspaceId}/time-entries/${timeEntryId}`, + body, + qs + ); + + responseData = { success: true }; + } + } + } + + if (Array.isArray(responseData)) { + + returnData.push.apply(returnData, responseData as IDataObject[]); + + } else if (responseData !== undefined) { + + returnData.push(responseData as IDataObject); + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts b/packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts index 783432fb3b..6ff55a26de 100644 --- a/packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts +++ b/packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts @@ -15,7 +15,7 @@ import { } from './GenericFunctions'; import { EntryTypeEnum } from './EntryTypeEnum'; -import { ICurrentUserDto } from './UserDtos'; +import { IUserDto } from './UserDtos'; import { IWorkspaceDto } from './WorkpaceInterfaces'; @@ -29,7 +29,7 @@ export class ClockifyTrigger implements INodeType { description: 'Watches Clockify For Events', defaults: { name: 'Clockify Trigger', - color: '#00FF00', + color: '#000000', }, inputs: [], outputs: ['main'], @@ -93,7 +93,7 @@ export class ClockifyTrigger implements INodeType { if (!webhookData.userId) { // Cache the user-id that we do not have to request it every time - const userInfo: ICurrentUserDto = await clockifyApiRequest.call(this, 'GET', 'user'); + const userInfo: IUserDto = await clockifyApiRequest.call(this, 'GET', 'user'); webhookData.userId = userInfo.id; } @@ -118,8 +118,7 @@ export class ClockifyTrigger implements INodeType { if (Array.isArray(result) && result.length !== 0) { result = [this.helpers.returnJsonArray(result)]; - return result; } - return null; + return result; } } diff --git a/packages/nodes-base/nodes/Clockify/CommonDtos.ts b/packages/nodes-base/nodes/Clockify/CommonDtos.ts index 2876fd4ffa..629cede9a2 100644 --- a/packages/nodes-base/nodes/Clockify/CommonDtos.ts +++ b/packages/nodes-base/nodes/Clockify/CommonDtos.ts @@ -17,3 +17,16 @@ export interface IMembershipDto { targetId: string; userId: string; } + +export interface ITagDto { + id: string; + name: any; + workspaceId: string; + archived: boolean; +} + +export interface ITimeIntervalDto { + duration: string; + end: string; + start: string; +} diff --git a/packages/nodes-base/nodes/Clockify/GenericFunctions.ts b/packages/nodes-base/nodes/Clockify/GenericFunctions.ts index 730fad23a3..b8288427c3 100644 --- a/packages/nodes-base/nodes/Clockify/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Clockify/GenericFunctions.ts @@ -1,15 +1,24 @@ -import { OptionsWithUri } from 'request'; import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, ILoadOptionsFunctions, - IPollFunctions + IPollFunctions, } from 'n8n-core'; -import { IDataObject } from 'n8n-workflow'; +import { + IDataObject, +} from 'n8n-workflow'; + +export async function clockifyApiRequest(this: ILoadOptionsFunctions | IPollFunctions | IExecuteFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any -export async function clockifyApiRequest(this: ILoadOptionsFunctions | IPollFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('clockifyApi'); + if (credentials === undefined) { throw new Error('No credentials got returned!'); + } const BASE_URL = 'https://api.clockify.me/api/v1'; @@ -22,17 +31,53 @@ export async function clockifyApiRequest(this: ILoadOptionsFunctions | IPollFunc qs, body, uri: `${BASE_URL}/${resource}`, - json: true + json: true, + useQuerystring: true, }; + try { + return await this.helpers.request!(options); + } catch (error) { let errorMessage = error.message; + if (error.response.body && error.response.body.message) { - errorMessage = `[${error.response.body.status_code}] ${error.response.body.message}`; + + errorMessage = `[${error.statusCode}] ${error.response.body.message}`; + } throw new Error('Clockify Error: ' + errorMessage); } } + +export async function clockifyApiRequestAllItems(this: IExecuteFunctions | IPollFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + query['page-size'] = 50; + + query.page = 1; + + do { + responseData = await clockifyApiRequest.call(this, method, endpoint, body, query); + + returnData.push.apply(returnData, responseData); + + if (query.limit && (returnData.length >= query.limit)) { + + return returnData; + } + + query.page++; + + } while ( + responseData.length !== 0 + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Clockify/ProjectDescription.ts b/packages/nodes-base/nodes/Clockify/ProjectDescription.ts new file mode 100644 index 0000000000..ec87a6d90f --- /dev/null +++ b/packages/nodes-base/nodes/Clockify/ProjectDescription.ts @@ -0,0 +1,528 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const projectOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'project', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a project', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a project', + }, + { + name: 'Get', + value: 'get', + description: 'Get a project', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all projects', + }, + { + name: 'Update', + value: 'update', + description: 'Update a project', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const projectFields = [ + + /* -------------------------------------------------------------------------- */ + /* project:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Project Name', + name: 'name', + type: 'string', + required: true, + default: '', + description: 'Name of project being created.', + displayOptions: { + show: { + resource: [ + 'project', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'project', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Billable', + name: 'billable', + type: 'boolean', + default: true, + }, + { + displayName: 'Color', + name: 'color', + type: 'color', + default: '#0000FF', + }, + { + displayName: 'Client ID', + name: 'clientId', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'workspaceId', + ], + loadOptionsMethod: 'loadClientsForWorkspace', + }, + default: '', + }, + { + displayName: 'Estimate', + name: 'estimateUi', + placeholder: 'Add Estimate', + type: 'fixedCollection', + default: {}, + typeOptions: { + multipleValues: false, + }, + options: [ + { + displayName: 'Estimate', + name: 'estimateValues', + values: [ + { + displayName: 'Estimate', + name: 'estimate', + type: 'number', + default: 0, + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Auto', + value: 'AUTO', + }, + { + name: 'Manual', + value: 'MANUAL', + }, + ], + default: 'AUTO', + }, + ], + }, + ], + }, + { + displayName: 'Is Public', + name: 'isPublic', + type: 'boolean', + default: true, + }, + { + displayName: 'Note', + name: 'note', + type: 'string', + default: '', + description: 'Note about the project', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* project:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Project ID', + name: 'projectId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'project', + ], + operation: [ + 'delete', + ], + }, + }, + }, + /* -------------------------------------------------------------------------- */ + /* project:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Project ID', + name: 'projectId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'project', + ], + operation: [ + 'get', + ], + }, + }, + }, + /* -------------------------------------------------------------------------- */ + /* project:getAll */ + /* -------------------------------------------------------------------------- */ + { + 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', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'project', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Archived', + name: 'archived', + type: 'boolean', + default: true, + }, + { + displayName: 'Billable', + name: 'billable', + type: 'boolean', + default: true, + }, + { + displayName: 'Client IDs', + name: 'clients', + type: 'multiOptions', + typeOptions: { + loadOptionsDependsOn: [ + 'workspaceId', + ], + loadOptionsMethod: 'loadClientsForWorkspace', + }, + default: [], + }, + { + displayName: 'Contains Client', + name: 'contains-client', + type: 'boolean', + default: false, + description: 'If provided, projects will be filtered by whether they have a client.; ' + }, + { + displayName: 'Client Status', + name: 'client-status', + type: 'options', + options: [ + { + name: 'Active', + value: 'ACTIVE', + }, + { + name: 'Archived', + value: 'ARCHIVED', + }, + ], + default: '', + description: 'If provided, projects will be filtered by whether they have a client.', + }, + { + displayName: 'Contains User', + name: 'contains-user', + type: 'boolean', + default: false, + description: 'If provided, projects will be filtered by whether they have users.', + }, + { + displayName: 'Is Template', + name: 'is-template', + type: 'boolean', + default: false, + description: 'If provided, projects will be filtered by whether they are used as a template.', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + { + displayName: 'Sort Column', + name: 'sort-column', + type: 'options', + options: [ + { + name: 'Name', + value: 'NAME', + }, + { + name: 'Client Name', + value: 'CLIENT_NAME', + }, + { + name: 'Duration', + value: 'DURATION', + }, + ], + default: '', + }, + { + displayName: 'Sort Order', + name: 'sort-order', + type: 'options', + options: [ + { + name: 'Ascending', + value: 'ASCENDING', + }, + { + name: 'Descending', + value: 'DESCENDING', + }, + ], + default: '', + }, + { + displayName: 'User IDs', + name: 'users', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'workspaceId', + ], + loadOptionsMethod: 'loadUsersForWorkspace', + }, + default: '', + }, + { + displayName: 'User Status', + name: 'user-status', + type: 'options', + options: [ + { + name: 'Active', + value: 'ACTIVE', + }, + { + name: 'Archived', + value: 'ARCHIVED', + }, + ], + default: '', + description: 'If provided, projects will be filtered by whether they have a client.', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* project:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Project ID', + name: 'projectId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'project', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'project', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Billable', + name: 'billable', + type: 'boolean', + default: true, + }, + { + displayName: 'Color', + name: 'color', + type: 'color', + default: '#0000FF', + }, + { + displayName: 'Client ID', + name: 'clientId', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'workspaceId', + ], + loadOptionsMethod: 'loadClientsForWorkspace', + }, + default: '', + }, + { + displayName: 'Estimate', + name: 'estimateUi', + placeholder: 'Add Estimate', + type: 'fixedCollection', + default: {}, + typeOptions: { + multipleValues: false, + }, + options: [ + { + displayName: 'Estimate', + name: 'estimateValues', + values: [ + { + displayName: 'Estimate', + name: 'estimate', + type: 'number', + default: 0, + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Auto', + value: 'AUTO', + }, + { + name: 'Manual', + value: 'MANUAL', + }, + ], + default: 'AUTO', + }, + ], + }, + ], + }, + { + displayName: 'Is Public', + name: 'isPublic', + type: 'boolean', + default: false, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + { + displayName: 'Note', + name: 'note', + type: 'string', + default: '', + description: 'Note about the project', + }, + ], + }, + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Clockify/ProjectInterfaces.ts b/packages/nodes-base/nodes/Clockify/ProjectInterfaces.ts new file mode 100644 index 0000000000..1bb6db6bae --- /dev/null +++ b/packages/nodes-base/nodes/Clockify/ProjectInterfaces.ts @@ -0,0 +1,56 @@ +import {IHourlyRateDto, IMembershipDto} from "./CommonDtos"; +import { INodeExecutionData } from "n8n-workflow"; + +enum EstimateEnum { + AUTO = "AUTO", + MANUAL = "MANUAL" +} + +interface IEstimateDto { + estimate: string; + type: EstimateEnum; +} + +export interface IProjectDto{ + archived: boolean; + billable: boolean; + clientId: string; + clientName: string | undefined; + color: string; + duration: string | undefined; + estimate: IEstimateDto | undefined; + hourlyRate: IHourlyRateDto | undefined; + id: string; + memberships: IMembershipDto[] | undefined; + name: string; + isPublic: boolean; + workspaceId: string; + note: string | undefined; +} + +export interface IProjectRequest { + name: string; + clientId: string; + isPublic: boolean; + estimate: IEstimateDto; + color: string; + note: string; + billable: boolean; + hourlyRate: IHourlyRateDto; + memberships: IMembershipDto; + tasks: ITaskDto; +} + +enum TaskStatusEnum { + ACTIVE = 'ACTIVE', + DONE = 'DONE' +} + +export interface ITaskDto { + assigneeIds: object; + estimate: string; + id: string; + name: string; + projectId: string; + status: TaskStatusEnum; +} diff --git a/packages/nodes-base/nodes/Clockify/TagDescription.ts b/packages/nodes-base/nodes/Clockify/TagDescription.ts new file mode 100644 index 0000000000..f3f0b212a7 --- /dev/null +++ b/packages/nodes-base/nodes/Clockify/TagDescription.ts @@ -0,0 +1,242 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const tagOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'tag', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add a tag', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a tag', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all tags', + }, + { + name: 'Update', + value: 'update', + description: 'Update a tag', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const tagFields = [ + + /* -------------------------------------------------------------------------- */ + /* tag:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + default: '', + description: 'Name of tag being created', + displayOptions: { + show: { + resource: [ + 'tag', + ], + operation: [ + 'add', + ], + }, + }, + }, + /* -------------------------------------------------------------------------- */ + /* tag:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Tag ID', + name: 'tagId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'tag', + ], + operation: [ + 'delete', + ], + }, + }, + }, + /* -------------------------------------------------------------------------- */ + /* tag:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'tag', + ], + }, + }, + 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: [ + 'tag', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'tag', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Archived', + name: 'archived', + type: 'boolean', + default: true, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + { + displayName: 'Sort Column', + name: 'sort-column', + type: 'options', + options: [ + { + name: 'Name', + value: 'NAME', + }, + ], + default: '', + }, + { + displayName: 'Sort Order', + name: 'sort-order', + type: 'options', + options: [ + { + name: 'Ascending', + value: 'ASCENDING', + }, + { + name: 'Descending', + value: 'DESCENDING', + }, + ], + default: '', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* tag:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Tag ID', + name: 'tagId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'tag', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'tag', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Archived', + name: 'archived', + type: 'boolean', + default: false, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Clockify/TimeEntryDescription.ts b/packages/nodes-base/nodes/Clockify/TimeEntryDescription.ts new file mode 100644 index 0000000000..2ce95efa59 --- /dev/null +++ b/packages/nodes-base/nodes/Clockify/TimeEntryDescription.ts @@ -0,0 +1,384 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const timeEntryOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'timeEntry', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a time entry', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a time entry', + }, + { + name: 'Get', + value: 'get', + description: 'Get time entrie', + }, + { + name: 'Update', + value: 'update', + description: 'Update a time entry', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const timeEntryFields = [ + + /* -------------------------------------------------------------------------- */ + /* timeEntry:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Start', + name: 'start', + type: 'dateTime', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'timeEntry', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'timeEntry', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Billable', + name: 'billable', + type: 'boolean', + default: false, + }, + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + placeholder: 'Add Custom Field', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + description: 'Filter by custom fields ', + default: {}, + options: [ + { + name: 'customFieldsValues', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field ID', + name: 'customFieldId', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'workspaceId', + ], + loadOptionsMethod: 'loadCustomFieldsForWorkspace', + }, + default: '', + description: 'The ID of the field to add custom field to.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'The value to set on custom field.', + }, + ], + }, + ], + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + }, + { + displayName: 'End', + name: 'end', + type: 'dateTime', + default: '', + }, + { + displayName: 'Project ID', + name: 'projectId', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'workspaceId', + ], + loadOptionsMethod: 'loadProjectsForWorkspace', + }, + default: '', + }, + { + displayName: 'Tag IDs', + name: 'tagIds', + type: 'multiOptions', + typeOptions: { + loadOptionsDependsOn: [ + 'workspaceId', + ], + loadOptionsMethod: 'loadTagsForWorkspace', + }, + default: [], + }, + { + displayName: 'Task ID', + name: 'taskId', + type: 'string', + default: '', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* timeEntry:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Time Entry ID', + name: 'timeEntryId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'timeEntry', + ], + operation: [ + 'delete', + ], + }, + }, + }, + /* -------------------------------------------------------------------------- */ + /* timeEntry:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Time Entry ID', + name: 'timeEntryId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'timeEntry', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'timeEntry', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Consider Duration Format', + name: 'consider-duration-format', + type: 'boolean', + default: false, + description: `If provided, returned timeentry's duration will be rounded to minutes or seconds based on duration format (hh:mm or hh:mm:ss) from workspace settings.`, + }, + { + displayName: 'Hydrated', + name: 'hydrated', + type: 'boolean', + default: false, + description: `If provided, returned timeentry's project,task and tags will be returned in full and not just their ids`, + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* timeEntry:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Time Entry ID', + name: 'timeEntryId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'timeEntry', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'timeEntry', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Billable', + name: 'billable', + type: 'boolean', + default: false, + }, + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + placeholder: 'Add Custom Field', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + description: 'Filter by custom fields ', + default: {}, + options: [ + { + name: 'customFieldsValues', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field ID', + name: 'customFieldId', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'workspaceId', + ], + loadOptionsMethod: 'loadCustomFieldsForWorkspace', + }, + default: '', + description: 'The ID of the field to add custom field to.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'The value to set on custom field.', + }, + ], + }, + ], + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + }, + { + displayName: 'End', + name: 'end', + type: 'dateTime', + default: '', + }, + { + displayName: 'Project ID', + name: 'projectId', + type: 'options', + typeOptions: { + loadOptionsDependsOn: [ + 'workspaceId', + ], + loadOptionsMethod: 'loadProjectsForWorkspace', + }, + default: '', + }, + { + displayName: 'Start', + name: 'start', + type: 'dateTime', + default: '', + }, + { + displayName: 'Tag IDs', + name: 'tagIds', + type: 'multiOptions', + typeOptions: { + loadOptionsDependsOn: [ + 'workspaceId', + ], + loadOptionsMethod: 'loadTagsForWorkspace', + }, + default: [], + }, + { + displayName: 'Task ID', + name: 'taskId', + type: 'string', + default: '', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Clockify/TimeEntryInterfaces.ts b/packages/nodes-base/nodes/Clockify/TimeEntryInterfaces.ts new file mode 100644 index 0000000000..9d1f39ce63 --- /dev/null +++ b/packages/nodes-base/nodes/Clockify/TimeEntryInterfaces.ts @@ -0,0 +1,35 @@ +import {ITimeIntervalDto} from "./CommonDtos"; + +interface ITimeEntriesDurationRequest { + start: string; + end: string; +} + +export interface ITimeEntryRequest { + id: string; + start: string; + billable: boolean; + description: string; + projectId: string; + userId: string; + taskId?: string | null; + end: string; + tagIds?: string[] | undefined; + timeInterval: ITimeEntriesDurationRequest; + workspaceId: string; + isLocked: boolean; +}; + +export interface ITimeEntryDto { + billable: boolean; + description: string; + id: string; + isLocked: boolean; + projectId: string; + tagIds: string[]; + taskId: string; + timeInterval: ITimeIntervalDto; + userId: string; + workspaceId: string; +} + diff --git a/packages/nodes-base/nodes/Clockify/UserDtos.ts b/packages/nodes-base/nodes/Clockify/UserDtos.ts index c6f0efdf7a..8665089aa1 100644 --- a/packages/nodes-base/nodes/Clockify/UserDtos.ts +++ b/packages/nodes-base/nodes/Clockify/UserDtos.ts @@ -7,7 +7,7 @@ enum UserStatusEnum { interface IUserSettingsDto { } -export interface ICurrentUserDto { +export interface IUserDto { activeWorkspace: string; defaultWorkspace: string; email: string; diff --git a/packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts b/packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts index a06540c364..d7f1f0ba3c 100644 --- a/packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts +++ b/packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts @@ -74,3 +74,9 @@ export interface IWorkspaceDto { name: string; workspaceSettings: IWorkspaceSettingsDto; } + +export interface IClientDto { + id: string; + name: string; + workspaceId: string; +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 56bd0cb5f6..0131302bb1 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -221,6 +221,7 @@ "dist/nodes/ClickUp/ClickUp.node.js", "dist/nodes/ClickUp/ClickUpTrigger.node.js", "dist/nodes/Clockify/ClockifyTrigger.node.js", + "dist/nodes/Clockify/Clockify.node.js", "dist/nodes/Cockpit/Cockpit.node.js", "dist/nodes/Coda/Coda.node.js", "dist/nodes/CoinGecko/CoinGecko.node.js",