From d99e4a7bfb7b6a088b56aad4566a12f5b93fa431 Mon Sep 17 00:00:00 2001 From: Mark Horninger Date: Mon, 3 Feb 2020 17:11:35 -0500 Subject: [PATCH 1/6] Building out the skeleton for Clockify integration --- .../credentials/ClockifyApi.credentials.ts | 21 +++ .../nodes/Clockify/ClockifyTrigger.node.ts | 121 ++++++++++++++++++ .../nodes-base/nodes/Clockify/CommonDtos.ts | 19 +++ .../nodes/Clockify/EntryTypeEnum.ts | 3 + .../nodes/Clockify/GenericFunctions.ts | 38 ++++++ .../nodes-base/nodes/Clockify/UserDtos.ts | 20 +++ .../nodes/Clockify/WorkpaceInterfaces.ts | 77 +++++++++++ .../Clockify/images/clockify-mark-blue.png | Bin 0 -> 5955 bytes packages/nodes-base/package.json | 3 + 9 files changed, 302 insertions(+) create mode 100644 packages/nodes-base/credentials/ClockifyApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Clockify/CommonDtos.ts create mode 100644 packages/nodes-base/nodes/Clockify/EntryTypeEnum.ts create mode 100644 packages/nodes-base/nodes/Clockify/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Clockify/UserDtos.ts create mode 100644 packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts create mode 100644 packages/nodes-base/nodes/Clockify/images/clockify-mark-blue.png 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..a67e7037b --- /dev/null +++ b/packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts @@ -0,0 +1,121 @@ +import {IPollFunctions} from 'n8n-core'; +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + clockifyApiRequest, +} from './GenericFunctions'; + +import {IWorkspaceDto} from "./WorkpaceInterfaces"; +import {EntryTypeEnum} from "./EntryTypeEnum"; +import {ICurrentUserDto} from "./UserDtos"; +import * as moment from "moment"; + + +export class ClockifyTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Clockify Event', + icon: 'file:images/clockify-mark-blue.png', + name: 'clockifyTrigger', + group: ['trigger'], + version: 1, + description: 'Watches Clockify For Events', + defaults: { + name: 'Clockify Event', + color: '#00FF00', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'clockifyApi', + required: 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: '', + }, + ] + }; + + 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'); + + const userInfo : ICurrentUserDto = await clockifyApiRequest.call(this,'GET', 'user'); + const qs : IDataObject = {}; + let resource: string; + let result = null; + + switch (triggerField) { + case EntryTypeEnum.NEW_TIME_ENTRY : + default: + resource = `workspaces/${workspaceId}/user/${userInfo.id}/time-entries`; + qs.start = webhookData.lastTimeChecked; + qs.end = moment().toISOString(); + qs.hydrated = true; + qs['in-progress'] = false; + break; + } + console.error(qs); + try { + result = await clockifyApiRequest.call(this, 'GET', resource, {}, qs ); + webhookData.lastTimeChecked = qs.end_date; + } + catch( e ) { + throw new Error(`Clockify Exception: ${e}`); + } + 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..b5449c5c5 --- /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..62a594350 --- /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..cf7ee1581 --- /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..fdf0c005e --- /dev/null +++ b/packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts @@ -0,0 +1,77 @@ +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/images/clockify-mark-blue.png b/packages/nodes-base/nodes/Clockify/images/clockify-mark-blue.png new file mode 100644 index 0000000000000000000000000000000000000000..ac4c44c7632112188285f1602a7d383dadb47c0b GIT binary patch literal 5955 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 SL?(OY4b08XnpPOQ$NnG5MH@>1 literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index b59b37bef..6c9be610b 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", @@ -112,6 +113,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", @@ -283,3 +285,4 @@ ] } } + From 447fff12b0e5b11f7ce47964c6d03ef69c840b71 Mon Sep 17 00:00:00 2001 From: Mark Horninger Date: Wed, 19 Feb 2020 12:34:32 -0500 Subject: [PATCH 2/6] Removed unnecessary logging. --- packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts b/packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts index a67e7037b..7b14182ff 100644 --- a/packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts +++ b/packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts @@ -104,7 +104,7 @@ export class ClockifyTrigger implements INodeType { qs['in-progress'] = false; break; } - console.error(qs); + try { result = await clockifyApiRequest.call(this, 'GET', resource, {}, qs ); webhookData.lastTimeChecked = qs.end_date; From 78bf915e65435b6e488e387a2c155a796499e3be Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 21 Feb 2020 11:34:51 +0100 Subject: [PATCH 3/6] :bug: Fix issue that settings did not get used --- packages/cli/src/WorkflowRunnerProcess.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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(); From 5548e4e794137e4364a05522c1494f1483fe1bc6 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 21 Feb 2020 11:41:08 +0100 Subject: [PATCH 4/6] :zap: Improved Clickify-Node --- .../nodes/Clockify/ClockifyTrigger.node.ts | 40 ++++++++++-------- .../nodes-base/nodes/Clockify/CommonDtos.ts | 8 ++-- .../nodes/Clockify/GenericFunctions.ts | 6 +-- .../nodes-base/nodes/Clockify/UserDtos.ts | 2 +- .../nodes/Clockify/WorkpaceInterfaces.ts | 35 ++++++++------- .../clockify-mark-blue.png => clockify.png} | Bin packages/nodes-base/package.json | 5 +-- 7 files changed, 49 insertions(+), 47 deletions(-) rename packages/nodes-base/nodes/Clockify/{images/clockify-mark-blue.png => clockify.png} (100%) diff --git a/packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts b/packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts index 7b14182ff..e2a64f336 100644 --- a/packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts +++ b/packages/nodes-base/nodes/Clockify/ClockifyTrigger.node.ts @@ -1,3 +1,5 @@ +import * as moment from 'moment-timezone'; + import {IPollFunctions} from 'n8n-core'; import { IDataObject, @@ -12,22 +14,21 @@ import { clockifyApiRequest, } from './GenericFunctions'; -import {IWorkspaceDto} from "./WorkpaceInterfaces"; -import {EntryTypeEnum} from "./EntryTypeEnum"; -import {ICurrentUserDto} from "./UserDtos"; -import * as moment from "moment"; +import { EntryTypeEnum } from './EntryTypeEnum'; +import { ICurrentUserDto } from './UserDtos'; +import { IWorkspaceDto } from './WorkpaceInterfaces'; export class ClockifyTrigger implements INodeType { description: INodeTypeDescription = { - displayName: 'Clockify Event', - icon: 'file:images/clockify-mark-blue.png', + displayName: 'Clockify Trigger', + icon: 'file:clockify.png', name: 'clockifyTrigger', group: ['trigger'], version: 1, description: 'Watches Clockify For Events', defaults: { - name: 'Clockify Event', + name: 'Clockify Trigger', color: '#00FF00', }, inputs: [], @@ -38,6 +39,7 @@ export class ClockifyTrigger implements INodeType { required: true, } ], + polling: true, properties: [ { displayName: 'Workspace', @@ -60,7 +62,7 @@ export class ClockifyTrigger implements INodeType { } ], required: true, - default: '', + default: EntryTypeEnum.NEW_TIME_ENTRY, }, ] }; @@ -89,7 +91,12 @@ export class ClockifyTrigger implements INodeType { const triggerField = this.getNodeParameter('watchField') as EntryTypeEnum; const workspaceId = this.getNodeParameter('workspaceId'); - const userInfo : ICurrentUserDto = await clockifyApiRequest.call(this,'GET', 'user'); + 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; @@ -97,21 +104,18 @@ export class ClockifyTrigger implements INodeType { switch (triggerField) { case EntryTypeEnum.NEW_TIME_ENTRY : default: - resource = `workspaces/${workspaceId}/user/${userInfo.id}/time-entries`; + const workflowTimezone = this.getTimezone(); + resource = `workspaces/${workspaceId}/user/${webhookData.userId}/time-entries`; qs.start = webhookData.lastTimeChecked; - qs.end = moment().toISOString(); + qs.end = moment().tz(workflowTimezone).format('YYYY-MM-DDTHH:mm:SS') + 'Z'; qs.hydrated = true; qs['in-progress'] = false; break; } - try { - result = await clockifyApiRequest.call(this, 'GET', resource, {}, qs ); - webhookData.lastTimeChecked = qs.end_date; - } - catch( e ) { - throw new Error(`Clockify Exception: ${e}`); - } + result = await clockifyApiRequest.call(this, 'GET', resource, {}, qs ); + webhookData.lastTimeChecked = qs.end; + if (Array.isArray(result) && result.length !== 0) { result = [this.helpers.returnJsonArray(result)]; } diff --git a/packages/nodes-base/nodes/Clockify/CommonDtos.ts b/packages/nodes-base/nodes/Clockify/CommonDtos.ts index b5449c5c5..2876fd4ff 100644 --- a/packages/nodes-base/nodes/Clockify/CommonDtos.ts +++ b/packages/nodes-base/nodes/Clockify/CommonDtos.ts @@ -4,10 +4,10 @@ export interface IHourlyRateDto { } enum MembershipStatusEnum { - PENDING = "PENDING", - ACTIVE = "ACTIVE", - DECLINED = "DECLINED", - INACTIVE = "INACTIVE" + PENDING = 'PENDING', + ACTIVE = 'ACTIVE', + DECLINED = 'DECLINED', + INACTIVE = 'INACTIVE' } export interface IMembershipDto { diff --git a/packages/nodes-base/nodes/Clockify/GenericFunctions.ts b/packages/nodes-base/nodes/Clockify/GenericFunctions.ts index 62a594350..730fad23a 100644 --- a/packages/nodes-base/nodes/Clockify/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Clockify/GenericFunctions.ts @@ -2,16 +2,16 @@ import { OptionsWithUri } from 'request'; import { ILoadOptionsFunctions, IPollFunctions -} from "n8n-core"; +} from 'n8n-core'; -import {IDataObject} from "n8n-workflow"; +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 BASE_URL = 'https://api.clockify.me/api/v1'; const options: OptionsWithUri = { headers: { diff --git a/packages/nodes-base/nodes/Clockify/UserDtos.ts b/packages/nodes-base/nodes/Clockify/UserDtos.ts index cf7ee1581..c6f0efdf7 100644 --- a/packages/nodes-base/nodes/Clockify/UserDtos.ts +++ b/packages/nodes-base/nodes/Clockify/UserDtos.ts @@ -1,4 +1,4 @@ -import {IMembershipDto} from "./CommonDtos"; +import { IMembershipDto } from './CommonDtos'; enum UserStatusEnum { ACTIVE, PENDING_EMAIL_VERIFICATION, DELETED diff --git a/packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts b/packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts index fdf0c005e..a06540c36 100644 --- a/packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts +++ b/packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts @@ -1,31 +1,31 @@ -import {IHourlyRateDto, IMembershipDto} from "./CommonDtos"; +import { IHourlyRateDto, IMembershipDto } from './CommonDtos'; enum AdminOnlyPagesEnum { - PROJECT ="PROJECT", - TEAM = "TEAM", - REPORTS = "REPORTS" + PROJECT ='PROJECT', + TEAM = 'TEAM', + REPORTS = 'REPORTS', } enum DaysOfWeekEnum { - MONDAY = "MONDAY", - TUESDAY = "TUESDAY", - WEDNESDAY = "WEDNESDAY", - THURSDAY = "THURSDAY", - FRIDAY = "FRIDAY", - SATURDAY = "SATURDAY", - SUNDAY = "SUNDAY" + MONDAY = 'MONDAY', + TUESDAY = 'TUESDAY', + WEDNESDAY = 'WEDNESDAY', + THURSDAY = 'THURSDAY', + FRIDAY = 'FRIDAY', + SATURDAY = 'SATURDAY', + SUNDAY = 'SUNDAY', } enum DatePeriodEnum { - DAYS="DAYS", - WEEKS = "WEEKS", - MONTHS = "MONTHS" + DAYS = 'DAYS', + WEEKS = 'WEEKS', + MONTHS = 'MONTHS', } enum AutomaticLockTypeEnum { - WEEKLY = "WEEKLY", - MONTHLY = "MONTHLY", - OLDER_THAN = "OLDER_THAN" + WEEKLY = 'WEEKLY', + MONTHLY = 'MONTHLY', + OLDER_THAN = 'OLDER_THAN', } interface IAutomaticLockDto { @@ -74,4 +74,3 @@ export interface IWorkspaceDto { name: string; workspaceSettings: IWorkspaceSettingsDto; } - diff --git a/packages/nodes-base/nodes/Clockify/images/clockify-mark-blue.png b/packages/nodes-base/nodes/Clockify/clockify.png similarity index 100% rename from packages/nodes-base/nodes/Clockify/images/clockify-mark-blue.png rename to packages/nodes-base/nodes/Clockify/clockify.png diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index f6fc8bae5..59b28c1d1 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -37,7 +37,7 @@ "dist/credentials/ChargebeeApi.credentials.js", "dist/credentials/ClearbitApi.credentials.js", "dist/credentials/ClickUpApi.credentials.js", - "dist/credentials/ClockifyApi.credentials.js", + "dist/credentials/ClockifyApi.credentials.js", "dist/credentials/CodaApi.credentials.js", "dist/credentials/CopperApi.credentials.js", "dist/credentials/DisqusApi.credentials.js", @@ -119,7 +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/Clockify/ClockifyTrigger.node.js", "dist/nodes/Coda/Coda.node.js", "dist/nodes/Copper/CopperTrigger.node.js", "dist/nodes/Cron.node.js", @@ -295,4 +295,3 @@ ] } } - From eacac9a630524782865ffd20865e9d6b127f6249 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 21 Feb 2020 11:41:43 +0100 Subject: [PATCH 5/6] :zap: Add missing Mocean-Icon --- packages/nodes-base/nodes/Mocean/logo.png | Bin 1833 -> 0 bytes packages/nodes-base/nodes/Mocean/mocean.png | Bin 0 -> 669 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/nodes-base/nodes/Mocean/logo.png create mode 100644 packages/nodes-base/nodes/Mocean/mocean.png diff --git a/packages/nodes-base/nodes/Mocean/logo.png b/packages/nodes-base/nodes/Mocean/logo.png deleted file mode 100644 index 5368d5f0b4c05e4dd27ad11560c880b384f60248..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1833 zcmV+^2iEwBP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!~g&e!~vBn z4jTXf0AG4kSaechcOY9y8Ey_$*C@9KL%gjmT zQUC*E6D|`=6HB8+0|NsK0|+oQFfcX*l14zbIaDao00o$so23DvxmmKADTvPn08hFV zXd5lqi~s-zLrFwIRA@u(T4`)lRTTba=}e)EP?jRQfwt0u(t>P3p|Y6@f}#k${lA%fZW`q022taliM^$vru-eC~dI}E~lhe25HFbL}% z24TI!Agp&7g!K-Cu-@^|5jM?Ulj~iuIS)3g88NNGaeB>Q-;#!)ft$EyL3OnYD`s@Z zq#@C2e(CoGn6oYy6_+a1d+_sdPex+v+}?<5+d!KmF5alZl%+X1bmmVuYTYoI09Ty{ zQ7yuezfODB#=XT@^4(dSyIcYPzXhtQTv$A%E2a+<2 zTLC66IfB)@is1Elv2uD3OdJxW=Jy`Ih5_&I0r*VMp)oMmxjjIQ8*UorAYF>T8v#x_SrBD|b{N1j9c%faIC`aN_h$R9=;JZGbjql}=^jN9yWa^&Gj(T76Q@ z`QdbV;FW-y&6t#$<(s9Y*pnX|b?V-1HuGvR!6L78d|Adh&5~ID1MfqFv^Cn;NzPU0 z#>UNza)V&RcW4Z6&RBR4zJcqEI3ymCp?GfAKK1$D@9hSfg~A#fg#2y8;XF1Lj-wnW z#-n`K2o#h2_U#40BX%V9ZG)TpUPt-C(J0Ltja$dZVfLtwY9GOt{l5beaQ>O=S&{b5 z8Q+Qq#e>5lMM^)%FFLH|#b=q<7I>?!gvy=Wie*C5hxM67F!KXSW*osb3+2ZLhueWW zPVD)jpJqrzM%2TC3Gvjc8zq#vWYAki1yFt~r*%^@58^{*4;f~LJB<<)vS7Jnewv6T z3}SeQO-Z#8pJAt+A2ZWV@ao0pIlZ+Bl32B?P`meoDs9;`M3vWCN?%H+o?Z4(>=ZP@ zbaHTy7D_IKr*EjK+@jmck&s|3y2mzVbUm;z%jcODhEvrS_9gWy z?-QaM`7G{7;T6_DgJ5eI3{#i2FeR+HH=>tg-uhF@u88u!TtI*5_o!#vB4uQ(BAlXv z5CAG5szn&4zsmJs3leAijMkA1Kmatt+(@GTN}tv$I-7pEfb^e=)u!W;+M}BF>xX3K z-(gY>N=1-+*R$Of(tW6P(#F4?WK-nlAAD~WDH1bQl2}L(9%taC2}LqMQex%o2|4UD zfw);#gT<4(Fxx=(;Vby|*NfbXf%oCACt~={&=_@}wzH7Zy+CKZn9OchBF%}ONz-cA zJWOR9R@2{_N?viP3YavwL0unZCt;xIn7=_ih&HTu$%^xK4ZMfng!kx}dn11$<|RL& z>~>JP*7`v>d%g?!w<%%%9oslJW(WJQJtQ|w93mVdJsQ@s9;J~C8OpPm0ok8(4W^#y2;os= z@0G4>aJ>>4^ZTgJWi09s+^EDQ9y5{sH^CIo+LX~L4dH{gqGxOq^m>M6sK&PU%l_ESW_fbe^MELiQvaB>9x7Sn^IZYQ>W{Sdlse#H zGn0lUFgxXp<*2CAbzX4-YCCJU<3K)f_RMBHgRtIV5Y{^k!g_~6Snn_h>m3GRy~7}^ZvcM* Xq(*?jdKNqp00000NkvXXu0mjfPl00L diff --git a/packages/nodes-base/nodes/Mocean/mocean.png b/packages/nodes-base/nodes/Mocean/mocean.png new file mode 100644 index 0000000000000000000000000000000000000000..f660c71e0a0f226599a928dd8d4eaac3f5871533 GIT binary patch literal 669 zcmV;O0%HA%P)hSn{p3q{8 z#+kF*Q-HxGU9lcmt`bkG`uzR)`TXzm`O4SqkF3{cj>t%Ny*zHYF=n;9&E~ku<&>}4 zPJF;Ya=QKg{@Ua6)Zg#U+wH~F>cG(Gp10hBqSA7g%r0cKDq*wW==86|;)jA`y^ijw0004`Nkl9!xpZ_;Q};f zDJ46qu39~iW9+M&^XNAC5k2>Sq4I8tkDN?YHXCA}Ma7&Yv{~oOKbruwk7GIRqc;*8 z2JR^65%3{LtC!gsP>?O*ZP@kt{mnMBlVUI^`Z9vEQb|lc-ed37HMSjqe9(WxRqWcH@u1SwG@8T^3l~SxJfPC1pz=Kb)x?$!?sSq^ zPfli9+~T%syqmY#KQXsH7(V)>tt*-0PrHe zHEw~*nEsB~K&@7*)xN0T#P1wvws5Li00000NkvXXu0mjf D0<%bt literal 0 HcmV?d00001 From 78121d6e9f0a577b36bfadded6659887cdbccb1c Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 21 Feb 2020 11:43:52 +0100 Subject: [PATCH 6/6] :zap: Optimize Clockify icon --- .../nodes-base/nodes/Clockify/clockify.png | Bin 5955 -> 2359 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/nodes-base/nodes/Clockify/clockify.png b/packages/nodes-base/nodes/Clockify/clockify.png index ac4c44c7632112188285f1602a7d383dadb47c0b..48a49a45883002efc273569bdcaa331da9d5120c 100644 GIT binary patch literal 2359 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 SL?(OY4b08XnpPOQ$NnG5MH@>1