From c06c5d4796043123694e507b4c3af2fb34f62227 Mon Sep 17 00:00:00 2001 From: Stams89 Date: Sat, 14 Sep 2024 12:40:11 +0300 Subject: [PATCH 1/3] get custom fields --- .../v2/description/ContactDescription.ts | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/packages/nodes-base/nodes/HighLevel/v2/description/ContactDescription.ts b/packages/nodes-base/nodes/HighLevel/v2/description/ContactDescription.ts index b60833eb71575..4f794eb55d171 100644 --- a/packages/nodes-base/nodes/HighLevel/v2/description/ContactDescription.ts +++ b/packages/nodes-base/nodes/HighLevel/v2/description/ContactDescription.ts @@ -168,12 +168,12 @@ const customFields: INodeProperties = { required: true, default: '', description: - 'Choose from the list, or specify an ID using an expression', + 'To load the options location should be set.Choose from the list, or specify an ID using an expression', typeOptions: { loadOptions: { routing: { request: { - url: '/custom-fields', + url: '=/locations/{{$parameter.locationId}}/customFields?model=contact', method: 'GET', }, output: { @@ -201,6 +201,7 @@ const customFields: INodeProperties = { }, }, }, + loadOptionsDependsOn: ['locationId'], }, }, { @@ -222,6 +223,25 @@ const customFields: INodeProperties = { }; const createProperties: INodeProperties[] = [ + { + displayName: 'Location ID', + name: 'locationId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: ['contact'], + operation: ['create'], + }, + }, + default: '', + routing: { + send: { + type: 'body', + property: 'locationId', + }, + }, + }, { displayName: 'Email', name: 'email', @@ -390,6 +410,19 @@ const createProperties: INodeProperties[] = [ }, }, }, + //TODO not supported + // { + // displayName: 'Note', + // name: 'notes', + // type: 'string', + // default: '', + // routing: { + // send: { + // type: 'body', + // property: 'notes', + // }, + // }, + // }, { displayName: 'Tags', name: 'tags', @@ -451,6 +484,18 @@ const updateProperties: INodeProperties[] = [ }, default: '', }, + { + displayName: 'Location ID', + name: 'locationId', + type: 'string', + displayOptions: { + show: { + resource: ['contact'], + operation: ['update'], + }, + }, + default: '', + }, { displayName: 'Update Fields', name: 'updateFields', @@ -613,7 +658,7 @@ const updateProperties: INodeProperties[] = [ type: 'options', default: '', description: - 'Choose from the list, or specify an ID using an expression', + 'To load the options location should be set. Choose from the list, or specify an ID using an expression.', typeOptions: { loadOptionsMethod: 'getTimezones', }, From fecad05588eb343207dba6c11805b33c90144166 Mon Sep 17 00:00:00 2001 From: Stams89 Date: Sat, 14 Sep 2024 12:42:06 +0300 Subject: [PATCH 2/3] fix timezone issue --- .../nodes/HighLevel/v2/GenericFunctions.ts | 52 ++++++++++++------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/packages/nodes-base/nodes/HighLevel/v2/GenericFunctions.ts b/packages/nodes-base/nodes/HighLevel/v2/GenericFunctions.ts index 04a92c512c6fe..9c8e9d0e8b54b 100644 --- a/packages/nodes-base/nodes/HighLevel/v2/GenericFunctions.ts +++ b/packages/nodes-base/nodes/HighLevel/v2/GenericFunctions.ts @@ -14,7 +14,7 @@ import type { IPollFunctions, IWebhookFunctions, } from 'n8n-workflow'; -import { NodeApiError } from 'n8n-workflow'; +import { ApplicationError, NodeApiError } from 'n8n-workflow'; import type { ToISOTimeOptions } from 'luxon'; import { DateTime } from 'luxon'; @@ -64,7 +64,7 @@ export async function dueDatePreSendAction( ); } const dueDate = dateToIsoSupressMillis(dueDateParam); - requestOptions.body = (requestOptions.body || {}) as object; + requestOptions.body = (requestOptions.body ?? {}) as object; Object.assign(requestOptions.body, { dueDate }); return requestOptions; } @@ -73,7 +73,7 @@ export async function contactIdentifierPreSendAction( this: IExecuteSingleFunctions, requestOptions: IHttpRequestOptions, ): Promise { - requestOptions.body = (requestOptions.body || {}) as object; + requestOptions.body = (requestOptions.body ?? {}) as object; let identifier = this.getNodeParameter('contactIdentifier', null) as string; if (!identifier) { const fields = this.getNodeParameter('updateFields') as { contactIdentifier: string }; @@ -93,7 +93,7 @@ export async function validEmailAndPhonePreSendAction( this: IExecuteSingleFunctions, requestOptions: IHttpRequestOptions, ): Promise { - const body = (requestOptions.body || {}) as { email?: string; phone?: string }; + const body = (requestOptions.body ?? {}) as { email?: string; phone?: string }; if (body.email && !isEmailValid(body.email)) { const message = `email "${body.email}" has invalid format`; @@ -112,7 +112,7 @@ export async function dateTimeToEpochPreSendAction( this: IExecuteSingleFunctions, requestOptions: IHttpRequestOptions, ): Promise { - const qs = (requestOptions.qs || {}) as { + const qs = (requestOptions.qs ?? {}) as { startDate?: string | number; endDate?: string | number; }; @@ -134,22 +134,22 @@ export async function addLocationIdPreSendAction( if (resource === 'contact') { if (operation === 'getAll') { - requestOptions.qs = requestOptions.qs || {}; + requestOptions.qs = requestOptions.qs ?? {}; Object.assign(requestOptions.qs, { locationId }); } if (operation === 'create') { - requestOptions.body = requestOptions.body || {}; + requestOptions.body = requestOptions.body ?? {}; Object.assign(requestOptions.body, { locationId }); } } if (resource === 'opportunity') { if (operation === 'create') { - requestOptions.body = requestOptions.body || {}; + requestOptions.body = requestOptions.body ?? {}; Object.assign(requestOptions.body, { locationId }); } if (operation === 'getAll') { - requestOptions.qs = requestOptions.qs || {}; + requestOptions.qs = requestOptions.qs ?? {}; Object.assign(requestOptions.qs, { location_id: locationId }); } } @@ -180,7 +180,7 @@ export async function highLevelApiRequest( method, body, qs, - url: url || `https://services.leadconnectorhq.com${resource}`, + url: url ?? `https://services.leadconnectorhq.com${resource}`, json: true, }; if (!Object.keys(body).length) { @@ -197,7 +197,7 @@ export async function taskUpdatePreSendAction( this: IExecuteSingleFunctions, requestOptions: IHttpRequestOptions, ): Promise { - const body = (requestOptions.body || {}) as { title?: string; dueDate?: string }; + const body = (requestOptions.body ?? {}) as { title?: string; dueDate?: string }; if (!body.title || !body.dueDate) { const contactId = this.getNodeParameter('contactId'); const taskId = this.getNodeParameter('taskId'); @@ -215,7 +215,7 @@ export async function splitTagsPreSendAction( this: IExecuteSingleFunctions, requestOptions: IHttpRequestOptions, ): Promise { - const body = (requestOptions.body || {}) as IDataObject; + const body = (requestOptions.body ?? {}) as IDataObject; if (body.tags) { if (Array.isArray(body.tags)) return requestOptions; body.tags = (body.tags as string).split(',').map((tag) => tag.trim()); @@ -346,10 +346,26 @@ export async function getUsers(this: ILoadOptionsFunctions): Promise { - const responseData = await highLevelApiRequest.call(this, 'GET', '/timezones'); - const timezones = responseData.timezones as string[]; - return timezones.map((zone) => ({ - name: zone, - value: zone, - })) as INodePropertyOptions[]; + try { + const locationId = this.getCurrentNodeParameter('locationId') as string; + if (!locationId) { + throw new ApplicationError('Location ID is not available.'); + } + const responseData = await highLevelApiRequest.call( + this, + 'GET', + `/locations/${locationId}/timezones`, + undefined, + ); + const timezones = responseData?.timeZones ?? []; + if (timezones.length === 0) { + throw new ApplicationError('No timezones available.'); + } + return timezones.map((zone: string) => ({ + name: zone.trim(), + value: zone.trim(), + })) as INodePropertyOptions[]; + } catch (error) { + throw new ApplicationError('Error fetching timezones from HighLevel API.'); + } } From c212e84a1cae68ae4801f67d7f8e3d1781284333 Mon Sep 17 00:00:00 2001 From: Stams89 Date: Sat, 14 Sep 2024 12:42:33 +0300 Subject: [PATCH 3/3] add calendar recourse --- .../nodes/HighLevel/v2/HighLevelV2.node.ts | 7 + .../v2/description/CalendarDescription.ts | 389 ++++++++++++++++++ 2 files changed, 396 insertions(+) create mode 100644 packages/nodes-base/nodes/HighLevel/v2/description/CalendarDescription.ts diff --git a/packages/nodes-base/nodes/HighLevel/v2/HighLevelV2.node.ts b/packages/nodes-base/nodes/HighLevel/v2/HighLevelV2.node.ts index 6c6b2cabf5962..f2c5ad912f4a8 100644 --- a/packages/nodes-base/nodes/HighLevel/v2/HighLevelV2.node.ts +++ b/packages/nodes-base/nodes/HighLevel/v2/HighLevelV2.node.ts @@ -10,6 +10,7 @@ import { NodeConnectionType } from 'n8n-workflow'; import { contactFields, contactNotes, contactOperations } from './description/ContactDescription'; import { opportunityFields, opportunityOperations } from './description/OpportunityDescription'; import { taskFields, taskOperations } from './description/TaskDescription'; +import { calendarFields, calendarOperations } from './description/CalendarDescription'; import { getContacts, getPipelines, @@ -38,6 +39,10 @@ const resources: INodeProperties[] = [ name: 'Task', value: 'task', }, + { + name: 'Calendar', + value: 'calendar', + }, ], default: 'contact', required: true, @@ -83,6 +88,8 @@ const versionDescription: INodeTypeDescription = { ...opportunityFields, ...taskOperations, ...taskFields, + ...calendarOperations, + ...calendarFields, ], }; diff --git a/packages/nodes-base/nodes/HighLevel/v2/description/CalendarDescription.ts b/packages/nodes-base/nodes/HighLevel/v2/description/CalendarDescription.ts new file mode 100644 index 0000000000000..fa7c2e98be4ec --- /dev/null +++ b/packages/nodes-base/nodes/HighLevel/v2/description/CalendarDescription.ts @@ -0,0 +1,389 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable n8n-nodes-base/node-param-description-excess-final-period */ +import type { INodeProperties } from 'n8n-workflow'; + +export const calendarOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: ['calendar'], + }, + }, + options: [ + { + name: 'Book Appointment', + value: 'bookAppointment', + action: 'Book Appointment in a calendar', + routing: { + request: { + method: 'POST', + url: '=/calendars/events/appointments', + }, + }, + }, + { + name: 'Get Free Slots', + value: 'getFreeSlots', + action: 'Get Free Slots of a calendar', + routing: { + request: { + method: 'GET', + url: '=/calendars/{{$parameter.calendarId}}/free-slots', + }, + }, + }, + ], + default: 'bookAppointment', + noDataExpression: true, + }, +]; + +const bookAppointmentProperties: INodeProperties[] = [ + { + displayName: 'Calendar ID', + name: 'calendarId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: ['calendar'], + operation: ['bookAppointment'], + }, + }, + default: '', + routing: { + send: { + type: 'body', + property: 'calendarId', + }, + }, + }, + { + displayName: 'Location ID', + name: 'locationId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: ['calendar'], + operation: ['bookAppointment'], + }, + }, + default: '', + routing: { + send: { + type: 'body', + property: 'locationId', + }, + }, + }, + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: ['calendar'], + operation: ['bookAppointment'], + }, + }, + default: '', + routing: { + send: { + type: 'body', + property: 'contactId', + }, + }, + }, + { + displayName: 'Start Time', + name: 'startTime', + type: 'string', + required: true, + description: 'Example: 2021-06-23T03:30:00+05:30', + displayOptions: { + show: { + resource: ['calendar'], + operation: ['bookAppointment'], + }, + }, + default: '', + routing: { + send: { + type: 'body', + property: 'startTime', + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['calendar'], + operation: ['bookAppointment'], + }, + }, + options: [ + { + displayName: 'End Time', + name: 'endTime', + type: 'string', + description: 'Example: 2021-06-23T04:30:00+05:30', + default: '', + routing: { + send: { + type: 'body', + property: 'endTime', + }, + }, + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + routing: { + send: { + type: 'body', + property: 'title', + }, + }, + }, + { + displayName: 'Appointment Status', + name: 'appointmentStatus', + type: 'options', + default: 'new', + description: + 'The status of the appointment. Allowed values: new, confirmed, cancelled, showed, noshow, invalid.', + options: [ + { + name: 'Cancelled', + value: 'cancelled', + }, + { + name: 'Confirmed', + value: 'confirmed', + }, + { + name: 'Invalid', + value: 'invalid', + }, + { + name: 'New', + value: 'new', + }, + { + name: 'No Show', + value: 'noshow', + }, + { + name: 'Showed', + value: 'showed', + }, + ], + routing: { + send: { + type: 'body', + property: 'appointmentStatus', + }, + }, + }, + { + displayName: 'Assigned User ID', + name: 'assignedUserId', + type: 'string', + default: '', + routing: { + send: { + type: 'body', + property: 'assignedUserId', + }, + }, + }, + { + displayName: 'Address', + name: 'address', + type: 'string', + default: '', + routing: { + send: { + type: 'body', + property: 'address', + }, + }, + }, + { + displayName: 'Ignore Date Range', + name: 'ignoreDateRange', + type: 'boolean', + default: false, + routing: { + send: { + type: 'body', + property: 'ignoreDateRange', + }, + }, + }, + { + displayName: 'Notify', + name: 'toNotify', + type: 'boolean', + default: true, + routing: { + send: { + type: 'body', + property: 'toNotify', + }, + }, + }, + ], + }, +]; + +const getFreeSlotsProperties: INodeProperties[] = [ + { + displayName: 'Calendar ID', + name: 'calendarId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: ['calendar'], + operation: ['getFreeSlots'], + }, + }, + default: '', + }, + { + displayName: 'Start Date', + name: 'startDate', + type: 'number', + //type: 'dateTime' TODO + default: '', + required: true, + description: 'The start date for fetching free calendar slots. Example: 1548898600000.', + displayOptions: { + show: { + resource: ['calendar'], + operation: ['getFreeSlots'], + }, + }, + routing: { + send: { + type: 'query', + property: 'startDate', + }, + }, + }, + { + displayName: 'End Date', + name: 'endDate', + type: 'number', + //type: 'dateTime' TODO + default: '', + required: true, + description: 'The end date for fetching free calendar slots. Example: 1601490599999.', + displayOptions: { + show: { + resource: ['calendar'], + operation: ['getFreeSlots'], + }, + }, + routing: { + send: { + type: 'query', + property: 'endDate', + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['calendar'], + operation: ['getFreeSlots'], + }, + }, + options: [ + { + displayName: 'Timezone', + name: 'timezone', + type: 'string', + default: '', + description: 'The timezone to use for the returned slots. Example: America/Chihuahua.', + routing: { + send: { + type: 'query', + property: 'timezone', + }, + }, + }, + { + displayName: 'User ID', + name: 'userId', + type: 'string', + default: '', + description: 'User ID to filter the slots (optional).', + routing: { + send: { + type: 'query', + property: 'userId', + }, + }, + }, + { + displayName: 'User IDs', + name: 'userIds', + type: 'collection', + default: {}, + options: [ + { + displayName: 'User IDs', + name: 'userIds', + type: 'string', + default: '', + description: 'Comma-separated list of user IDs to filter the slots.', + routing: { + send: { + type: 'query', + property: 'userIds', + }, + }, + }, + ], + }, + { + displayName: 'Apply Look Busy', + name: 'enableLookBusy', + type: 'boolean', + default: false, + // eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether + description: 'Apply Look Busy to the slots.', + routing: { + send: { + type: 'query', + property: 'enableLookBusy', + }, + }, + }, + ], + }, +]; + +export const calendarFields: INodeProperties[] = [ + ...bookAppointmentProperties, + ...getFreeSlotsProperties, +];