diff --git a/packages/nodes-base/credentials/HighLevelApi.credentials.ts b/packages/nodes-base/credentials/HighLevelApi.credentials.ts new file mode 100644 index 0000000000..c7301cc206 --- /dev/null +++ b/packages/nodes-base/credentials/HighLevelApi.credentials.ts @@ -0,0 +1,34 @@ +import { + IAuthenticateGeneric, + ICredentialTestRequest, + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class HighLevelApi implements ICredentialType { + name = 'highLevelApi'; + displayName = 'HighLevel API'; + documentationUrl = 'highLevel'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; + authenticate: IAuthenticateGeneric = { + type: 'generic', + properties: { + headers: { + Authorization: '=Bearer {{$credentials.apiKey}}', + }, + }, + }; + test: ICredentialTestRequest = { + request: { + baseURL: 'https://rest.gohighlevel.com/v1', + url: '/custom-values/', + }, + }; +} diff --git a/packages/nodes-base/nodes/HighLevel/GenericFunctions.ts b/packages/nodes-base/nodes/HighLevel/GenericFunctions.ts new file mode 100644 index 0000000000..dfa31f163f --- /dev/null +++ b/packages/nodes-base/nodes/HighLevel/GenericFunctions.ts @@ -0,0 +1,282 @@ +import { + DeclarativeRestApiSettings, + IDataObject, + IExecuteFunctions, + IExecutePaginationFunctions, + IExecuteSingleFunctions, + IHookFunctions, + IHttpRequestOptions, + ILoadOptionsFunctions, + IN8nHttpFullResponse, + INodeExecutionData, + INodePropertyOptions, + IPollFunctions, + IWebhookFunctions, + NodeApiError, +} from 'n8n-workflow'; + +import { OptionsWithUri } from 'request'; + +import { DateTime, ToISOTimeOptions } from 'luxon'; + +const VALID_EMAIL_REGEX = + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; +const VALID_PHONE_REGEX = + /((?:\+|00)[17](?: |\-)?|(?:\+|00)[1-9]\d{0,2}(?: |\-)?|(?:\+|00)1\-\d{3}(?: |\-)?)?(0\d|\([0-9]{3}\)|[1-9]{0,3})(?:((?: |\-)[0-9]{2}){4}|((?:[0-9]{2}){4})|((?: |\-)[0-9]{3}(?: |\-)[0-9]{4})|([0-9]{7}))/; + +export function isEmailValid(email: string): boolean { + return VALID_EMAIL_REGEX.test(String(email).toLowerCase()); +} + +export function isPhoneValid(phone: string): boolean { + return VALID_PHONE_REGEX.test(String(phone)); +} + +function dateToIsoSupressMillis(dateTime: string) { + const options: ToISOTimeOptions = { suppressMilliseconds: true }; + return DateTime.fromISO(dateTime).toISO(options); +} + +export async function taskPostReceiceAction( + this: IExecuteSingleFunctions, + items: INodeExecutionData[], + response: IN8nHttpFullResponse, +): Promise { + const contactId = this.getNodeParameter('contactId'); + items.forEach((item) => (item.json.contactId = contactId)); + return items; +} + +export async function dueDatePreSendAction( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, +): Promise { + let dueDateParam = this.getNodeParameter('dueDate', null) as string; + if (!dueDateParam) { + const fields = this.getNodeParameter('updateFields') as { dueDate: string }; + dueDateParam = fields.dueDate; + } + if (!dueDateParam) { + throw new NodeApiError( + this.getNode(), + {}, + { message: 'dueDate is required', description: 'dueDate is required' }, + ); + } + const dueDate = dateToIsoSupressMillis(dueDateParam); + requestOptions.body = (requestOptions.body || {}) as object; + Object.assign(requestOptions.body, { dueDate }); + return requestOptions; +} + +export async function contactIdentifierPreSendAction( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, +): Promise { + requestOptions.body = (requestOptions.body || {}) as object; + let identifier = this.getNodeParameter('contactIdentifier', null) as string; + if (!identifier) { + const fields = this.getNodeParameter('updateFields') as { contactIdentifier: string }; + identifier = fields.contactIdentifier; + } + if (isEmailValid(identifier)) { + Object.assign(requestOptions.body, { email: identifier }); + } else if (isPhoneValid(identifier)) { + Object.assign(requestOptions.body, { phone: identifier }); + } else { + Object.assign(requestOptions.body, { contactId: identifier }); + } + return requestOptions; +} + +export async function validEmailAndPhonePreSendAction( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, +): Promise { + const body = (requestOptions.body || {}) as { email?: string; phone?: string }; + + if (body.email && !isEmailValid(body.email)) { + const message = `email "${body.email}" has invalid format`; + throw new NodeApiError(this.getNode(), {}, { message, description: message }); + } + + if (body.phone && !isPhoneValid(body.phone)) { + const message = `phone "${body.phone}" has invalid format`; + throw new NodeApiError(this.getNode(), {}, { message, description: message }); + } + + return requestOptions; +} + +export async function dateTimeToEpochPreSendAction( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, +): Promise { + const qs = (requestOptions.qs || {}) as { + startDate?: string | number; + endDate?: string | number; + }; + const toEpoch = (dt: string) => new Date(dt).getTime(); + if (qs.startDate) qs.startDate = toEpoch(qs.startDate as string); + if (qs.endDate) qs.endDate = toEpoch(qs.endDate as string); + return requestOptions; +} + +export async function opportunityUpdatePreSendAction( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, +): Promise { + const body = (requestOptions.body || {}) as { title?: string; status?: string }; + if (!body.status || !body.title) { + const pipelineId = this.getNodeParameter('pipelineId'); + const opportunityId = this.getNodeParameter('opportunityId'); + const resource = `/pipelines/${pipelineId}/opportunities/${opportunityId}`; + const responseData = await highLevelApiRequest.call(this, 'GET', resource); + body.status = body.status || responseData.status; + body.title = body.title || responseData.name; + requestOptions.body = body; + } + return requestOptions; +} + +export async function taskUpdatePreSendAction( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, +): Promise { + const body = (requestOptions.body || {}) as { title?: string; dueDate?: string }; + if (!body.title || !body.dueDate) { + const contactId = this.getNodeParameter('contactId'); + const taskId = this.getNodeParameter('taskId'); + const resource = `/contacts/${contactId}/tasks/${taskId}`; + const responseData = await highLevelApiRequest.call(this, 'GET', resource); + body.title = body.title || responseData.title; + // the api response dueDate has to be formatted or it will error on update + body.dueDate = body.dueDate || dateToIsoSupressMillis(responseData.dueDate); + requestOptions.body = body; + } + return requestOptions; +} + +export async function splitTagsPreSendAction( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, +): Promise { + 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()); + } + return requestOptions; +} + +export async function highLevelApiPagination( + this: IExecutePaginationFunctions, + requestData: DeclarativeRestApiSettings.ResultOptions, +): Promise { + const responseData: INodeExecutionData[] = []; + const resource = this.getNodeParameter('resource') as string; + const returnAll = this.getNodeParameter('returnAll', false) as boolean; + + const resourceMapping: { [key: string]: string } = { + contact: 'contacts', + opportunity: 'opportunities', + }; + const rootProperty = resourceMapping[resource]; + + requestData.options.qs = requestData.options.qs || {}; + if (returnAll) requestData.options.qs.limit = 100; + + let responseTotal = 0; + + do { + const pageResponseData: INodeExecutionData[] = await this.makeRoutingRequest(requestData); + const items = pageResponseData[0].json[rootProperty] as []; + items.forEach((item) => responseData.push({ json: item })); + + const meta = pageResponseData[0].json.meta as IDataObject; + const startAfterId = meta.startAfterId as string; + const startAfter = meta.startAfter as number; + requestData.options.qs = { startAfterId, startAfter }; + responseTotal = (meta.total as number) || 0; + } while (returnAll && responseTotal > responseData.length); + + return responseData; +} + +export async function highLevelApiRequest( + this: + | IExecuteFunctions + | IExecuteSingleFunctions + | IWebhookFunctions + | IPollFunctions + | IHookFunctions + | ILoadOptionsFunctions, + method: string, + resource: string, + body: IDataObject = {}, + qs: IDataObject = {}, + uri?: string, + option: IDataObject = {}, +) { + let options: OptionsWithUri = { + method, + body, + qs, + uri: uri || `https://rest.gohighlevel.com/v1${resource}`, + json: true, + }; + if (!Object.keys(body).length) { + delete options.body; + } + if (!Object.keys(qs).length) { + delete options.qs; + } + options = Object.assign({}, options, option); + try { + return await this.helpers.requestWithAuthentication.call(this, 'highLevelApi', options); + } catch (error) { + throw new NodeApiError(this.getNode(), error, { + message: error.message, + }); + } +} + +export async function getPipelineStages( + this: ILoadOptionsFunctions, +): Promise { + const pipelineId = this.getCurrentNodeParameter('pipelineId') as string; + const responseData = await highLevelApiRequest.call(this, 'GET', '/pipelines'); + const pipelines = responseData.pipelines as [ + { id: string; stages: [{ id: string; name: string }] }, + ]; + const pipeline = pipelines.find((p) => p.id === pipelineId); + if (pipeline) { + const options: INodePropertyOptions[] = pipeline.stages.map((stage) => { + const name = stage.name; + const value = stage.id; + return { name, value }; + }); + return options; + } + return []; +} + +export async function getUsers(this: ILoadOptionsFunctions): Promise { + const responseData = await highLevelApiRequest.call(this, 'GET', '/users'); + const users = responseData.users as [{ id: string; name: string; email: string }]; + const options: INodePropertyOptions[] = users.map((user) => { + const name = user.name; + const value = user.id; + return { name, value }; + }); + return options; +} + +export async function getTimezones(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[]; +} diff --git a/packages/nodes-base/nodes/HighLevel/HighLevel.node.json b/packages/nodes-base/nodes/HighLevel/HighLevel.node.json new file mode 100644 index 0000000000..dff68c3b19 --- /dev/null +++ b/packages/nodes-base/nodes/HighLevel/HighLevel.node.json @@ -0,0 +1,18 @@ +{ + "node": "n8n-nodes-base.highLevel", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Marketing & Content", "Sales"], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/highLevel" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.highLevel/" + } + ] + } +} diff --git a/packages/nodes-base/nodes/HighLevel/HighLevel.node.ts b/packages/nodes-base/nodes/HighLevel/HighLevel.node.ts new file mode 100644 index 0000000000..b22cc567ca --- /dev/null +++ b/packages/nodes-base/nodes/HighLevel/HighLevel.node.ts @@ -0,0 +1,88 @@ +import { INodeProperties, INodeType, INodeTypeDescription } from 'n8n-workflow'; + +import { contactFields, contactNotes, contactOperations } from './description/ContactDescription'; +import { opportunityFields, opportunityOperations } from './description/OpportunityDescription'; +import { taskFields, taskOperations } from './description/TaskDescription'; +import { + getPipelineStages, + getTimezones, + getUsers, + highLevelApiPagination, +} from './GenericFunctions'; + +const ressources: INodeProperties[] = [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Contact', + value: 'contact', + }, + { + name: 'Opportunity', + value: 'opportunity', + }, + { + name: 'Task', + value: 'task', + }, + ], + default: 'contact', + required: true, + }, +]; + +export class HighLevel implements INodeType { + description: INodeTypeDescription = { + displayName: 'HighLevel', + name: 'highLevel', + icon: 'file:highLevel.svg', + group: ['transform'], + version: 1, + description: 'Consume HighLevel API', + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + defaults: { + name: 'HighLevel', + color: '#f1be40', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'highLevelApi', + required: true, + }, + ], + requestDefaults: { + baseURL: 'https://rest.gohighlevel.com/v1', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }, + requestOperations: { + pagination: highLevelApiPagination, + }, + properties: [ + ...ressources, + ...contactOperations, + ...contactNotes, + ...contactFields, + ...opportunityOperations, + ...opportunityFields, + ...taskOperations, + ...taskFields, + ], + }; + + methods = { + loadOptions: { + getPipelineStages, + getUsers, + getTimezones, + }, + }; +} diff --git a/packages/nodes-base/nodes/HighLevel/description/ContactDescription.ts b/packages/nodes-base/nodes/HighLevel/description/ContactDescription.ts new file mode 100644 index 0000000000..8adece44cd --- /dev/null +++ b/packages/nodes-base/nodes/HighLevel/description/ContactDescription.ts @@ -0,0 +1,856 @@ +import { INodeProperties } from 'n8n-workflow'; +import { splitTagsPreSendAction, validEmailAndPhonePreSendAction } from '../GenericFunctions'; + +export const contactOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['contact'], + }, + }, + options: [ + { + name: 'Create or Update', + value: 'create', + routing: { + request: { + method: 'POST', + url: '/contacts', + }, + send: { + preSend: [validEmailAndPhonePreSendAction, splitTagsPreSendAction], + }, + output: { + postReceive: [ + { + type: 'rootProperty', + properties: { + property: 'contact', + }, + }, + ], + }, + }, + action: 'Create or update a contact', + }, + { + name: 'Delete', + value: 'delete', + routing: { + request: { + method: 'DELETE', + url: '=/contacts/{{$parameter.contactId}}', + }, + output: { + postReceive: [ + { + type: 'set', + properties: { + value: '={{ { "success": true } }}', + }, + }, + ], + }, + }, + action: 'Delete a contact', + }, + { + name: 'Get', + value: 'get', + routing: { + request: { + method: 'GET', + url: '=/contacts/{{$parameter.contactId}}', + }, + output: { + postReceive: [ + { + type: 'rootProperty', + properties: { + property: 'contact', + }, + }, + ], + }, + }, + action: 'Get a contact', + }, + { + name: 'Get All', + value: 'getAll', + routing: { + request: { + method: 'GET', + url: '=/contacts', + }, + send: { + paginate: true, + }, + }, + action: 'Get all contacts', + }, + { + name: 'Lookup', + value: 'lookup', + routing: { + request: { + method: 'GET', + url: '=/contacts/lookup?email={{$parameter.email}}&phone={{$parameter.phone}}', + }, + output: { + postReceive: [ + { + type: 'rootProperty', + properties: { + property: 'contacts', + }, + }, + ], + }, + }, + action: 'Lookup a contact', + }, + { + name: 'Update', + value: 'update', + routing: { + request: { + method: 'PUT', + url: '=/contacts/{{$parameter.contactId}}', + }, + send: { + preSend: [validEmailAndPhonePreSendAction, splitTagsPreSendAction], + }, + output: { + postReceive: [ + { + type: 'rootProperty', + properties: { + property: 'contact', + }, + }, + ], + }, + }, + action: 'Update a contact', + }, + ], + default: 'create', + }, +]; + +export const contactNotes: INodeProperties[] = [ + { + displayName: + 'Create a new contact or update an existing one if email or phone matches (upsert)', + name: 'contactCreateNotice', + type: 'notice', + displayOptions: { + show: { + resource: ['contact'], + operation: ['create'], + }, + }, + default: '', + }, +]; + +const customFields: INodeProperties = { + displayName: 'Custom Fields', + name: 'customFields', + placeholder: 'Add Field', + type: 'fixedCollection', + default: {}, + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'values', + displayName: 'Value', + values: [ + { + displayName: 'Field Name or ID', + name: 'fieldId', + type: 'options', + required: true, + default: '', + description: + 'Choose from the list, or specify an ID using an expression', + typeOptions: { + loadOptions: { + routing: { + request: { + url: '/custom-fields', + method: 'GET', + }, + output: { + postReceive: [ + { + type: 'rootProperty', + properties: { + property: 'customFields', + }, + }, + { + type: 'setKeyValue', + properties: { + name: '={{$responseItem.name}}', + value: '={{$responseItem.id}}', + }, + }, + { + type: 'sort', + properties: { + key: 'name', + }, + }, + ], + }, + }, + }, + }, + }, + { + displayName: 'Field Value', + name: 'fieldValue', + type: 'string', + default: '', + routing: { + send: { + value: '={{$value}}', + property: '=customField.{{$parent.fieldId}}', + type: 'body', + }, + }, + }, + ], + }, + ], +}; + +const createProperties: INodeProperties[] = [ + { + displayName: 'Email', + name: 'email', + type: 'string', + placeholder: 'name@email.com', + description: 'Email or Phone are required to create contact', + displayOptions: { + show: { + resource: ['contact'], + operation: ['create'], + }, + }, + default: '', + routing: { + send: { + type: 'body', + property: 'email', + }, + }, + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + description: + 'Phone or Email are required to create contact. Phone number has to start with a valid country code leading with + sign.', + placeholder: '+491234567890', + displayOptions: { + show: { + resource: ['contact'], + operation: ['create'], + }, + }, + default: '', + routing: { + send: { + type: 'body', + property: 'phone', + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['contact'], + operation: ['create'], + }, + }, + options: [ + { + displayName: 'Address', + name: 'address1', + type: 'string', + default: '', + routing: { + send: { + type: 'body', + property: 'address1', + }, + }, + }, + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + routing: { + send: { + type: 'body', + property: 'city', + }, + }, + }, + customFields, + { + displayName: 'Do Not Disturb', + name: 'dnd', + description: + 'Whether automated/manual outbound messages are permitted to go out or not. True means NO outbound messages are permitted.', + type: 'boolean', + default: false, + routing: { + send: { + type: 'body', + property: 'dnd', + }, + }, + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + routing: { + send: { + type: 'body', + property: 'firstName', + }, + }, + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + routing: { + send: { + type: 'body', + property: 'lastName', + }, + }, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: 'e.g. John Deo', + description: + "The full name of the contact, will be overwritten by 'First Name' and 'Last Name' if set", + routing: { + send: { + type: 'body', + property: 'name', + }, + }, + }, + { + displayName: 'Postal Code', + name: 'postalCode', + type: 'string', + default: '', + routing: { + send: { + type: 'body', + property: 'postalCode', + }, + }, + }, + { + displayName: 'Source', + name: 'source', + type: 'string', + default: '', + placeholder: 'e.g. Public API', + routing: { + send: { + type: 'body', + property: 'source', + }, + }, + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + routing: { + send: { + type: 'body', + property: 'state', + }, + }, + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + hint: 'Comma separated list of tags, array of strings can be set in expression', + default: '', + routing: { + send: { + type: 'body', + property: 'tags', + }, + }, + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Timezone', + name: 'timezone', + type: 'options', + default: '', + description: + 'Choose from the list, or specify an ID using an expression', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + routing: { + send: { + type: 'body', + property: 'timezone', + }, + }, + }, + { + displayName: 'Website', + name: 'website', + type: 'string', + default: '', + routing: { + send: { + type: 'body', + property: 'website', + }, + }, + }, + ], + }, +]; + +const updateProperties: INodeProperties[] = [ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: ['contact'], + operation: ['update'], + }, + }, + default: '', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['contact'], + operation: ['update'], + }, + }, + options: [ + { + displayName: 'Address', + name: 'address1', + type: 'string', + default: '', + routing: { + send: { + type: 'body', + property: 'address1', + }, + }, + }, + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + routing: { + send: { + type: 'body', + property: 'city', + }, + }, + }, + customFields, + { + displayName: 'Do Not Disturb', + name: 'dnd', + description: + 'Whether automated/manual outbound messages are permitted to go out or not. True means NO outbound messages are permitted.', + type: 'boolean', + default: false, + routing: { + send: { + type: 'body', + property: 'dnd', + }, + }, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + placeholder: 'name@email.com', + default: '', + routing: { + send: { + type: 'body', + property: 'email', + }, + }, + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + routing: { + send: { + type: 'body', + property: 'firstName', + }, + }, + }, + { + displayName: 'Last Name', + name: 'lastName', + type: 'string', + default: '', + routing: { + send: { + type: 'body', + property: 'lastName', + }, + }, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + description: + "The full name of the contact, will be overwritten by 'First Name' and 'Last Name' if set", + default: 'e.g. John Deo', + routing: { + send: { + type: 'body', + property: 'name', + }, + }, + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: + 'Phone number has to start with a valid country code leading with + sign', + placeholder: '+491234567890', + routing: { + send: { + type: 'body', + property: 'phone', + }, + }, + }, + { + displayName: 'Postal Code', + name: 'postalCode', + type: 'string', + default: '', + routing: { + send: { + type: 'body', + property: 'postalCode', + }, + }, + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + routing: { + send: { + type: 'body', + property: 'state', + }, + }, + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + hint: 'Comma separated list of tags, array of strings can be set in expression', + default: '', + routing: { + send: { + type: 'body', + property: 'tags', + }, + }, + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Timezone', + name: 'timezone', + type: 'options', + default: '', + description: + 'Choose from the list, or specify an ID using an expression', + typeOptions: { + loadOptionsMethod: 'getTimezones', + }, + routing: { + send: { + type: 'body', + property: 'timezone', + }, + }, + }, + { + displayName: 'Website', + name: 'website', + type: 'string', + default: '', + routing: { + send: { + type: 'body', + property: 'website', + }, + }, + }, + ], + }, +]; + +const deleteProperties: INodeProperties[] = [ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + displayOptions: { + show: { + resource: ['contact'], + operation: ['delete'], + }, + }, + default: '', + }, +]; + +const getProperties: INodeProperties[] = [ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: ['contact'], + operation: ['get'], + }, + }, + default: '', + }, +]; + +const getAllProperties: INodeProperties[] = [ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['contact'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['contact'], + operation: ['getAll'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 20, + routing: { + send: { + type: 'query', + property: 'limit', + }, + output: { + maxResults: '={{$value}}', // Set maxResults to the value of current parameter + }, + }, + description: 'Max number of results to return', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: ['contact'], + operation: ['getAll'], + }, + }, + options: [ + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + description: + 'Query will search on these fields: Name, Phone, Email, Tags, and Company Name', + routing: { + send: { + type: 'query', + property: 'query', + }, + }, + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: ['contact'], + operation: ['getAll'], + }, + }, + options: [ + { + displayName: 'Order', + name: 'order', + type: 'options', + options: [ + { + name: 'Descending', + value: 'desc', + }, + { + name: 'Ascending', + value: 'asc', + }, + ], + default: 'desc', + routing: { + send: { + type: 'query', + property: 'order', + }, + }, + }, + { + displayName: 'Sort By', + name: 'sortBy', + type: 'options', + options: [ + { + name: 'Date Added', + value: 'date_added', + }, + { + name: 'Date Updated', + value: 'date_updated', + }, + ], + default: 'date_added', + routing: { + send: { + type: 'query', + property: 'sortBy', + }, + }, + }, + ], + }, +]; + +const lookupProperties: INodeProperties[] = [ + { + displayName: 'Email', + name: 'email', + type: 'string', + placeholder: 'name@email.com', + description: + 'Lookup Contact by Email. If Email is not found it will try to find a contact by phone.', + displayOptions: { + show: { + resource: ['contact'], + operation: ['lookup'], + }, + }, + default: '', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + description: + 'Lookup Contact by Phone. It will first try to find a contact by Email and than by Phone.', + displayOptions: { + show: { + resource: ['contact'], + operation: ['lookup'], + }, + }, + default: '', + }, +]; + +export const contactFields: INodeProperties[] = [ + ...createProperties, + ...updateProperties, + ...deleteProperties, + ...getProperties, + ...getAllProperties, + ...lookupProperties, +]; diff --git a/packages/nodes-base/nodes/HighLevel/description/OpportunityDescription.ts b/packages/nodes-base/nodes/HighLevel/description/OpportunityDescription.ts new file mode 100644 index 0000000000..b791d25ddd --- /dev/null +++ b/packages/nodes-base/nodes/HighLevel/description/OpportunityDescription.ts @@ -0,0 +1,740 @@ +import { INodeProperties } from 'n8n-workflow'; + +import { + contactIdentifierPreSendAction, + dateTimeToEpochPreSendAction, + opportunityUpdatePreSendAction, + splitTagsPreSendAction, +} from '../GenericFunctions'; + +export const opportunityOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['opportunity'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + routing: { + send: { + preSend: [splitTagsPreSendAction], + }, + request: { + method: 'POST', + url: '=/pipelines/{{$parameter.pipelineId}}/opportunities', + }, + }, + action: 'Create an opportunity', + }, + { + name: 'Delete', + value: 'delete', + routing: { + request: { + method: 'DELETE', + url: '=/pipelines/{{$parameter.pipelineId}}/opportunities/{{$parameter.opportunityId}}', + }, + output: { + postReceive: [ + { + type: 'set', + properties: { + value: '={{ { "success": true } }}', + }, + }, + ], + }, + }, + action: 'Delete an opportunity', + }, + { + name: 'Get', + value: 'get', + routing: { + request: { + method: 'GET', + url: '=/pipelines/{{$parameter.pipelineId}}/opportunities/{{$parameter.opportunityId}}', + }, + }, + action: 'Get an opportunity', + }, + { + name: 'Get All', + value: 'getAll', + routing: { + request: { + method: 'GET', + url: '=/pipelines/{{$parameter.pipelineId}}/opportunities', + }, + send: { + paginate: true, + }, + }, + action: 'Get all opportunities', + }, + { + name: 'Update', + value: 'update', + routing: { + request: { + method: 'PUT', + url: '=/pipelines/{{$parameter.pipelineId}}/opportunities/{{$parameter.opportunityId}}', + }, + send: { + preSend: [opportunityUpdatePreSendAction, splitTagsPreSendAction], + }, + }, + action: 'Update an opportunity', + }, + ], + default: 'create', + }, +]; + +const pipelineId: INodeProperties = { + displayName: 'Pipeline Name or ID', + name: 'pipelineId', + type: 'options', + displayOptions: { + show: { + resource: ['opportunity'], + operation: ['create', 'delete', 'get', 'getAll', 'update'], + }, + }, + description: + 'Choose from the list, or specify an ID using an expression', + typeOptions: { + loadOptions: { + routing: { + request: { + url: '/pipelines', + method: 'GET', + }, + output: { + postReceive: [ + { + type: 'rootProperty', + properties: { + property: 'pipelines', + }, + }, + { + type: 'setKeyValue', + properties: { + name: '={{$responseItem.name}}', + value: '={{$responseItem.id}}', + }, + }, + { + type: 'sort', + properties: { + key: 'name', + }, + }, + ], + }, + }, + }, + }, + default: '', +}; + +const createProperties: INodeProperties[] = [ + { + displayName: 'Stage Name or ID', + name: 'stageId', + type: 'options', + required: true, + description: + 'Choose from the list, or specify an ID using an expression', + displayOptions: { + show: { + resource: ['opportunity'], + operation: ['create'], + }, + }, + default: '', + typeOptions: { + loadOptionsDependsOn: ['pipelineId'], + loadOptionsMethod: 'getPipelineStages', + }, + routing: { + send: { + type: 'body', + property: 'stageId', + }, + }, + }, + { + displayName: 'Contact Identifier', + name: 'contactIdentifier', + required: true, + type: 'string', + description: 'Either Email, Phone or Contact ID', + hint: 'There can only be one opportunity for each contact.', + displayOptions: { + show: { + resource: ['opportunity'], + operation: ['create'], + }, + }, + default: '', + routing: { + send: { + preSend: [contactIdentifierPreSendAction], + }, + }, + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + required: true, + displayOptions: { + show: { + resource: ['opportunity'], + operation: ['create'], + }, + }, + default: '', + routing: { + send: { + type: 'body', + property: 'title', + }, + }, + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + required: true, + displayOptions: { + show: { + resource: ['opportunity'], + operation: ['create'], + }, + }, + options: [ + { + name: 'Open', + value: 'open', + }, + { + name: 'Won', + value: 'won', + }, + { + name: 'Lost', + value: 'lost', + }, + { + name: 'Abandoned', + value: 'abandoned', + }, + ], + default: 'open', + routing: { + send: { + type: 'body', + property: 'status', + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['opportunity'], + operation: ['create'], + }, + }, + options: [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Assigned To', + name: 'assignedTo', + type: 'options', + default: '', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options + description: + 'Choose staff member from the list, or specify an ID using an expression', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + routing: { + send: { + type: 'body', + property: 'assignedTo', + }, + }, + }, + { + displayName: 'Company Name', + name: 'companyName', + type: 'string', + default: '', + routing: { + send: { + type: 'body', + property: 'companyName', + }, + }, + }, + { + displayName: 'Monetary Value', + name: 'monetaryValue', + type: 'number', + default: '', + description: 'Monetary value of lead opportunity', + routing: { + send: { + type: 'body', + property: 'monetaryValue', + }, + }, + }, + + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + placeholder: 'e.g. John Deo', + routing: { + send: { + type: 'body', + property: 'name', + }, + }, + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + hint: 'Comma separated list of tags, array of strings can be set in expression', + default: '', + routing: { + send: { + type: 'body', + property: 'tags', + }, + }, + }, + ], + }, +]; + +const deleteProperties: INodeProperties[] = [ + { + displayName: 'Opportunity ID', + name: 'opportunityId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: ['opportunity'], + operation: ['delete'], + }, + }, + default: '', + }, +]; + +const getProperties: INodeProperties[] = [ + { + displayName: 'Opportunity ID', + name: 'opportunityId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: ['opportunity'], + operation: ['get'], + }, + }, + default: '', + }, +]; + +const getAllProperties: INodeProperties[] = [ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['opportunity'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['opportunity'], + operation: ['getAll'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 20, + routing: { + send: { + type: 'query', + property: 'limit', + }, + }, + description: 'Max number of results to return', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: ['opportunity'], + operation: ['getAll'], + }, + }, + options: [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Assigned To', + name: 'assignedTo', + type: 'options', + default: '', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options + description: + 'Choose staff member from the list, or specify an ID using an expression', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + routing: { + send: { + type: 'query', + property: 'assignedTo', + }, + }, + }, + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'string', + default: '', + routing: { + send: { + type: 'query', + property: 'campaignId', + }, + }, + }, + { + displayName: 'End Date', + name: 'endDate', + type: 'dateTime', + default: '', + routing: { + send: { + type: 'query', + property: 'endDate', + preSend: [dateTimeToEpochPreSendAction], + }, + }, + }, + // api should filter by monetary value but doesn't + // { + // displayName: 'Monetary Value', + // name: 'monetaryValue', + // type: 'number', + // default: '', + // routing: { + // send: { + // type: 'query', + // property: 'monetaryValue', + // }, + // }, + // }, + { + displayName: 'Stage Name or ID', + name: 'stageId', + type: 'options', + default: '', + description: + 'Choose from the list, or specify an ID using an expression', + typeOptions: { + loadOptionsDependsOn: ['pipelineId'], + loadOptionsMethod: 'getPipelineStages', + }, + routing: { + send: { + type: 'query', + property: 'stageId', + }, + }, + }, + { + displayName: 'Start Date', + name: 'startDate', + type: 'dateTime', + default: '', + routing: { + send: { + type: 'query', + property: 'startDate', + preSend: [dateTimeToEpochPreSendAction], + }, + }, + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Open', + value: 'open', + }, + { + name: 'Won', + value: 'won', + }, + { + name: 'Lost', + value: 'lost', + }, + { + name: 'Abandoned', + value: 'abandoned', + }, + ], + default: 'open', + routing: { + send: { + type: 'query', + property: 'status', + }, + }, + }, + { + displayName: 'Query', + name: 'query', + type: 'string', + default: '', + description: + 'Query will search on these fields: Name, Phone, Email, Tags, and Company Name', + routing: { + send: { + type: 'query', + property: 'query', + }, + }, + }, + ], + }, +]; + +const updateProperties: INodeProperties[] = [ + { + displayName: 'Opportunity ID', + name: 'opportunityId', + type: 'string', + required: true, + hint: 'You cannot update an opportunity\'s pipeline ID.', + displayOptions: { + show: { + resource: ['opportunity'], + operation: ['update'], + }, + }, + default: '', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['opportunity'], + operation: ['update'], + }, + }, + options: [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Assigned To', + name: 'assignedTo', + type: 'options', + default: '', + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options + description: + 'Choose staff member from the list, or specify an ID using an expression', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + routing: { + send: { + type: 'body', + property: 'assignedTo', + }, + }, + }, + { + displayName: 'Company Name', + name: 'companyName', + type: 'string', + default: '', + routing: { + send: { + type: 'body', + property: 'companyName', + }, + }, + }, + { + displayName: 'Contact Identifier', + name: 'contactIdentifier', + type: 'string', + description: 'Either Email, Phone or Contact ID', + hint: 'There can only be one opportunity for each contact.', + default: '', + routing: { + send: { + preSend: [contactIdentifierPreSendAction], + }, + }, + }, + { + displayName: 'Monetary Value', + name: 'monetaryValue', + type: 'number', + default: '', + description: 'Monetary value of lead opportunity', + routing: { + send: { + type: 'body', + property: 'monetaryValue', + }, + }, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + placeholder: 'e.g. John Deo', + routing: { + send: { + type: 'body', + property: 'name', + }, + }, + }, + { + displayName: 'Stage Name or ID', + name: 'stageId', + type: 'options', + default: '', + description: + 'Choose from the list, or specify an ID using an expression', + typeOptions: { + loadOptionsDependsOn: ['pipelineId'], + loadOptionsMethod: 'getPipelineStages', + }, + routing: { + send: { + type: 'body', + property: 'stageId', + }, + }, + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Open', + value: 'open', + }, + { + name: 'Won', + value: 'won', + }, + { + name: 'Lost', + value: 'lost', + }, + { + name: 'Abandoned', + value: 'abandoned', + }, + ], + default: 'open', + routing: { + send: { + type: 'body', + property: 'status', + }, + }, + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + routing: { + send: { + type: 'body', + property: 'title', + }, + }, + }, + { + displayName: 'Tags', + name: 'tags', + type: 'string', + hint: 'Comma separated list of tags, array of strings can be set in expression', + default: '', + routing: { + send: { + type: 'body', + property: 'tags', + }, + }, + }, + ], + }, +]; + +export const opportunityFields: INodeProperties[] = [ + pipelineId, + ...createProperties, + ...updateProperties, + ...deleteProperties, + ...getProperties, + ...getAllProperties, +]; diff --git a/packages/nodes-base/nodes/HighLevel/description/TaskDescription.ts b/packages/nodes-base/nodes/HighLevel/description/TaskDescription.ts new file mode 100644 index 0000000000..30d890294d --- /dev/null +++ b/packages/nodes-base/nodes/HighLevel/description/TaskDescription.ts @@ -0,0 +1,475 @@ +import { INodeProperties } from 'n8n-workflow'; + +import { dueDatePreSendAction, taskPostReceiceAction, taskUpdatePreSendAction } from '../GenericFunctions'; + +export const taskOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['task'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + routing: { + request: { + method: 'POST', + url: '=/contacts/{{$parameter.contactId}}/tasks', + }, + output: { + postReceive: [taskPostReceiceAction], + }, + }, + action: 'Create a task', + }, + { + name: 'Delete', + value: 'delete', + routing: { + request: { + method: 'DELETE', + url: '=/contacts/{{$parameter.contactId}}/tasks/{{$parameter.taskId}}', + }, + output: { + postReceive: [ + { + type: 'set', + properties: { + value: '={{ { "success": true } }}', + }, + }, + ], + }, + }, + action: 'Delete a task', + }, + { + name: 'Get', + value: 'get', + routing: { + request: { + method: 'GET', + url: '=/contacts/{{$parameter.contactId}}/tasks/{{$parameter.taskId}}', + }, + output: { + postReceive: [taskPostReceiceAction], + }, + }, + action: 'Get a task', + }, + { + name: 'Get All', + value: 'getAll', + routing: { + request: { + method: 'GET', + url: '=/contacts/{{$parameter.contactId}}/tasks', + }, + output: { + postReceive: [ + { + type: 'rootProperty', + properties: { + property: 'tasks', + }, + }, + taskPostReceiceAction, + ], + }, + }, + action: 'Get all tasks', + }, + { + name: 'Update', + value: 'update', + routing: { + request: { + method: 'PUT', + url: '=/contacts/{{$parameter.contactId}}/tasks/{{$parameter.taskId}}', + }, + send: { + preSend: [taskUpdatePreSendAction], + }, + output: { + postReceive: [taskPostReceiceAction], + }, + }, + action: 'Update a task', + }, + ], + default: 'create', + }, +]; + +const createProperties: INodeProperties[] = [ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + displayOptions: { + show: { + resource: ['task'], + operation: ['create'], + }, + }, + default: '', + required: true, + description: 'Contact the task belongs to', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: ['task'], + operation: ['create'], + }, + }, + routing: { + send: { + type: 'body', + property: 'title', + }, + }, + }, + { + displayName: 'Due Date', + name: 'dueDate', + type: 'dateTime', + required: true, + default: '', + displayOptions: { + show: { + resource: ['task'], + operation: ['create'], + }, + }, + routing: { + send: { + type: 'body', + property: 'dueDate', + preSend: [dueDatePreSendAction], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['task'], + operation: ['create'], + }, + }, + options: [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Assigned To', + name: 'assignedTo', + type: 'options', + default: '', + description: + 'Choose from the list, or specify an ID using an expression', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + routing: { + send: { + type: 'body', + property: 'assignedTo', + }, + }, + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + routing: { + send: { + type: 'body', + property: 'description', + }, + }, + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Incompleted', + value: 'incompleted', + }, + { + name: 'Completed', + value: 'completed', + }, + ], + default: 'incompleted', + routing: { + send: { + type: 'body', + property: 'status', + }, + }, + }, + ], + }, +]; + +const deleteProperties: INodeProperties[] = [ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + displayOptions: { + show: { + resource: ['task'], + operation: ['delete'], + }, + }, + default: '', + required: true, + description: 'Contact the task belongs to', + }, + { + displayName: 'Task ID', + name: 'taskId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: ['task'], + operation: ['delete'], + }, + }, + default: '', + }, +]; + +const getProperties: INodeProperties[] = [ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + displayOptions: { + show: { + resource: ['task'], + operation: ['get'], + }, + }, + default: '', + required: true, + description: 'Contact the task belongs to', + }, + { + displayName: 'Task ID', + name: 'taskId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: ['task'], + operation: ['get'], + }, + }, + default: '', + }, +]; + +const getAllProperties: INodeProperties[] = [ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + displayOptions: { + show: { + resource: ['task'], + operation: ['getAll'], + }, + }, + default: '', + required: true, + description: 'Contact the task belongs to', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['task'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['task'], + operation: ['getAll'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 20, + routing: { + send: { + type: 'query', + property: 'limit', + }, + }, + description: 'Max number of results to return', + }, +]; + +const updateProperties: INodeProperties[] = [ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + displayOptions: { + show: { + resource: ['task'], + operation: ['update'], + }, + }, + default: '', + required: true, + description: 'Contact the task belongs to', + }, + { + displayName: 'Task ID', + name: 'taskId', + type: 'string', + displayOptions: { + show: { + resource: ['task'], + operation: ['update'], + }, + }, + default: '', + required: true, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['task'], + operation: ['update'], + }, + }, + options: [ + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options + displayName: 'Assigned To', + name: 'assignedTo', + type: 'options', + default: '', + description: + 'Choose from the list, or specify an ID using an expression', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + routing: { + send: { + type: 'body', + property: 'assignedTo', + }, + }, + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + routing: { + send: { + type: 'body', + property: 'description', + }, + }, + }, + { + displayName: 'Due Date', + name: 'dueDate', + type: 'dateTime', + default: '', + routing: { + send: { + type: 'body', + property: 'dueDate', + preSend: [dueDatePreSendAction], + }, + }, + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Incompleted', + value: 'incompleted', + }, + { + name: 'Completed', + value: 'completed', + }, + ], + default: 'incompleted', + routing: { + send: { + type: 'body', + property: 'status', + }, + }, + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + routing: { + send: { + type: 'body', + property: 'title', + }, + }, + }, + ], + }, +]; + +export const taskFields: INodeProperties[] = [ + ...createProperties, + ...updateProperties, + ...deleteProperties, + ...getProperties, + ...getAllProperties, +]; diff --git a/packages/nodes-base/nodes/HighLevel/highLevel.svg b/packages/nodes-base/nodes/HighLevel/highLevel.svg new file mode 100644 index 0000000000..f81310d29a --- /dev/null +++ b/packages/nodes-base/nodes/HighLevel/highLevel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index e56ed216ee..9e5ca6da58 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -138,6 +138,7 @@ "dist/credentials/HarvestApi.credentials.js", "dist/credentials/HarvestOAuth2Api.credentials.js", "dist/credentials/HelpScoutOAuth2Api.credentials.js", + "dist/credentials/HighLevelApi.credentials.js", "dist/credentials/HomeAssistantApi.credentials.js", "dist/credentials/HttpBasicAuth.credentials.js", "dist/credentials/HttpDigestAuth.credentials.js", @@ -476,6 +477,7 @@ "dist/nodes/Harvest/Harvest.node.js", "dist/nodes/HelpScout/HelpScout.node.js", "dist/nodes/HelpScout/HelpScoutTrigger.node.js", + "dist/nodes/HighLevel/HighLevel.node.js", "dist/nodes/HomeAssistant/HomeAssistant.node.js", "dist/nodes/HtmlExtract/HtmlExtract.node.js", "dist/nodes/HttpRequest/HttpRequest.node.js", @@ -808,3 +810,5 @@ ] } } + +