diff --git a/packages/cli/src/WorkflowRunnerProcess.ts b/packages/cli/src/WorkflowRunnerProcess.ts index b9472fc66..250e23cda 100644 --- a/packages/cli/src/WorkflowRunnerProcess.ts +++ b/packages/cli/src/WorkflowRunnerProcess.ts @@ -58,7 +58,7 @@ export class WorkflowRunnerProcess { const nodeTypes = NodeTypes(); await nodeTypes.init(nodeTypesData); - this.workflow = new Workflow({ id: this.data.workflowData.id as string | undefined, name: this.data.workflowData.name, nodes: this.data.workflowData!.nodes, connections: this.data.workflowData!.connections, active: this.data.workflowData!.active, nodeTypes, staticData: this.data.workflowData!.staticData}); + this.workflow = new Workflow({ id: this.data.workflowData.id as string | undefined, name: this.data.workflowData.name, nodes: this.data.workflowData!.nodes, connections: this.data.workflowData!.connections, active: this.data.workflowData!.active, nodeTypes, staticData: this.data.workflowData!.staticData, settings: this.data.workflowData!.settings}); const additionalData = await WorkflowExecuteAdditionalData.getBase(this.data.credentials); additionalData.hooks = this.getProcessForwardHooks(); diff --git a/packages/nodes-base/credentials/ClockifyApi.credentials.ts b/packages/nodes-base/credentials/ClockifyApi.credentials.ts new file mode 100644 index 000000000..ce84ab275 --- /dev/null +++ b/packages/nodes-base/credentials/ClockifyApi.credentials.ts @@ -0,0 +1,21 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class ClockifyApi implements ICredentialType { + name = 'clockifyApi'; + displayName = 'Clockify API'; + properties = [ + // 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', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts b/packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts new file mode 100644 index 000000000..e2a64f336 --- /dev/null +++ b/packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts @@ -0,0 +1,125 @@ +import * as moment from 'moment-timezone'; + +import {IPollFunctions} from 'n8n-core'; +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + clockifyApiRequest, +} from './GenericFunctions'; + +import { EntryTypeEnum } from './EntryTypeEnum'; +import { ICurrentUserDto } from './UserDtos'; +import { IWorkspaceDto } from './WorkpaceInterfaces'; + + +export class ClockifyTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Clockify Trigger', + icon: 'file:clockify.png', + name: 'clockifyTrigger', + group: ['trigger'], + version: 1, + description: 'Watches Clockify For Events', + defaults: { + name: 'Clockify Trigger', + color: '#00FF00', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'clockifyApi', + required: true, + } + ], + polling: true, + properties: [ + { + displayName: 'Workspace', + name: 'workspaceId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'listWorkspaces', + }, + required: true, + default: '', + }, + { + displayName: 'Trigger', + name: 'watchField', + type: 'options', + options: [ + { + name: 'New Time Entry', + value: EntryTypeEnum.NEW_TIME_ENTRY, + } + ], + required: true, + default: EntryTypeEnum.NEW_TIME_ENTRY, + }, + ] + }; + + 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 poll(this: IPollFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const triggerField = this.getNodeParameter('watchField') as EntryTypeEnum; + const workspaceId = this.getNodeParameter('workspaceId'); + + 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'); + webhookData.userId = userInfo.id; + } + + const qs : IDataObject = {}; + let resource: string; + let result = null; + + switch (triggerField) { + case EntryTypeEnum.NEW_TIME_ENTRY : + default: + const workflowTimezone = this.getTimezone(); + resource = `workspaces/${workspaceId}/user/${webhookData.userId}/time-entries`; + qs.start = webhookData.lastTimeChecked; + qs.end = moment().tz(workflowTimezone).format('YYYY-MM-DDTHH:mm:SS') + 'Z'; + qs.hydrated = true; + qs['in-progress'] = false; + break; + } + + result = await clockifyApiRequest.call(this, 'GET', resource, {}, qs ); + webhookData.lastTimeChecked = qs.end; + + if (Array.isArray(result) && result.length !== 0) { + result = [this.helpers.returnJsonArray(result)]; + } + return result; + + } +} diff --git a/packages/nodes-base/nodes/Clockify/CommonDtos.ts b/packages/nodes-base/nodes/Clockify/CommonDtos.ts new file mode 100644 index 000000000..2876fd4ff --- /dev/null +++ b/packages/nodes-base/nodes/Clockify/CommonDtos.ts @@ -0,0 +1,19 @@ +export interface IHourlyRateDto { + amount: number; + currency: string; +} + +enum MembershipStatusEnum { + PENDING = 'PENDING', + ACTIVE = 'ACTIVE', + DECLINED = 'DECLINED', + INACTIVE = 'INACTIVE' +} + +export interface IMembershipDto { + hourlyRate: IHourlyRateDto; + membershipStatus: MembershipStatusEnum; + membershipType: string; + targetId: string; + userId: string; +} diff --git a/packages/nodes-base/nodes/Clockify/EntryTypeEnum.ts b/packages/nodes-base/nodes/Clockify/EntryTypeEnum.ts new file mode 100644 index 000000000..0df2a7101 --- /dev/null +++ b/packages/nodes-base/nodes/Clockify/EntryTypeEnum.ts @@ -0,0 +1,3 @@ +export enum EntryTypeEnum { + NEW_TIME_ENTRY, +} diff --git a/packages/nodes-base/nodes/Clockify/GenericFunctions.ts b/packages/nodes-base/nodes/Clockify/GenericFunctions.ts new file mode 100644 index 000000000..730fad23a --- /dev/null +++ b/packages/nodes-base/nodes/Clockify/GenericFunctions.ts @@ -0,0 +1,38 @@ +import { OptionsWithUri } from 'request'; +import { + ILoadOptionsFunctions, + IPollFunctions +} from 'n8n-core'; + +import { IDataObject } from 'n8n-workflow'; + +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'; + + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + 'X-Api-Key': credentials.apiKey as string, + }, + method, + qs, + body, + uri: `${BASE_URL}/${resource}`, + json: 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}`; + } + + throw new Error('Clockify Error: ' + errorMessage); + } +} diff --git a/packages/nodes-base/nodes/Clockify/UserDtos.ts b/packages/nodes-base/nodes/Clockify/UserDtos.ts new file mode 100644 index 000000000..c6f0efdf7 --- /dev/null +++ b/packages/nodes-base/nodes/Clockify/UserDtos.ts @@ -0,0 +1,20 @@ +import { IMembershipDto } from './CommonDtos'; + +enum UserStatusEnum { + ACTIVE, PENDING_EMAIL_VERIFICATION, DELETED +} + +interface IUserSettingsDto { +} + +export interface ICurrentUserDto { + activeWorkspace: string; + defaultWorkspace: string; + email: string; + id: string; + memberships: IMembershipDto []; + name: string; + profilePicture: string; + settings: IUserSettingsDto; + status: UserStatusEnum; +} diff --git a/packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts b/packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts new file mode 100644 index 000000000..a06540c36 --- /dev/null +++ b/packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts @@ -0,0 +1,76 @@ +import { IHourlyRateDto, IMembershipDto } from './CommonDtos'; + +enum AdminOnlyPagesEnum { + PROJECT ='PROJECT', + TEAM = 'TEAM', + REPORTS = 'REPORTS', +} + +enum DaysOfWeekEnum { + MONDAY = 'MONDAY', + TUESDAY = 'TUESDAY', + WEDNESDAY = 'WEDNESDAY', + THURSDAY = 'THURSDAY', + FRIDAY = 'FRIDAY', + SATURDAY = 'SATURDAY', + SUNDAY = 'SUNDAY', +} + +enum DatePeriodEnum { + DAYS = 'DAYS', + WEEKS = 'WEEKS', + MONTHS = 'MONTHS', +} + +enum AutomaticLockTypeEnum { + WEEKLY = 'WEEKLY', + MONTHLY = 'MONTHLY', + OLDER_THAN = 'OLDER_THAN', +} + +interface IAutomaticLockDto { + changeDay: DaysOfWeekEnum; + dayOfMonth: number; + firstDay: DaysOfWeekEnum; + olderThanPeriod: DatePeriodEnum; + olderThanValue: number; + type: AutomaticLockTypeEnum; +} + +interface IRound { + minutes: string; + round: string; +} + +interface IWorkspaceSettingsDto { + adminOnlyPages: AdminOnlyPagesEnum[]; + automaticLock: IAutomaticLockDto; + canSeeTimeSheet: boolean; + defaultBillableProjects: boolean; + forceDescription: boolean; + forceProjects: boolean; + forceTags: boolean; + forceTasks: boolean; + lockTimeEntries: string; + onlyAdminsCreateProject: boolean; + onlyAdminsCreateTag: boolean; + onlyAdminsSeeAllTimeEntries: boolean; + onlyAdminsSeeBillableRates: boolean; + onlyAdminsSeeDashboard: boolean; + onlyAdminsSeePublicProjectsEntries: boolean; + projectFavorites: boolean; + projectGroupingLabel: string; + projectPickerSpecialFilter: boolean; + round: IRound; + timeRoundingInReports: boolean; + trackTimeDownToSecond: boolean; +} + +export interface IWorkspaceDto { + hourlyRate: IHourlyRateDto; + id: string; + imageUrl: string; + memberships: IMembershipDto[]; + name: string; + workspaceSettings: IWorkspaceSettingsDto; +} diff --git a/packages/nodes-base/nodes/Clockify/clockify.png b/packages/nodes-base/nodes/Clockify/clockify.png new file mode 100644 index 000000000..48a49a458 Binary files /dev/null and b/packages/nodes-base/nodes/Clockify/clockify.png differ diff --git a/packages/nodes-base/nodes/Mocean/logo.png b/packages/nodes-base/nodes/Mocean/logo.png deleted file mode 100644 index 5368d5f0b..000000000 Binary files a/packages/nodes-base/nodes/Mocean/logo.png and /dev/null differ diff --git a/packages/nodes-base/nodes/Mocean/mocean.png b/packages/nodes-base/nodes/Mocean/mocean.png new file mode 100644 index 000000000..f660c71e0 Binary files /dev/null and b/packages/nodes-base/nodes/Mocean/mocean.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 150c7e133..59b28c1d1 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -37,6 +37,7 @@ "dist/credentials/ChargebeeApi.credentials.js", "dist/credentials/ClearbitApi.credentials.js", "dist/credentials/ClickUpApi.credentials.js", + "dist/credentials/ClockifyApi.credentials.js", "dist/credentials/CodaApi.credentials.js", "dist/credentials/CopperApi.credentials.js", "dist/credentials/DisqusApi.credentials.js", @@ -118,6 +119,7 @@ "dist/nodes/Clearbit/Clearbit.node.js", "dist/nodes/ClickUp/ClickUp.node.js", "dist/nodes/ClickUp/ClickUpTrigger.node.js", + "dist/nodes/Clockify/ClockifyTrigger.node.js", "dist/nodes/Coda/Coda.node.js", "dist/nodes/Copper/CopperTrigger.node.js", "dist/nodes/Cron.node.js",