diff --git a/packages/nodes-base/credentials/ClockifyApi.credentials.ts b/packages/nodes-base/credentials/ClockifyApi.credentials.ts index 87482adf32336..665be4f57479c 100644 --- a/packages/nodes-base/credentials/ClockifyApi.credentials.ts +++ b/packages/nodes-base/credentials/ClockifyApi.credentials.ts @@ -1,4 +1,6 @@ import { + IAuthenticateGeneric, + ICredentialTestRequest, ICredentialType, INodeProperties, } from 'n8n-workflow'; @@ -9,9 +11,6 @@ export class ClockifyApi implements ICredentialType { displayName = 'Clockify API'; documentationUrl = 'clockify'; properties: INodeProperties[] = [ - // The credentials to get from user and save encrypted. - // Properties can be defined exactly in the same way - // as node properties. { displayName: 'API Key', name: 'apiKey', @@ -19,4 +18,18 @@ export class ClockifyApi implements ICredentialType { default: '', }, ]; + authenticate: IAuthenticateGeneric = { + type: 'generic', + properties: { + headers: { + 'X-Api-Key': '={{$credentials.apiKey}}', + }, + }, + }; + test: ICredentialTestRequest = { + request: { + baseURL: 'https://api.clockify.me/api/v1', + url: '/workspaces', + }, + }; } diff --git a/packages/nodes-base/nodes/Clockify/ClientDescription.ts b/packages/nodes-base/nodes/Clockify/ClientDescription.ts new file mode 100644 index 0000000000000..bbc38bade0050 --- /dev/null +++ b/packages/nodes-base/nodes/Clockify/ClientDescription.ts @@ -0,0 +1,271 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const clientOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: [ + 'client', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a client', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a client', + }, + { + name: 'Get', + value: 'get', + description: 'Get a client', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all clients', + }, + { + name: 'Update', + value: 'update', + description: 'Update a client', + }, + ], + default: 'create', + }, +]; + +export const clientFields: INodeProperties[] = [ + + /* -------------------------------------------------------------------------- */ + /* client:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Client Name', + name: 'name', + type: 'string', + required: true, + default: '', + description: 'Name of client being created', + displayOptions: { + show: { + resource: [ + 'client', + ], + operation: [ + 'create', + ], + }, + }, + }, + /* -------------------------------------------------------------------------- */ + /* client:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Client ID', + name: 'clientId', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'client', + ], + operation: [ + 'delete', + ], + }, + }, + }, + /* -------------------------------------------------------------------------- */ + /* client:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Client ID', + name: 'clientId', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'client', + ], + operation: [ + 'get', + ], + }, + }, + }, + /* -------------------------------------------------------------------------- */ + /* client:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'client', + ], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'client', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'Max number of results to return', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'client', + ], + operation: [ + 'getAll', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Archived', + name: 'archived', + type: 'boolean', + default: false, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'If provided, clients will be filtered by name', + }, + { + displayName: 'Sort Order', + name: 'sort-order', + type: 'options', + options: [ + { + name: 'Ascending', + value: 'ASCENDING', + }, + { + name: 'Descending', + value: 'DESCENDING', + }, + ], + default: '', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* client:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Client ID', + name: 'clientId', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'client', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'client', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'client', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Address', + name: 'address', + type: 'string', + default: '', + description: 'Address of client being created/updated', + }, + { + displayName: 'Archived', + name: 'archived', + type: 'boolean', + default: false, + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Clockify/Clockify.node.ts b/packages/nodes-base/nodes/Clockify/Clockify.node.ts index 14cecf69aeef9..0cf4e69ca2ab7 100644 --- a/packages/nodes-base/nodes/Clockify/Clockify.node.ts +++ b/packages/nodes-base/nodes/Clockify/Clockify.node.ts @@ -29,6 +29,11 @@ import { IProjectDto, } from './ProjectInterfaces'; +import { + clientFields, + clientOperations, +} from './ClientDescription'; + import { projectFields, projectOperations, @@ -49,6 +54,16 @@ import { timeEntryOperations, } from './TimeEntryDescription'; +import { + userFields, + userOperations, +} from './UserDescription'; + +import { + workspaceFields, + workspaceOperations, +} from './WorkspaceDescription'; + import moment from 'moment-timezone'; export class Clockify implements INodeType { @@ -78,6 +93,10 @@ export class Clockify implements INodeType { type: 'options', noDataExpression: true, options: [ + { + name: 'Client', + value: 'client', + }, { name: 'Project', value: 'project', @@ -94,13 +113,25 @@ export class Clockify implements INodeType { name: 'Time Entry', value: 'timeEntry', }, + { + name: 'User', + value: 'user', + }, + { + name: 'Workspace', + value: 'workspace', + }, ], default: 'project', }, + ...clientOperations, ...projectOperations, ...tagOperations, ...taskOperations, ...timeEntryOperations, + ...userOperations, + ...workspaceOperations, + ...workspaceFields, { displayName: 'Workspace Name or ID', name: 'workspaceId', @@ -111,10 +142,19 @@ export class Clockify implements INodeType { }, required: true, default: [], + displayOptions: { + hide: { + resource: [ + 'workspace', + ], + }, + }, }, + ...clientFields, ...projectFields, ...tagFields, ...taskFields, + ...userFields, ...timeEntryFields, ], }; @@ -243,8 +283,122 @@ export class Clockify implements INodeType { const operation = this.getNodeParameter('operation', 0) as string; for (let i = 0; i < length; i++) { - try { + if (resource === 'client') { + + + if (operation === 'create') { + + 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}/clients`, + body, + qs, + ); + } + + if (operation === 'delete') { + + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + + const clientId = this.getNodeParameter('clientId', i) as string; + + responseData = await clockifyApiRequest.call( + this, + 'DELETE', + `/workspaces/${workspaceId}/clients/${clientId}`, + {}, + qs, + ); + } + + if (operation === 'update') { + + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + + const clientId = this.getNodeParameter('clientId', i) as string; + const name = this.getNodeParameter('name', i) as string; + + const updateFields = this.getNodeParameter( + 'updateFields', + i, + ) as IDataObject; + + const body: IDataObject = { + name, + }; + + Object.assign(body, updateFields); + + responseData = await clockifyApiRequest.call( + this, + 'PUT', + `/workspaces/${workspaceId}/clients/${clientId}`, + body, + qs, + ); + } + + if (operation === 'get') { + + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + + const clientId = this.getNodeParameter('clientId', i) as string; + + responseData = await clockifyApiRequest.call( + this, + 'GET', + `/workspaces/${workspaceId}/clients/${clientId}`, + {}, + 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}/clients`, + {}, + qs, + ); + + } else { + + qs.limit = this.getNodeParameter('limit', i) as number; + + responseData = await clockifyApiRequestAllItems.call( + this, + 'GET', + `/workspaces/${workspaceId}/clients`, + {}, + qs, + ); + + responseData = responseData.splice(0, qs.limit); + } + } + } + if (resource === 'project') { if (operation === 'create') { @@ -291,7 +445,6 @@ export class Clockify implements INodeType { qs, ); - responseData = { success: true }; } if (operation === 'get') { @@ -729,12 +882,66 @@ export class Clockify implements INodeType { } } + if (resource === 'user') { + + 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}/users`, + {}, + qs, + ); + + } else { + + qs.limit = this.getNodeParameter('limit', i) as number; + + responseData = await clockifyApiRequestAllItems.call( + this, + 'GET', + `/workspaces/${workspaceId}/users`, + {}, + qs, + ); + + responseData = responseData.splice(0, qs.limit); + } + } + } + + if (resource === 'workspace') { + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + responseData = await clockifyApiRequest.call( + this, + 'GET', + '/workspaces', + {}, + qs, + ); + if (!returnAll) { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.splice(0, qs.limit); + } + } + } + if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); } else if (responseData !== undefined) { - returnData.push(responseData as IDataObject); } } catch (error) { diff --git a/packages/nodes-base/nodes/Clockify/GenericFunctions.ts b/packages/nodes-base/nodes/Clockify/GenericFunctions.ts index 2809ac10cb10b..dc478ecaf1c5f 100644 --- a/packages/nodes-base/nodes/Clockify/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Clockify/GenericFunctions.ts @@ -9,18 +9,15 @@ import { } from 'n8n-core'; import { - IDataObject, NodeApiError, NodeOperationError, + IDataObject, NodeApiError, } 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 - - const credentials = await this.getCredentials('clockifyApi'); const BASE_URL = 'https://api.clockify.me/api/v1'; const options: OptionsWithUri = { headers: { 'Content-Type': 'application/json', - 'X-Api-Key': credentials.apiKey as string, }, method, qs, @@ -31,7 +28,7 @@ export async function clockifyApiRequest(this: ILoadOptionsFunctions | IPollFunc }; try { - return await this.helpers.request!(options); + return await this.helpers.requestWithAuthentication.call(this, 'clockifyApi', options); } catch (error) { throw new NodeApiError(this.getNode(), error); } diff --git a/packages/nodes-base/nodes/Clockify/TaskDescription.ts b/packages/nodes-base/nodes/Clockify/TaskDescription.ts index 4eaa8be99c8ee..3e0bc9dc9a214 100644 --- a/packages/nodes-base/nodes/Clockify/TaskDescription.ts +++ b/packages/nodes-base/nodes/Clockify/TaskDescription.ts @@ -107,6 +107,7 @@ export const taskFields: INodeProperties[] = [ default: {}, options: [ { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options displayName: 'Assignee Names or IDs', name: 'assigneeIds', type: 'multiOptions', @@ -314,6 +315,7 @@ export const taskFields: INodeProperties[] = [ default: {}, options: [ { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options displayName: 'Assignee Names or IDs', name: 'assigneeIds', type: 'multiOptions', diff --git a/packages/nodes-base/nodes/Clockify/TimeEntryDescription.ts b/packages/nodes-base/nodes/Clockify/TimeEntryDescription.ts index 922a1580b8c1a..a87fe02ff35bc 100644 --- a/packages/nodes-base/nodes/Clockify/TimeEntryDescription.ts +++ b/packages/nodes-base/nodes/Clockify/TimeEntryDescription.ts @@ -154,6 +154,7 @@ export const timeEntryFields: INodeProperties[] = [ default: '', }, { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options displayName: 'Tag Names or IDs', name: 'tagIds', type: 'multiOptions', @@ -364,6 +365,7 @@ export const timeEntryFields: INodeProperties[] = [ default: '', }, { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options displayName: 'Tag Names or IDs', name: 'tagIds', type: 'multiOptions', diff --git a/packages/nodes-base/nodes/Clockify/UserDescription.ts b/packages/nodes-base/nodes/Clockify/UserDescription.ts new file mode 100644 index 0000000000000..605af49c2cd79 --- /dev/null +++ b/packages/nodes-base/nodes/Clockify/UserDescription.ts @@ -0,0 +1,169 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const userOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + description: 'Get all users', + }, + ], + default: 'getAll', + }, +]; + +export const userFields: INodeProperties[] = [ + + /* -------------------------------------------------------------------------- */ + /* user:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'user', + ], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'user', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'Max number of results to return', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'If provided, you\'ll get a filtered list of users that contain the provided string in their email address', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'If provided, you\'ll get a filtered list of users that contain the provided string in their name', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Active', + value: 'ACTIVE', + }, + { + name: 'Inactive', + value: 'INACTIVE', + }, + { + name: 'Pending', + value: 'PENDING', + }, + { + name: 'Declined', + value: 'DECLINED', + }, + ], + default: '', + description: 'If provided, you\'ll get a filtered list of users with the corresponding status', + }, + { + displayName: 'Sort Column', + name: 'sort-column', + type: 'options', + options: [ + { + name: 'Email', + value: 'EMAIL', + }, + { + name: 'Name', + value: 'NAME', + }, + { + name: 'Hourly Rate', + value: 'HOURLYRATE', + }, + ], + default: '', + }, + { + displayName: 'Sort Order', + name: 'sort-order', + type: 'options', + options: [ + { + name: 'Ascending', + value: 'ASCENDING', + }, + { + name: 'Descending', + value: 'DESCENDING', + }, + ], + default: '', + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Clockify/WorkspaceDescription.ts b/packages/nodes-base/nodes/Clockify/WorkspaceDescription.ts new file mode 100644 index 0000000000000..4b9282769aaff --- /dev/null +++ b/packages/nodes-base/nodes/Clockify/WorkspaceDescription.ts @@ -0,0 +1,71 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const workspaceOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: [ + 'workspace', + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + description: 'Get all workspaces', + }, + ], + default: 'getAll', + }, +]; + +export const workspaceFields: INodeProperties[] = [ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'workspace', + ], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'workspace', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'Max number of results to return', + }, +];