feat(Clockify Node): Add more resources and improvements (#3411)

*  Add more Clockify resources

* Moved headers in credentials file and added testing

* add address to additional fields

* Add pagination to workflow

* 🎨 Rename additional fields to filter

* Remove non-required fields to additional fields

* 🔥 Remove loading of client id

*  Improvements

*  Small change

Co-authored-by: pemontto <pemontto@gmail.com>
Co-authored-by: ricardo <ricardoespinoza105@gmail.com>
This commit is contained in:
agobrech 2022-06-29 09:04:36 +02:00 committed by GitHub
parent 6e595c7276
commit 447d19024c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 743 additions and 11 deletions

View file

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

View file

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

View file

@ -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) {

View file

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

View file

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

View file

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

View file

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

View file

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