From 3351113f28f28ae2c17b5e4fe684e647723e9521 Mon Sep 17 00:00:00 2001 From: Ronalds Upenieks <32895755+Rupenieks@users.noreply.github.com> Date: Tue, 24 Nov 2020 14:15:47 +0100 Subject: [PATCH] :zap: Add Harvest OAuth2 support (#697) * credentials, generic functions fit to oauth2, UI elements * :zap: Improvements to Harvest-Node * Update Harvest.node.ts * :zap: Add OAuth2 and move account id from credentials to node parameters * :zap: Minor improvements to Harvest-Node Co-authored-by: Rupenieks Co-authored-by: ricardo Co-authored-by: Jan Oberhauser --- packages/cli/BREAKING-CHANGES.md | 14 + .../credentials/HarvestApi.credentials.ts | 7 - .../HarvestOAuth2Api.credentials.ts | 48 + .../nodes/Harvest/CompanyDescription.ts | 2 +- .../nodes/Harvest/EstimateDescription.ts | 278 +++--- .../nodes/Harvest/ExpenseDescription.ts | 306 +++--- .../nodes/Harvest/GenericFunctions.ts | 125 +-- .../nodes-base/nodes/Harvest/Harvest.node.ts | 113 ++- .../nodes/Harvest/InvoiceDescription.ts | 328 +++---- .../nodes/Harvest/TaskDescription.ts | 1 - .../nodes/Harvest/TimeEntryDescription.ts | 914 +++++++++--------- packages/nodes-base/package.json | 1 + 12 files changed, 1133 insertions(+), 1004 deletions(-) create mode 100644 packages/nodes-base/credentials/HarvestOAuth2Api.credentials.ts diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index a63c2d2deb..865d0bb419 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -2,6 +2,20 @@ This list shows all the versions which include breaking changes and how to upgrade. +## 0.95.0 + +### What changed? + +In the Harvest Node, we moved the account field from the credentials to the node parameters. This will allow you to work witn multiples accounts without having to create multiples credentials. + +### When is action necessary? + +If you are using the Harvest Node. + +### How to upgrade: + +Open the node set the parameter `Account ID`. + ## 0.94.0 ### What changed? diff --git a/packages/nodes-base/credentials/HarvestApi.credentials.ts b/packages/nodes-base/credentials/HarvestApi.credentials.ts index 683010aa16..080ad2198d 100644 --- a/packages/nodes-base/credentials/HarvestApi.credentials.ts +++ b/packages/nodes-base/credentials/HarvestApi.credentials.ts @@ -8,13 +8,6 @@ export class HarvestApi implements ICredentialType { displayName = 'Harvest API'; documentationUrl = 'harvest'; properties = [ - { - displayName: 'Account ID', - name: 'accountId', - type: 'string' as NodePropertyTypes, - default: '', - description: 'Visit your account details page, and grab the Account ID. See Harvest Personal Access Tokens.', - }, { displayName: 'Access Token', name: 'accessToken', diff --git a/packages/nodes-base/credentials/HarvestOAuth2Api.credentials.ts b/packages/nodes-base/credentials/HarvestOAuth2Api.credentials.ts new file mode 100644 index 0000000000..0232af5cab --- /dev/null +++ b/packages/nodes-base/credentials/HarvestOAuth2Api.credentials.ts @@ -0,0 +1,48 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class HarvestOAuth2Api implements ICredentialType { + name = 'harvestOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Harvest OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://id.getharvest.com/oauth2/authorize', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://id.getharvest.com/api/v2/oauth2/token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: 'all', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body', + description: 'Resource to consume.', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Harvest/CompanyDescription.ts b/packages/nodes-base/nodes/Harvest/CompanyDescription.ts index d67939afa0..917b29deb6 100644 --- a/packages/nodes-base/nodes/Harvest/CompanyDescription.ts +++ b/packages/nodes-base/nodes/Harvest/CompanyDescription.ts @@ -1,6 +1,6 @@ import { INodeProperties } from 'n8n-workflow'; -const resource = [ 'company' ]; +const resource = ['company']; export const companyOperations = [ { diff --git a/packages/nodes-base/nodes/Harvest/EstimateDescription.ts b/packages/nodes-base/nodes/Harvest/EstimateDescription.ts index aac2090e26..f827a27d8b 100644 --- a/packages/nodes-base/nodes/Harvest/EstimateDescription.ts +++ b/packages/nodes-base/nodes/Harvest/EstimateDescription.ts @@ -1,6 +1,6 @@ import { INodeProperties } from 'n8n-workflow'; -const resource = [ 'estimate' ]; +const resource = ['estimate']; export const estimateOperations = [ { @@ -47,149 +47,149 @@ export const estimateOperations = [ export const estimateFields = [ -/* -------------------------------------------------------------------------- */ -/* estimate:getAll */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* estimate:getAll */ + /* -------------------------------------------------------------------------- */ -{ - displayName: 'Return All', - name: 'returnAll', - type: 'boolean', - displayOptions: { - show: { - resource, - operation: [ - 'getAll', - ], - }, - }, - default: false, - description: 'Returns a list of your estimates.', -}, -{ - displayName: 'Limit', - name: 'limit', - type: 'number', - displayOptions: { - show: { - resource, - operation: [ - 'getAll', - ], - returnAll: [ - false, - ], - }, - }, - typeOptions: { - minValue: 1, - maxValue: 100, - }, - default: 100, - description: 'How many results to return.', -}, -{ - displayName: 'Filters', - name: 'filters', - type: 'collection', - placeholder: 'Add Filter', - default: {}, - displayOptions: { - show: { - resource, - operation: [ - 'getAll', - ], - }, - }, - options: [ - { - displayName: 'Client ID', - name: 'client_id', - type: 'string', - default: '', - description: 'Only return time entries belonging to the client with the given ID.', - }, - { - displayName: 'From', - name: 'from', - type: 'dateTime', - default: '', - description: 'Only return time entries with a spent_date on or after the given date.', - }, - { - displayName: 'State', - name: 'state', - type: 'string', - default: '', - description: 'Only return estimates with a state matching the value provided. Options: draft, sent, accepted, or declined.', - }, - { - displayName: 'To', - name: 'to', - type: 'dateTime', - default: '', - description: 'Only return time entries with a spent_date on or before the given date.', - }, - { - displayName: 'Updated Since', - name: 'updated_since', - type: 'dateTime', - default: '', - description: 'Only return time entries that have been updated since the given date and time.', - }, - { - displayName: 'Page', - name: 'page', - type: 'number', - typeOptions: { - minValue: 1, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource, + operation: [ + 'getAll', + ], }, - default: 1, - description: 'The page number to use in pagination. For instance, if you make a list request and receive 100 records, your subsequent call can include page=2 to retrieve the next page of the list. (Default: 1)', - }, - ], -}, - -/* -------------------------------------------------------------------------- */ -/* estimate:get */ -/* -------------------------------------------------------------------------- */ -{ - displayName: 'Estimate Id', - name: 'id', - type: 'string', - default: '', - required: true, - displayOptions: { - show: { - operation: [ - 'get', - ], - resource, }, + default: false, + description: 'Returns a list of your estimates.', }, - description: 'The ID of the estimate you are retrieving.', -}, - -/* -------------------------------------------------------------------------- */ -/* estimate:delete */ -/* -------------------------------------------------------------------------- */ -{ - displayName: 'Estimate Id', - name: 'id', - type: 'string', - default: '', - required: true, - displayOptions: { - show: { - operation: [ - 'delete', - ], - resource, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource, + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource, + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Client ID', + name: 'client_id', + type: 'string', + default: '', + description: 'Only return time entries belonging to the client with the given ID.', + }, + { + displayName: 'From', + name: 'from', + type: 'dateTime', + default: '', + description: 'Only return time entries with a spent_date on or after the given date.', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + description: 'Only return estimates with a state matching the value provided. Options: draft, sent, accepted, or declined.', + }, + { + displayName: 'To', + name: 'to', + type: 'dateTime', + default: '', + description: 'Only return time entries with a spent_date on or before the given date.', + }, + { + displayName: 'Updated Since', + name: 'updated_since', + type: 'dateTime', + default: '', + description: 'Only return time entries that have been updated since the given date and time.', + }, + { + displayName: 'Page', + name: 'page', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 1, + description: 'The page number to use in pagination. For instance, if you make a list request and receive 100 records, your subsequent call can include page=2 to retrieve the next page of the list. (Default: 1)', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* estimate:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Estimate Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource, + }, + }, + description: 'The ID of the estimate you are retrieving.', + }, + + /* -------------------------------------------------------------------------- */ + /* estimate:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Estimate Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource, + }, + }, + description: 'The ID of the estimate want to delete.', }, - description: 'The ID of the estimate want to delete.', -}, /* -------------------------------------------------------------------------- */ /* estimate:create */ @@ -291,7 +291,7 @@ export const estimateFields = [ ], }, - /* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ /* estimate:update */ /* -------------------------------------------------------------------------- */ { diff --git a/packages/nodes-base/nodes/Harvest/ExpenseDescription.ts b/packages/nodes-base/nodes/Harvest/ExpenseDescription.ts index 0b78b10543..a32e836c4b 100644 --- a/packages/nodes-base/nodes/Harvest/ExpenseDescription.ts +++ b/packages/nodes-base/nodes/Harvest/ExpenseDescription.ts @@ -1,6 +1,6 @@ import { INodeProperties } from 'n8n-workflow'; -const resource = [ 'expense' ]; +const resource = ['expense']; export const expenseOperations = [ { @@ -47,163 +47,163 @@ export const expenseOperations = [ export const expenseFields = [ -/* -------------------------------------------------------------------------- */ -/* expense:getAll */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* expense:getAll */ + /* -------------------------------------------------------------------------- */ -{ - displayName: 'Return All', - name: 'returnAll', - type: 'boolean', - displayOptions: { - show: { - resource, - operation: [ - 'getAll', - ], - }, - }, - default: false, - description: 'Returns a list of your expenses.', -}, -{ - displayName: 'Limit', - name: 'limit', - type: 'number', - displayOptions: { - show: { - resource, - operation: [ - 'getAll', - ], - returnAll: [ - false, - ], - }, - }, - typeOptions: { - minValue: 1, - maxValue: 100, - }, - default: 100, - description: 'How many results to return.', -}, -{ - displayName: 'Filters', - name: 'filters', - type: 'collection', - placeholder: 'Add Filter', - default: {}, - displayOptions: { - show: { - resource, - operation: [ - 'getAll', - ], - }, - }, - options: [ - { - displayName: 'Client ID', - name: 'client_id', - type: 'string', - default: '', - description: 'Only return time entries belonging to the client with the given ID.', - }, - { - displayName: 'From', - name: 'from', - type: 'dateTime', - default: '', - description: 'Only return time entries with a spent_date on or after the given date.', - }, - { - displayName: 'Is Billed', - name: 'is_billed', - type: 'boolean', - default: false, - description: 'Pass true to only return time entries that have been invoiced and false to return time entries that have not been invoiced.', - }, - { - displayName: 'Page', - name: 'page', - type: 'number', - typeOptions: { - minValue: 1, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource, + operation: [ + 'getAll', + ], }, - default: 1, - description: 'The page number to use in pagination. For instance, if you make a list request and receive 100 records, your subsequent call can include page=2 to retrieve the next page of the list. (Default: 1)', - }, - { - displayName: 'Project ID', - name: 'project_id', - type: 'string', - default: '', - description: 'Only return time entries belonging to the client with the given ID.', - }, - { - displayName: 'To', - name: 'to', - type: 'dateTime', - default: '', - description: 'Only return time entries with a spent_date on or before the given date.', - }, - { - displayName: 'Updated Since', - name: 'updated_since', - type: 'dateTime', - default: '', - description: 'Only return time entries that have been updated since the given date and time.', - }, - { - displayName: 'User ID', - name: 'user_id', - type: 'string', - default: '', - description: 'Only return time entries belonging to the user with the given ID.', - }, - ], -}, - -/* -------------------------------------------------------------------------- */ -/* expense:get */ -/* -------------------------------------------------------------------------- */ -{ - displayName: 'Expense Id', - name: 'id', - type: 'string', - default: '', - required: true, - displayOptions: { - show: { - operation: [ - 'get', - ], - resource, }, + default: false, + description: 'Returns a list of your expenses.', }, - description: 'The ID of the expense you are retrieving.', -}, - -/* -------------------------------------------------------------------------- */ -/* expense:delete */ -/* -------------------------------------------------------------------------- */ -{ - displayName: 'Expense Id', - name: 'id', - type: 'string', - default: '', - required: true, - displayOptions: { - show: { - operation: [ - 'delete', - ], - resource, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource, + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource, + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Client ID', + name: 'client_id', + type: 'string', + default: '', + description: 'Only return time entries belonging to the client with the given ID.', + }, + { + displayName: 'From', + name: 'from', + type: 'dateTime', + default: '', + description: 'Only return time entries with a spent_date on or after the given date.', + }, + { + displayName: 'Is Billed', + name: 'is_billed', + type: 'boolean', + default: false, + description: 'Pass true to only return time entries that have been invoiced and false to return time entries that have not been invoiced.', + }, + { + displayName: 'Page', + name: 'page', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 1, + description: 'The page number to use in pagination. For instance, if you make a list request and receive 100 records, your subsequent call can include page=2 to retrieve the next page of the list. (Default: 1)', + }, + { + displayName: 'Project ID', + name: 'project_id', + type: 'string', + default: '', + description: 'Only return time entries belonging to the client with the given ID.', + }, + { + displayName: 'To', + name: 'to', + type: 'dateTime', + default: '', + description: 'Only return time entries with a spent_date on or before the given date.', + }, + { + displayName: 'Updated Since', + name: 'updated_since', + type: 'dateTime', + default: '', + description: 'Only return time entries that have been updated since the given date and time.', + }, + { + displayName: 'User ID', + name: 'user_id', + type: 'string', + default: '', + description: 'Only return time entries belonging to the user with the given ID.', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* expense:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Expense Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource, + }, + }, + description: 'The ID of the expense you are retrieving.', + }, + + /* -------------------------------------------------------------------------- */ + /* expense:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Expense Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource, + }, + }, + description: 'The ID of the expense you want to delete.', }, - description: 'The ID of the expense you want to delete.', -}, /* -------------------------------------------------------------------------- */ /* expense:create */ @@ -309,7 +309,7 @@ export const expenseFields = [ ], }, - /* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ /* invoice:update */ /* -------------------------------------------------------------------------- */ { diff --git a/packages/nodes-base/nodes/Harvest/GenericFunctions.ts b/packages/nodes-base/nodes/Harvest/GenericFunctions.ts index 2f9cec0738..fd5ed8178c 100644 --- a/packages/nodes-base/nodes/Harvest/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Harvest/GenericFunctions.ts @@ -1,60 +1,53 @@ -import { OptionsWithUri } from 'request'; +import { + OptionsWithUri, +} from 'request'; + import { IExecuteFunctions, IExecuteSingleFunctions, IHookFunctions, ILoadOptionsFunctions, } from 'n8n-core'; -import { IDataObject } from 'n8n-workflow'; -export async function harvestApiRequest( - this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, - method: string, - qs: IDataObject = {}, - uri: string, - body: IDataObject = {}, - option: IDataObject = {}, - ): Promise { // tslint:disable-line:no-any - - const credentials = this.getCredentials('harvestApi') as IDataObject; - - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } - - qs.access_token = credentials.accessToken; - qs.account_id = credentials.accountId; - // Convert to query string into a format the API can read - const queryStringElements: string[] = []; - for (const key of Object.keys(qs)) { - if (Array.isArray(qs[key])) { - (qs[key] as string[]).forEach(value => { - queryStringElements.push(`${key}=${value}`); - }); - } else { - queryStringElements.push(`${key}=${qs[key]}`); - } - } +import { + IDataObject +} from 'n8n-workflow'; +export async function harvestApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, qs: IDataObject = {}, path: string, body: IDataObject = {}, option: IDataObject = {}, uri?: string): Promise { // tslint:disable-line:no-any let options: OptionsWithUri = { + headers: { + 'Harvest-Account-Id': `${this.getNodeParameter('accountId', 0)}`, + 'User-Agent': 'Harvest App', + 'Authorization': '', + }, method, body, - uri: `https://api.harvestapp.com/v2/${uri}?${queryStringElements.join('&')}`, + uri: uri || `https://api.harvestapp.com/v2/${path}`, + qs, json: true, - headers: { - "User-Agent": "Harvest API", - }, }; options = Object.assign({}, options, option); if (Object.keys(options.body).length === 0) { delete options.body; } + const authenticationMethod = this.getNodeParameter('authentication', 0); try { - const result = await this.helpers.request!(options); + if (authenticationMethod === 'accessToken') { + const credentials = this.getCredentials('harvestApi') as IDataObject; - return result; + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + //@ts-ignore + options.headers['Authorization'] = `Bearer ${credentials.accessToken}`; + + return await this.helpers.request!(options); + } else { + return await this.helpers.requestOAuth2!.call(this, 'harvestOAuth2Api', options); + } } catch (error) { if (error.statusCode === 401) { // Return a clear error @@ -76,27 +69,51 @@ export async function harvestApiRequest( * and return all results */ export async function harvestApiRequestAllItems( - this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, - method: string, - qs: IDataObject = {}, - uri: string, - resource: string, - body: IDataObject = {}, - option: IDataObject = {}, - ): Promise { // tslint:disable-line:no-any + this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, + method: string, + qs: IDataObject = {}, + uri: string, + resource: string, + body: IDataObject = {}, + option: IDataObject = {}, +): Promise { // tslint:disable-line:no-any const returnData: IDataObject[] = []; let responseData; - try { - do { - responseData = await harvestApiRequest.call(this, method, qs, uri, body, option); - qs.page = responseData.next_page; - returnData.push.apply(returnData, responseData[resource]); - } while (responseData.next_page); - return returnData; - } catch(error) { - throw error; - } + do { + responseData = await harvestApiRequest.call(this, method, qs, uri, body, option); + qs.page = responseData.next_page; + returnData.push.apply(returnData, responseData[resource]); + } while (responseData.next_page); + + return returnData; } + +/** + * fetch All resource using paginated calls + */ +export async function getAllResource(this: IExecuteFunctions | ILoadOptionsFunctions, resource: string, i: number) { + const endpoint = resource; + const qs: IDataObject = {}; + const requestMethod = 'GET'; + + qs.per_page = 100; + + const additionalFields = this.getNodeParameter('filters', i) as IDataObject; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + Object.assign(qs, additionalFields); + + let responseData: IDataObject = {}; + if (returnAll) { + responseData[resource] = await harvestApiRequestAllItems.call(this, requestMethod, qs, endpoint, resource); + } else { + const limit = this.getNodeParameter('limit', i) as string; + qs.per_page = limit; + responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint); + } + return responseData[resource] as IDataObject[]; +} + diff --git a/packages/nodes-base/nodes/Harvest/Harvest.node.ts b/packages/nodes-base/nodes/Harvest/Harvest.node.ts index c41bc400e3..b7edc1a7d5 100644 --- a/packages/nodes-base/nodes/Harvest/Harvest.node.ts +++ b/packages/nodes-base/nodes/Harvest/Harvest.node.ts @@ -1,9 +1,12 @@ import { IExecuteFunctions, + ILoadOptionsFunctions, } from 'n8n-core'; + import { IDataObject, INodeExecutionData, + INodePropertyOptions, INodeType, INodeTypeDescription, } from 'n8n-workflow'; @@ -12,31 +15,41 @@ import { clientFields, clientOperations, } from './ClientDescription'; + import { contactFields, contactOperations, } from './ContactDescription'; -import { companyOperations } from './CompanyDescription'; + +import { + companyOperations, +} from './CompanyDescription'; + import { estimateFields, estimateOperations, } from './EstimateDescription'; + import { expenseFields, expenseOperations, } from './ExpenseDescription'; + import { + getAllResource, harvestApiRequest, - harvestApiRequestAllItems, } from './GenericFunctions'; + import { invoiceFields, invoiceOperations, } from './InvoiceDescription'; + import { projectFields, projectOperations, } from './ProjectDescription'; + import { taskFields, taskOperations, @@ -45,36 +58,12 @@ import { timeEntryFields, timeEntryOperations, } from './TimeEntryDescription'; + import { userFields, userOperations, } from './UserDescription'; -/** - * fetch All resource using paginated calls - */ -async function getAllResource(this: IExecuteFunctions, resource: string, i: number) { - const endpoint = resource; - const qs: IDataObject = {}; - const requestMethod = 'GET'; - - qs.per_page = 100; - - const additionalFields = this.getNodeParameter('filters', i) as IDataObject; - const returnAll = this.getNodeParameter('returnAll', i) as boolean; - Object.assign(qs, additionalFields); - - let responseData: IDataObject = {}; - if (returnAll) { - responseData[resource] = await harvestApiRequestAllItems.call(this, requestMethod, qs, endpoint, resource); - } else { - const limit = this.getNodeParameter('limit', i) as string; - qs.per_page = limit; - responseData = await harvestApiRequest.call(this, requestMethod, qs, endpoint); - } - return responseData[resource] as IDataObject[]; -} - export class Harvest implements INodeType { description: INodeTypeDescription = { displayName: 'Harvest', @@ -86,7 +75,7 @@ export class Harvest implements INodeType { description: 'Access data on Harvest', defaults: { name: 'Harvest', - color: '#22BB44', + color: '#e7863f', }, inputs: ['main'], outputs: ['main'], @@ -94,9 +83,46 @@ export class Harvest implements INodeType { { name: 'harvestApi', required: true, + displayOptions: { + show: { + authentication: [ + 'accessToken', + ], + }, + }, + }, + { + name: 'harvestOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, }, ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'Access Token', + value: 'accessToken', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'accessToken', + description: 'Method of authentication.', + }, + + { displayName: 'Resource', name: 'resource', @@ -148,6 +174,17 @@ export class Harvest implements INodeType { description: 'The resource to operate on.', }, + { + displayName: 'Account ID', + name: 'accountId', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getAccounts', + }, + default: '', + }, + // operations ...clientOperations, ...companyOperations, @@ -173,6 +210,26 @@ export class Harvest implements INodeType { ], }; + methods = { + loadOptions: { + // Get all the available accounts to display them to user so that he can + // select them easily + async getAccounts(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const { accounts } = await harvestApiRequest.call(this, 'GET', {}, '', {}, {}, 'https://id.getharvest.com/api/v2/accounts'); + for (const account of accounts) { + const accountName = account.name; + const accountId = account.id; + returnData.push({ + name: accountName, + value: accountId, + }); + } + return returnData; + }, + }, + }; + async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: IDataObject[] = []; diff --git a/packages/nodes-base/nodes/Harvest/InvoiceDescription.ts b/packages/nodes-base/nodes/Harvest/InvoiceDescription.ts index 83c457752b..4778507a1a 100644 --- a/packages/nodes-base/nodes/Harvest/InvoiceDescription.ts +++ b/packages/nodes-base/nodes/Harvest/InvoiceDescription.ts @@ -1,6 +1,6 @@ import { INodeProperties } from 'n8n-workflow'; -const resource = [ 'invoice' ]; +const resource = ['invoice']; export const invoiceOperations = [ { @@ -47,174 +47,174 @@ export const invoiceOperations = [ export const invoiceFields = [ -/* -------------------------------------------------------------------------- */ -/* invoice:getAll */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* invoice:getAll */ + /* -------------------------------------------------------------------------- */ -{ - displayName: 'Return All', - name: 'returnAll', - type: 'boolean', - displayOptions: { - show: { - resource, - operation: [ - 'getAll', - ], - }, - }, - default: false, - description: 'Returns a list of your invoices.', -}, -{ - displayName: 'Limit', - name: 'limit', - type: 'number', - displayOptions: { - show: { - resource, - operation: [ - 'getAll', - ], - returnAll: [ - false, - ], - }, - }, - typeOptions: { - minValue: 1, - maxValue: 100, - }, - default: 100, - description: 'How many results to return.', -}, -{ - displayName: 'Filters', - name: 'filters', - type: 'collection', - placeholder: 'Add Filter', - default: {}, - displayOptions: { - show: { - resource, - operation: [ - 'getAll', - ], - }, - }, - options: [ - { - displayName: 'Client ID', - name: 'client_id', - type: 'string', - default: '', - description: 'Only return time entries belonging to the client with the given ID.', - }, - { - displayName: 'Project ID', - name: 'project_id', - type: 'string', - default: '', - description: 'Only return time entries belonging to the client with the given ID.', - }, - { - displayName: 'Updated Since', - name: 'updated_since', - type: 'dateTime', - default: '', - description: 'Only return time entries that have been updated since the given date and time.', - }, - { - displayName: 'From', - name: 'from', - type: 'dateTime', - default: '', - description: 'Only return time entries with a spent_date on or after the given date.', - }, - { - displayName: 'To', - name: 'to', - type: 'dateTime', - default: '', - description: 'Only return time entries with a spent_date on or before the given date.', - }, - { - displayName: 'State', - name: 'state', - type: 'multiOptions', - options: [ - { - name: 'draft', - value: 'draft', - }, - { - name: 'open', - value: 'open', - }, - { - name: 'paid', - value: 'paid', - }, - { - name: 'closed', - value: 'closed', - }, - ], - default: [], - description: 'Only return invoices with a state matching the value provided. Options: draft, open, paid, or closed.', - }, - { - displayName: 'Page', - name: 'page', - type: 'number', - typeOptions: { - minValue: 1, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource, + operation: [ + 'getAll', + ], }, - default: 1, - description: 'The page number to use in pagination. For instance, if you make a list request and receive 100 records, your subsequent call can include page=2 to retrieve the next page of the list. (Default: 1)', - }, - ], -}, - -/* -------------------------------------------------------------------------- */ -/* invoice:get */ -/* -------------------------------------------------------------------------- */ -{ - displayName: 'Invoice Id', - name: 'id', - type: 'string', - default: '', - required: true, - displayOptions: { - show: { - operation: [ - 'get', - ], - resource, }, + default: false, + description: 'Returns a list of your invoices.', }, - description: 'The ID of the invoice you are retrieving.', -}, - -/* -------------------------------------------------------------------------- */ -/* invoice:delete */ -/* -------------------------------------------------------------------------- */ -{ - displayName: 'Invoice Id', - name: 'id', - type: 'string', - default: '', - required: true, - displayOptions: { - show: { - operation: [ - 'delete', - ], - resource, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource, + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource, + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Client ID', + name: 'client_id', + type: 'string', + default: '', + description: 'Only return time entries belonging to the client with the given ID.', + }, + { + displayName: 'Project ID', + name: 'project_id', + type: 'string', + default: '', + description: 'Only return time entries belonging to the client with the given ID.', + }, + { + displayName: 'Updated Since', + name: 'updated_since', + type: 'dateTime', + default: '', + description: 'Only return time entries that have been updated since the given date and time.', + }, + { + displayName: 'From', + name: 'from', + type: 'dateTime', + default: '', + description: 'Only return time entries with a spent_date on or after the given date.', + }, + { + displayName: 'To', + name: 'to', + type: 'dateTime', + default: '', + description: 'Only return time entries with a spent_date on or before the given date.', + }, + { + displayName: 'State', + name: 'state', + type: 'multiOptions', + options: [ + { + name: 'draft', + value: 'draft', + }, + { + name: 'open', + value: 'open', + }, + { + name: 'paid', + value: 'paid', + }, + { + name: 'closed', + value: 'closed', + }, + ], + default: [], + description: 'Only return invoices with a state matching the value provided. Options: draft, open, paid, or closed.', + }, + { + displayName: 'Page', + name: 'page', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 1, + description: 'The page number to use in pagination. For instance, if you make a list request and receive 100 records, your subsequent call can include page=2 to retrieve the next page of the list. (Default: 1)', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* invoice:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Invoice Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource, + }, + }, + description: 'The ID of the invoice you are retrieving.', + }, + + /* -------------------------------------------------------------------------- */ + /* invoice:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Invoice Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource, + }, + }, + description: 'The ID of the invoice want to delete.', }, - description: 'The ID of the invoice want to delete.', -}, /* -------------------------------------------------------------------------- */ /* invoice:create */ @@ -344,7 +344,7 @@ export const invoiceFields = [ ], }, - /* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ /* invoice:update */ /* -------------------------------------------------------------------------- */ { diff --git a/packages/nodes-base/nodes/Harvest/TaskDescription.ts b/packages/nodes-base/nodes/Harvest/TaskDescription.ts index 2be01d5fe9..a4926d70d1 100644 --- a/packages/nodes-base/nodes/Harvest/TaskDescription.ts +++ b/packages/nodes-base/nodes/Harvest/TaskDescription.ts @@ -48,7 +48,6 @@ export const taskFields = [ /* -------------------------------------------------------------------------- */ /* task:getAll */ /* -------------------------------------------------------------------------- */ - { displayName: 'Return All', name: 'returnAll', diff --git a/packages/nodes-base/nodes/Harvest/TimeEntryDescription.ts b/packages/nodes-base/nodes/Harvest/TimeEntryDescription.ts index be6d6d02b9..b4c40e8ce9 100644 --- a/packages/nodes-base/nodes/Harvest/TimeEntryDescription.ts +++ b/packages/nodes-base/nodes/Harvest/TimeEntryDescription.ts @@ -1,5 +1,5 @@ import { INodeProperties } from 'n8n-workflow'; -export const resource = [ 'timeEntry' ]; +export const resource = ['timeEntry']; export const timeEntryOperations = [ { displayName: 'Operation', @@ -63,485 +63,485 @@ export const timeEntryOperations = [ ] as INodeProperties[]; export const timeEntryFields = [ -/* -------------------------------------------------------------------------- */ -/* timeEntry:getAll */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* timeEntry:getAll */ + /* -------------------------------------------------------------------------- */ -{ - displayName: 'Return All', - name: 'returnAll', - type: 'boolean', - displayOptions: { - show: { - resource, - operation: [ - 'getAll', - ], - }, - }, - default: false, - description: 'Returns a list of your time entries.', -}, -{ - displayName: 'Limit', - name: 'limit', - type: 'number', - displayOptions: { - show: { - resource, - operation: [ - 'getAll', - ], - returnAll: [ - false, - ], - }, - }, - typeOptions: { - minValue: 1, - maxValue: 100, - }, - default: 100, - description: 'How many results to return.', -}, -{ - displayName: 'Filters', - name: 'filters', - type: 'collection', - placeholder: 'Add Filter', - default: {}, - displayOptions: { - show: { - resource, - operation: [ - 'getAll', - ], - }, - }, - options: [ - { - displayName: 'Client ID', - name: 'client_id', - type: 'string', - default: '', - description: 'Only return time entries belonging to the client with the given ID.', - }, - { - displayName: 'From', - name: 'from', - type: 'dateTime', - default: '', - description: 'Only return time entries with a spent_date on or after the given date.', - }, - { - displayName: 'Is Billed', - name: 'is_billed', - type: 'boolean', - default: true, - description: 'Pass true to only return time entries that have been invoiced and false to return time entries that have not been invoiced.', - }, - { - displayName: 'Is Running', - name: 'is_running', - type: 'boolean', - default: true, - description: 'Pass true to only return running time entries and false to return non-running time entries.', - }, - { - displayName: 'To', - name: 'to', - type: 'dateTime', - default: '', - description: 'Only return time entries with a spent_date on or before the given date.', - }, - { - displayName: 'Updated Since', - name: 'updated_since', - type: 'dateTime', - default: '', - description: 'Only return time entries that have been updated since the given date and time.', - }, - { - displayName: 'Page', - name: 'page', - type: 'number', - typeOptions: { - minValue: 1, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource, + operation: [ + 'getAll', + ], }, - default: 1, - description: 'The page number to use in pagination. For instance, if you make a list request and receive 100 records, your subsequent call can include page=2 to retrieve the next page of the list. (Default: 1)', - }, - { - displayName: 'User ID', - name: 'user_id', - type: 'string', - default: '', - description: 'Only return time entries belonging to the user with the given ID.', - }, - ], -}, - -/* -------------------------------------------------------------------------- */ -/* timeEntry:get */ -/* -------------------------------------------------------------------------- */ -{ - displayName: 'Time Entry Id', - name: 'id', - type: 'string', - default: '', - required: true, - displayOptions: { - show: { - operation: [ - 'get', - ], - resource, }, + default: false, + description: 'Returns a list of your time entries.', }, - description: 'The ID of the time entry you are retrieving.', -}, - -/* -------------------------------------------------------------------------- */ -/* timeEntry:delete */ -/* -------------------------------------------------------------------------- */ -{ - displayName: 'Time Entry Id', - name: 'id', - type: 'string', - default: '', - required: true, - displayOptions: { - show: { - operation: [ - 'delete', - ], - resource, - }, - }, - description: 'The ID of the time entry you are deleting.', -}, - -/* -------------------------------------------------------------------------- */ -/* timeEntry:deleteExternal */ -/* -------------------------------------------------------------------------- */ -{ - displayName: 'Time Entry Id', - name: 'id', - type: 'string', - default: '', - required: true, - displayOptions: { - show: { - operation: [ - 'deleteExternal', - ], - resource, - }, - }, - description: 'The ID of the time entry whose external reference you are deleting.', -}, - -/* -------------------------------------------------------------------------- */ -/* timeEntry:stopTime */ -/* -------------------------------------------------------------------------- */ -{ - displayName: 'Time Entry Id', - name: 'id', - type: 'string', - default: '', - required: true, - displayOptions: { - show: { - operation: [ - 'stopTime', - ], - resource, - }, - }, - description: 'Stop a running time entry. Stopping a time entry is only possible if it’s currently running.', -}, - -/* -------------------------------------------------------------------------- */ -/* timeEntry:restartTime */ -/* -------------------------------------------------------------------------- */ -{ - displayName: 'Time Entry Id', - name: 'id', - type: 'string', - default: '', - required: true, - displayOptions: { - show: { - operation: [ - 'restartTime', - ], - resource, - }, - }, - description: 'Restart a stopped time entry. Restarting a time entry is only possible if it isn’t currently running.', -}, - -/* -------------------------------------------------------------------------- */ -/* timeEntry:update */ -/* -------------------------------------------------------------------------- */ -{ - displayName: 'Time Entry Id', - name: 'id', - type: 'string', - default: '', - required: true, - displayOptions: { - show: { - operation: [ - 'update', - ], - resource, - }, - }, - description: 'The ID of the time entry to update.', -}, -{ - displayName: 'Update Fields', - name: 'updateFields', - type: 'collection', - placeholder: 'Add Field', - displayOptions: { - show: { - operation: [ - 'update', - ], - resource, - }, - }, - default: {}, - options: [ - { - displayName: 'Ended Time', - name: 'ended_time', - type: 'string', - default: '', - placeholder: '3:00pm', - description: 'The time the entry ended.', - }, - { - displayName: 'Hours', - name: 'hours', - type: 'number', - typeOptions: { - minValue: 0, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource, + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], }, - default: 0, - description: 'The current amount of time tracked.', }, - { - displayName: 'Notes', - name: 'notes', - type: 'string', - default: '', - description: 'These are notes about the time entry..', - }, - { - displayName: 'Started Time', - name: 'started_time', - type: 'string', - default: '', - placeholder: '3:00pm', - description: 'The time the entry started. Defaults to the current time. Example: “8:00am”.', - }, - ], -}, - -/* -------------------------------------------------------------------------- */ -/* timeEntry:createByDuration */ -/* -------------------------------------------------------------------------- */ -{ - displayName: 'Project Id', - name: 'projectId', - type: 'string', - displayOptions: { - show: { - operation: [ - 'createByDuration', - ], - resource, + typeOptions: { + minValue: 1, + maxValue: 100, }, + default: 100, + description: 'How many results to return.', }, - default: '', - required: true, - description: 'The ID of the project to associate with the time entry.', -}, -{ - displayName: 'Task Id', - name: 'taskId', - type: 'string', - displayOptions: { - show: { - operation: [ - 'createByDuration', - ], - resource, - }, - }, - default: '', - required: true, - description: 'The ID of the task to associate with the time entry.', -}, -{ - displayName: 'Spent Date', - name: 'spentDate', - type: 'dateTime', - displayOptions: { - show: { - operation: [ - 'createByDuration', - ], - resource, - }, - }, - default: '', - required: true, - description: 'The ISO 8601 formatted date the time entry was spent.', -}, -{ - displayName: 'Additional Fields', - name: 'additionalFields', - type: 'collection', - placeholder: 'Add Field', - displayOptions: { - show: { - operation: [ - 'createByDuration', - ], - resource, - }, - }, - default: {}, - options: [ - { - displayName: 'Hours', - name: 'hours', - type: 'number', - typeOptions: { - minValue: 0, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource, + operation: [ + 'getAll', + ], }, - default: 0, - description: 'The current amount of time tracked.', }, - { - displayName: 'Notes', - name: 'notes', - type: 'string', - default: '', - description: 'These are notes about the time entry..', - }, - { - displayName: 'User ID', - name: 'user_id', - type: 'string', - default: '', - description: 'The ID of the user to associate with the time entry. Defaults to the currently authenticated user’s ID.', - }, - ], -}, + options: [ + { + displayName: 'Client ID', + name: 'client_id', + type: 'string', + default: '', + description: 'Only return time entries belonging to the client with the given ID.', + }, + { + displayName: 'From', + name: 'from', + type: 'dateTime', + default: '', + description: 'Only return time entries with a spent_date on or after the given date.', + }, + { + displayName: 'Is Billed', + name: 'is_billed', + type: 'boolean', + default: true, + description: 'Pass true to only return time entries that have been invoiced and false to return time entries that have not been invoiced.', + }, + { + displayName: 'Is Running', + name: 'is_running', + type: 'boolean', + default: true, + description: 'Pass true to only return running time entries and false to return non-running time entries.', + }, + { + displayName: 'To', + name: 'to', + type: 'dateTime', + default: '', + description: 'Only return time entries with a spent_date on or before the given date.', + }, + { + displayName: 'Updated Since', + name: 'updated_since', + type: 'dateTime', + default: '', + description: 'Only return time entries that have been updated since the given date and time.', + }, + { + displayName: 'Page', + name: 'page', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 1, + description: 'The page number to use in pagination. For instance, if you make a list request and receive 100 records, your subsequent call can include page=2 to retrieve the next page of the list. (Default: 1)', + }, + { + displayName: 'User ID', + name: 'user_id', + type: 'string', + default: '', + description: 'Only return time entries belonging to the user with the given ID.', + }, + ], + }, -/* -------------------------------------------------------------------------- */ -/* timeEntry:createByStartEnd */ -/* -------------------------------------------------------------------------- */ -{ - displayName: 'Project Id', - name: 'projectId', - type: 'string', - displayOptions: { - show: { - operation: [ - 'createByStartEnd', - ], - resource, + /* -------------------------------------------------------------------------- */ + /* timeEntry:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Time Entry Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + resource, + }, }, + description: 'The ID of the time entry you are retrieving.', }, - default: '', - required: true, - description: 'The ID of the project to associate with the time entry.', -}, -{ - displayName: 'Task Id', - name: 'taskId', - type: 'string', - displayOptions: { - show: { - operation: [ - 'createByStartEnd', - ], - resource, + + /* -------------------------------------------------------------------------- */ + /* timeEntry:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Time Entry Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'delete', + ], + resource, + }, }, + description: 'The ID of the time entry you are deleting.', }, - default: '', - required: true, - description: 'The ID of the task to associate with the time entry.', -}, -{ - displayName: 'Spent Date', - name: 'spentDate', - type: 'dateTime', - displayOptions: { - show: { - operation: [ - 'createByStartEnd', - ], - resource, + + /* -------------------------------------------------------------------------- */ + /* timeEntry:deleteExternal */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Time Entry Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'deleteExternal', + ], + resource, + }, }, + description: 'The ID of the time entry whose external reference you are deleting.', }, - default: '', - required: true, - description: 'The ISO 8601 formatted date the time entry was spent.', -}, -{ - displayName: 'Additional Fields', - name: 'additionalFields', - type: 'collection', - placeholder: 'Add Field', - displayOptions: { - show: { - operation: [ - 'createByStartEnd', - ], - resource, + + /* -------------------------------------------------------------------------- */ + /* timeEntry:stopTime */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Time Entry Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'stopTime', + ], + resource, + }, }, + description: 'Stop a running time entry. Stopping a time entry is only possible if it’s currently running.', }, - default: {}, - options: [ - { - displayName: 'Ended Time', - name: 'ended_time', - type: 'string', - default: '', - placeholder: '3:00pm', - description: 'The time the entry ended.', + + /* -------------------------------------------------------------------------- */ + /* timeEntry:restartTime */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Time Entry Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'restartTime', + ], + resource, + }, }, - { - displayName: 'Notes', - name: 'notes', - type: 'string', - default: '', - description: 'These are notes about the time entry..', + description: 'Restart a stopped time entry. Restarting a time entry is only possible if it isn’t currently running.', + }, + + /* -------------------------------------------------------------------------- */ + /* timeEntry:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Time Entry Id', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource, + }, }, - { - displayName: 'Started Time', - name: 'started_time', - type: 'string', - default: '', - placeholder: '8:00am', - description: 'The time the entry started. Defaults to the current time. Example: “8:00am”.', + description: 'The ID of the time entry to update.', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource, + }, }, - { - displayName: 'User ID', - name: 'user_id', - type: 'string', - default: '', - description: 'The ID of the user to associate with the time entry. Defaults to the currently authenticated user’s ID.', + default: {}, + options: [ + { + displayName: 'Ended Time', + name: 'ended_time', + type: 'string', + default: '', + placeholder: '3:00pm', + description: 'The time the entry ended.', + }, + { + displayName: 'Hours', + name: 'hours', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + description: 'The current amount of time tracked.', + }, + { + displayName: 'Notes', + name: 'notes', + type: 'string', + default: '', + description: 'These are notes about the time entry..', + }, + { + displayName: 'Started Time', + name: 'started_time', + type: 'string', + default: '', + placeholder: '3:00pm', + description: 'The time the entry started. Defaults to the current time. Example: “8:00am”.', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* timeEntry:createByDuration */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Project Id', + name: 'projectId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'createByDuration', + ], + resource, + }, }, - ], -}, + default: '', + required: true, + description: 'The ID of the project to associate with the time entry.', + }, + { + displayName: 'Task Id', + name: 'taskId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'createByDuration', + ], + resource, + }, + }, + default: '', + required: true, + description: 'The ID of the task to associate with the time entry.', + }, + { + displayName: 'Spent Date', + name: 'spentDate', + type: 'dateTime', + displayOptions: { + show: { + operation: [ + 'createByDuration', + ], + resource, + }, + }, + default: '', + required: true, + description: 'The ISO 8601 formatted date the time entry was spent.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'createByDuration', + ], + resource, + }, + }, + default: {}, + options: [ + { + displayName: 'Hours', + name: 'hours', + type: 'number', + typeOptions: { + minValue: 0, + }, + default: 0, + description: 'The current amount of time tracked.', + }, + { + displayName: 'Notes', + name: 'notes', + type: 'string', + default: '', + description: 'These are notes about the time entry..', + }, + { + displayName: 'User ID', + name: 'user_id', + type: 'string', + default: '', + description: 'The ID of the user to associate with the time entry. Defaults to the currently authenticated user’s ID.', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* timeEntry:createByStartEnd */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Project Id', + name: 'projectId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'createByStartEnd', + ], + resource, + }, + }, + default: '', + required: true, + description: 'The ID of the project to associate with the time entry.', + }, + { + displayName: 'Task Id', + name: 'taskId', + type: 'string', + displayOptions: { + show: { + operation: [ + 'createByStartEnd', + ], + resource, + }, + }, + default: '', + required: true, + description: 'The ID of the task to associate with the time entry.', + }, + { + displayName: 'Spent Date', + name: 'spentDate', + type: 'dateTime', + displayOptions: { + show: { + operation: [ + 'createByStartEnd', + ], + resource, + }, + }, + default: '', + required: true, + description: 'The ISO 8601 formatted date the time entry was spent.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'createByStartEnd', + ], + resource, + }, + }, + default: {}, + options: [ + { + displayName: 'Ended Time', + name: 'ended_time', + type: 'string', + default: '', + placeholder: '3:00pm', + description: 'The time the entry ended.', + }, + { + displayName: 'Notes', + name: 'notes', + type: 'string', + default: '', + description: 'These are notes about the time entry..', + }, + { + displayName: 'Started Time', + name: 'started_time', + type: 'string', + default: '', + placeholder: '8:00am', + description: 'The time the entry started. Defaults to the current time. Example: “8:00am”.', + }, + { + displayName: 'User ID', + name: 'user_id', + type: 'string', + default: '', + description: 'The ID of the user to associate with the time entry. Defaults to the currently authenticated user’s ID.', + }, + ], + }, diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index b08d3ac0f3..e2c4f39c14 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -95,6 +95,7 @@ "dist/credentials/YouTubeOAuth2Api.credentials.js", "dist/credentials/GumroadApi.credentials.js", "dist/credentials/HarvestApi.credentials.js", + "dist/credentials/HarvestOAuth2Api.credentials.js", "dist/credentials/HelpScoutOAuth2Api.credentials.js", "dist/credentials/HttpBasicAuth.credentials.js", "dist/credentials/HttpDigestAuth.credentials.js",