From e209077160ca9126f75f10a4cb7846c18adbedaf Mon Sep 17 00:00:00 2001 From: Erin Date: Thu, 2 Jul 2020 16:41:59 -0400 Subject: [PATCH 01/41] :sparkles: ConvertKit Trigger and Regular Node --- .../credentials/ConvertKitApi.credentials.ts | 21 ++ .../nodes/ConvertKit/ConvertKit.node.ts | 289 ++++++++++++++++++ .../ConvertKit/ConvertKitTrigger.node.ts | 163 ++++++++++ .../nodes/ConvertKit/FieldDescription.ts | 83 +++++ .../nodes/ConvertKit/FormDescription.ts | 174 +++++++++++ .../nodes/ConvertKit/GenericFunctions.ts | 44 +++ .../nodes/ConvertKit/SequenceDescription.ts | 174 +++++++++++ .../nodes/ConvertKit/TagDescription.ts | 204 +++++++++++++ .../nodes/ConvertKit/convertKit.png | Bin 0 -> 4958 bytes packages/nodes-base/package.json | 3 + 10 files changed, 1155 insertions(+) create mode 100644 packages/nodes-base/credentials/ConvertKitApi.credentials.ts create mode 100644 packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts create mode 100644 packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts create mode 100644 packages/nodes-base/nodes/ConvertKit/FieldDescription.ts create mode 100644 packages/nodes-base/nodes/ConvertKit/FormDescription.ts create mode 100644 packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts create mode 100644 packages/nodes-base/nodes/ConvertKit/TagDescription.ts create mode 100644 packages/nodes-base/nodes/ConvertKit/convertKit.png diff --git a/packages/nodes-base/credentials/ConvertKitApi.credentials.ts b/packages/nodes-base/credentials/ConvertKitApi.credentials.ts new file mode 100644 index 0000000000..1685c11a24 --- /dev/null +++ b/packages/nodes-base/credentials/ConvertKitApi.credentials.ts @@ -0,0 +1,21 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class ConvertKitApi implements ICredentialType { + name = 'convertKitApi'; + displayName = 'ConvertKit Api'; + properties = [ + { + displayName: 'API Secret', + name: 'apiSecret', + type: 'string' as NodePropertyTypes, + default: '', + typeOptions: { + password: true, + }, + }, + ]; +} diff --git a/packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts b/packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts new file mode 100644 index 0000000000..71f58a4172 --- /dev/null +++ b/packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts @@ -0,0 +1,289 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeTypeDescription, + INodeType, +} from 'n8n-workflow'; + +import { + convertKitApiRequest, +} from './GenericFunctions'; + +import { + fieldOperations, + fieldFields, +} from './FieldDescription'; + +import { + formOperations, + formFields, +} from './FormDescription'; + +import { + sequenceOperations, + sequenceFields, +} from './SequenceDescription'; + +import { + tagOperations, + tagFields, +} from './TagDescription'; + +export class ConvertKit implements INodeType { + description: INodeTypeDescription = { + displayName: 'ConvertKit', + name: 'convertKit', + icon: 'file:convertKit.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume ConvertKit API.', + defaults: { + name: 'ConvertKit', + color: '#fb6970', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'convertKitApi', + required: true, + } + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Field', + value: 'field', + }, + { + name: 'Form', + value: 'form', + }, + { + name: 'Sequence', + value: 'sequence', + }, + { + name: 'Tag', + value: 'tag', + }, + ], + default: 'field', + description: 'The resource to operate on.' + }, + //-------------------- + // Field Description + //-------------------- + ...fieldOperations, + ...fieldFields, + //-------------------- + // FormDescription + //-------------------- + ...formOperations, + ...formFields, + //-------------------- + // Sequence Description + //-------------------- + ...sequenceOperations, + ...sequenceFields, + //-------------------- + // Tag Description + //-------------------- + ...tagOperations, + ...tagFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + + let method = ''; + let endpoint = ''; + const qs: IDataObject = {}; + let responseData; + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + const fullOperation = `${resource}/${operation}`; + + for (let i = 0; i < items.length; i++) { + //-------------------- + // Field Operations + //-------------------- + if(resource === 'field') { + //--------- + // Update + //--------- + if(operation === 'update') { + qs.label = this.getNodeParameter('label', i) as string; + + const id = this.getNodeParameter('id', i) as string; + + method = 'PUT'; + endpoint = `/custom_fields/${id}`; + //--------- + // Get All + //--------- + } else if(operation === 'getAll') { + method = 'GET'; + endpoint = '/custom_fields'; + //--------- + // Create + //--------- + } else if(operation === 'create') { + qs.label = this.getNodeParameter('label', i) as string; + + method = 'POST'; + endpoint = '/custom_fields'; + //--------- + // Delete + //--------- + } else if(operation === 'delete') { + const id = this.getNodeParameter('id', i) as string; + + method = 'DELETE'; + endpoint = `/custom_fields/${id}`; + } else { + throw new Error(`The operation "${operation}" is not known!`); + } + //-------------------------------------------- + // Form, Sequence, and Tag Operations + //-------------------------------------------- + } else if(['form', 'sequence', 'tag'].includes(resource)) { + //----------------- + // Add Subscriber + //----------------- + if(operation === 'addSubscriber') { + qs.email= this.getNodeParameter('email', i) as string; + const id = this.getNodeParameter('id', i); + + const additionalParams = this.getNodeParameter('additionalFields', 0) as IDataObject; + + if(additionalParams.firstName) { + qs.first_name = additionalParams.firstName; + } + + if(additionalParams.fields !== undefined) { + const fields = {} as IDataObject; + const fieldsParams = additionalParams.fields as IDataObject; + const field = fieldsParams?.field as IDataObject[]; + + for(let j = 0; j < field.length; j++) { + const key = field[j].key as string; + const value = field[j].value as string; + + fields[key] = value; + } + + qs.fields = fields; + } + + if(resource === 'form') { + method = 'POST'; + endpoint = `/forms/${id}/subscribe`; + } else if(resource === 'sequence') { + method = 'POST'; + endpoint = `/sequences/${id}/subscribe`; + } else if(resource === 'tag') { + method = 'POST'; + endpoint = `/tags/${id}/subscribe`; + } + //----------------- + // Get All + //----------------- + } else if(operation === 'getAll') { + method = 'GET'; + if(resource === 'form') { + endpoint = '/forms'; + } else if(resource === 'tag') { + endpoint = '/tags'; + } else if(resource === 'sequence') { + endpoint = '/sequences'; + } + //-------------------- + // Get Subscriptions + //-------------------- + } else if(operation === 'getSubscriptions') { + const id = this.getNodeParameter('id', i); + const additionalParams = this.getNodeParameter('additionalFields', 0) as IDataObject; + if(additionalParams.subscriberState) { + qs.subscriber_state = additionalParams.subscriberState; + } + + method = 'GET'; + if(resource === 'form') { + endpoint = `/forms/${id}/subscriptions`; + } else if(resource === 'tag') { + endpoint = `/tags/${id}/subscriptions`; + } else if(resource === 'sequence') { + endpoint = `/sequences/${id}/subscriptions`; + } + //------------ + // Create Tag + //------------ + } else if(operation === 'create') { + const name = this.getNodeParameter('name', i); + qs.tag = { name, }; + + method = 'POST'; + endpoint = '/tags'; + //------------ + // Remove Tag + //------------ + } else if(operation === 'removeSubscriber') { + const id = this.getNodeParameter('id', i); + + qs.email = this.getNodeParameter('email', i); + + method = 'POST'; + endpoint = `/tags/${id}/unsubscribe`; + } else { + throw new Error(`The operation "${operation}" is not known!`); + } + } else { + throw new Error(`The resource "${resource}" is not known!`); + } + + responseData = await convertKitApiRequest.call(this, method, endpoint, {}, qs); + + if(fullOperation === 'field/getAll') { + responseData = responseData.custom_fields; + } else if(['form/addSubscriber', 'tag/addSubscriber', 'sequence/addSubscriber'].includes(fullOperation)) { + responseData = responseData.subscription; + } else if(fullOperation === 'form/getAll') { + responseData = responseData.forms; + } else if(['form/getSubscriptions', 'tag/getSubscriptions'].includes(fullOperation)) { + responseData = responseData.subscriptions; + } else if(fullOperation === 'tag/getAll') { + responseData = responseData.tags; + } else if(fullOperation === 'sequence/getAll') { + responseData = responseData.courses; + } + + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } else { + if(method === 'GET') { + returnData.push( { } ); + } else { + returnData.push( { success: true } ); + } + } + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts b/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts new file mode 100644 index 0000000000..5b1c6c2274 --- /dev/null +++ b/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts @@ -0,0 +1,163 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeTypeDescription, + INodeType, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + convertKitApiRequest, +} from './GenericFunctions'; + + +export class ConvertKitTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'ConvertKit Trigger', + name: 'convertKitTrigger', + icon: 'file:convertKit.png', + subtitle: '={{$parameter["event"]}}', + group: ['trigger'], + version: 1, + description: 'Handle ConvertKit events via webhooks', + defaults: { + name: 'ConvertKit Trigger', + color: '#fb6970', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'convertKitApi', + required: true, + } + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Event', + name: 'event', + type: 'options', + required: true, + default: 'subscriberActivated', + description: 'The events that can trigger the webhook and whether they are enabled.', + options: [ + { + name: 'Subscriber Activated', + value: 'subscriberActivated', + description: 'Whether the webhook is triggered when a subscriber is activated.', + }, + { + name: 'Link Clicked', + value: 'linkClicked', + description: 'Whether the webhook is triggered when a link is clicked.', + }, + ], + }, + { + displayName: 'Initiating Link', + name: 'link', + type: 'string', + required: true, + default: '', + description: 'The URL of the initiating link', + displayOptions: { + show: { + event: [ + 'linkClicked', + ], + }, + }, + }, + ], + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + + if(webhookData.webhookId) { + return true; + } + return false; + }, + + async create(this: IHookFunctions): Promise { + let webhook; + const webhookUrl = this.getNodeWebhookUrl('default'); + const event = this.getNodeParameter('event', 0); + const endpoint = '/automations/hooks'; + + const qs: IDataObject = {}; + + try { + qs.target_url = webhookUrl; + + if(event === 'subscriberActivated') { + qs.event = { + name: 'subscriber.subscriber_activate', + }; + } else if(event === 'linkClicked') { + const link = this.getNodeParameter('link', 0) as string; + qs.event = { + name: 'subscriber.link_click', + initiator_value: link, + }; + } + webhook = await convertKitApiRequest.call(this, 'POST', endpoint, {}, qs); + } catch (error) { + throw error; + } + + if (webhook.rule.id === undefined) { + return false; + } + + const webhookData = this.getWorkflowStaticData('node'); + webhookData.webhookId = webhook.rule.id as string; + webhookData.events = event; + return true; + }, + + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId !== undefined) { + const endpoint = `/automations/hooks/${webhookData.webhookId}`; + try { + await convertKitApiRequest.call(this, 'DELETE', endpoint, {}, {}); + } catch (error) { + return false; + } + delete webhookData.webhookId; + delete webhookData.events; + } + return true; + }, + }, + }; + + + async webhook(this: IWebhookFunctions): Promise { + const returnData: IDataObject[] = []; + returnData.push(this.getBodyData()); + + return { + workflowData: [ + this.helpers.returnJsonArray(returnData), + ], + }; + } +} diff --git a/packages/nodes-base/nodes/ConvertKit/FieldDescription.ts b/packages/nodes-base/nodes/ConvertKit/FieldDescription.ts new file mode 100644 index 0000000000..8966e60efa --- /dev/null +++ b/packages/nodes-base/nodes/ConvertKit/FieldDescription.ts @@ -0,0 +1,83 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const fieldOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'field', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a field.', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a field.', + }, + { + name: 'Get All', + value: 'getAll', + description: `List all of your account's custom fields.`, + }, + { + name: 'Update', + value: 'update', + description: 'Update a field.', + }, + ], + default: 'update', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const fieldFields = [ + { + displayName: 'Field ID', + name: 'id', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'field', + ], + operation: [ + 'update', + 'delete', + ], + }, + }, + default: '', + description: 'The ID of your custom field.', + }, + { + displayName: 'Label', + name: 'label', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'field', + ], + operation: [ + 'update', + 'create', + ], + }, + }, + default: '', + description: 'The label of the custom field.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ConvertKit/FormDescription.ts b/packages/nodes-base/nodes/ConvertKit/FormDescription.ts new file mode 100644 index 0000000000..3e266d18a5 --- /dev/null +++ b/packages/nodes-base/nodes/ConvertKit/FormDescription.ts @@ -0,0 +1,174 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const formOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'form', + ], + }, + }, + options: [ + { + name: 'Add Subscriber', + value: 'addSubscriber', + description: 'Add a subscriber.', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get a list of all the forms for your account.', + }, + { + name: 'Get Subscriptions', + value: 'getSubscriptions', + description: 'List subscriptions to a form including subscriber data.', + }, + ], + default: 'addSubscriber', + description: 'The operations to perform.', + }, +] as INodeProperties[]; + +export const formFields = [ + { + displayName: 'Email Address', + name: 'email', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'form', + ], + operation: [ + 'addSubscriber', + ], + }, + }, + default: '', + description: `The subscriber's email address.`, + }, + { + displayName: 'Form ID', + name: 'id', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'form', + ], + operation: [ + 'addSubscriber', + 'getSubscriptions', + ], + }, + }, + default: '', + description: 'Form ID.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'form', + ], + operation: [ + 'addSubscriber', + ], + }, + }, + options: [ + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: `The subscriber's first name.`, + }, + { + displayName: 'Custom Fields', + name: 'fields', + placeholder: 'Add Custom Field', + description: 'Object of key/value pairs for custom fields (the custom field must exist before you can use it here).', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'field', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field Key', + name: 'key', + type: 'string', + default: '', + placeholder: 'last_name', + description: `The field's key.`, + }, + { + displayName: 'Field Value', + name: 'value', + type: 'string', + default: '', + placeholder: 'Doe', + description: 'Value of the field.', + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'form', + ], + operation: [ + 'getSubscriptions', + ], + }, + }, + options: [ + { + displayName: 'Subscriber State', + name: 'subscriberState', + type: 'options', + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'Cancelled', + value: 'cancelled', + }, + ], + default: 'active', + }, + ], + description: 'Receive only active subscribers or cancelled subscribers.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts b/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts new file mode 100644 index 0000000000..525aed043a --- /dev/null +++ b/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts @@ -0,0 +1,44 @@ +import { + OptionsWithUri +} from 'request'; + +import { + IExecuteFunctions, + ILoadOptionsFunctions, + IExecuteSingleFunctions, +} from 'n8n-core'; + +import { + IDataObject, + IHookFunctions +} from 'n8n-workflow'; + +export async function convertKitApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IHookFunctions, + method: string, endpoint: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('convertKitApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + let options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + qs, + body, + uri: uri ||`https://api.convertkit.com/v3${endpoint}`, + json: true, + }; + options = Object.assign({}, options, option); + if (Object.keys(options.body).length === 0) { + delete options.body; + } + try { + qs.api_secret = credentials.apiSecret; + + return await this.helpers.request!(options); + } catch (error) { + throw new Error(`ConvertKit error response: ${error.message}`); + } +} diff --git a/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts b/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts new file mode 100644 index 0000000000..1b82b741fa --- /dev/null +++ b/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts @@ -0,0 +1,174 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const sequenceOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'sequence', + ], + }, + }, + options: [ + { + name: 'Add Subscriber', + value: 'addSubscriber', + description: 'Add a subscriber.', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Returns a list of sequences for the account.', + }, + { + name: 'Get Subscriptions', + value: 'getSubscriptions', + description: 'List subscriptions to a sequence including subscriber data.', + }, + ], + default: 'addSubscriber', + description: 'The operations to perform.', + }, +] as INodeProperties[]; + +export const sequenceFields = [ + { + displayName: 'Email Address', + name: 'email', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'sequence', + ], + operation: [ + 'addSubscriber', + ], + }, + }, + default: '', + description: `The subscriber's email address.`, + }, + { + displayName: 'Sequence ID', + name: 'id', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'sequence', + ], + operation: [ + 'addSubscriber', + 'getSubscriptions', + ], + }, + }, + default: '', + description: 'Sequence ID.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'sequence', + ], + operation: [ + 'addSubscriber', + ], + }, + }, + options: [ + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: `The subscriber's first name.`, + }, + { + displayName: 'Custom Fields', + name: 'fields', + placeholder: 'Add Custom Field', + description: 'Object of key/value pairs for custom fields (the custom field must exist before you can use it here).', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'field', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field Key', + name: 'key', + type: 'string', + default: '', + placeholder: 'last_name', + description: `The field's key.`, + }, + { + displayName: 'Field Value', + name: 'value', + type: 'string', + default: '', + placeholder: 'Doe', + description: 'Value of the field.', + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'sequence', + ], + operation: [ + 'getSubscriptions', + ], + }, + }, + options: [ + { + displayName: 'Subscriber State', + name: 'subscriberState', + type: 'options', + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'Cancelled', + value: 'cancelled', + }, + ], + default: 'active', + }, + ], + description: 'Receive only active subscribers or cancelled subscribers.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ConvertKit/TagDescription.ts b/packages/nodes-base/nodes/ConvertKit/TagDescription.ts new file mode 100644 index 0000000000..a1a718ba3c --- /dev/null +++ b/packages/nodes-base/nodes/ConvertKit/TagDescription.ts @@ -0,0 +1,204 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const tagOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'tag', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a tag.', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Returns a list of tags for the account.', + }, + { + name: 'Get Subscriptions', + value: 'getSubscriptions', + description: 'List subscriptions to a tag including subscriber data.', + }, + { + name: 'Remove Subscriber', + value: 'removeSubscriber', + description: 'Remove a tag from a subscriber.', + }, + { + name: 'Add Subscriber', + value: 'addSubscriber', + description: 'Add a tag to a subscriber.', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const tagFields = [ + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'tag', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: 'Tag name.', + }, + { + displayName: 'Email Address', + name: 'email', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'tag', + ], + operation: [ + 'addSubscriber', + 'removeSubscriber', + ], + }, + }, + default: '', + description: 'Subscriber email address.', + }, + { + displayName: 'Tag ID', + name: 'id', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'tag', + ], + operation: [ + 'addSubscriber', + 'removeSubscriber', + 'getSubscriptions', + ], + }, + }, + default: '', + description: 'Tag ID.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'tag', + ], + operation: [ + 'addSubscriber', + ], + }, + }, + options: [ + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: 'Subscriber first name.', + }, + { + displayName: 'Custom Fields', + name: 'fields', + placeholder: 'Add Custom Field', + description: 'Object of key/value pairs for custom fields (the custom field must exist before you can use it here).', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'field', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field Key', + name: 'key', + type: 'string', + default: '', + placeholder: 'last_name', + description: `The field's key.`, + }, + { + displayName: 'Field Value', + name: 'value', + type: 'string', + default: '', + placeholder: 'Doe', + description: 'Value of the field.', + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'tag', + ], + operation: [ + 'getSubscriptions', + ], + }, + }, + options: [ + { + displayName: 'Subscriber State', + name: 'subscriberState', + type: 'options', + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'Cancelled', + value: 'cancelled', + }, + ], + default: 'active', + }, + ], + description: 'Receive only active subscribers or cancelled subscribers.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ConvertKit/convertKit.png b/packages/nodes-base/nodes/ConvertKit/convertKit.png new file mode 100644 index 0000000000000000000000000000000000000000..2dc9cf3ee52a2d64f636c43724bc9d1b81cb6372 GIT binary patch literal 4958 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 Date: Wed, 5 Aug 2020 21:55:53 -0400 Subject: [PATCH 02/41] :zap: Improvements to Converkit-Node --- .../nodes/ConvertKit/ConvertKit.node.ts | 507 ++++++++++++------ ...scription.ts => CustomFieldDescription.ts} | 61 ++- .../nodes/ConvertKit/FormDescription.ts | 74 ++- .../nodes/ConvertKit/GenericFunctions.ts | 20 +- .../nodes/ConvertKit/SequenceDescription.ts | 82 ++- .../nodes/ConvertKit/TagDescription.ts | 160 +----- .../ConvertKit/TagSubscriberDescription.ts | 219 ++++++++ 7 files changed, 794 insertions(+), 329 deletions(-) rename packages/nodes-base/nodes/ConvertKit/{FieldDescription.ts => CustomFieldDescription.ts} (52%) create mode 100644 packages/nodes-base/nodes/ConvertKit/TagSubscriberDescription.ts diff --git a/packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts b/packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts index 71f58a4172..8c9870639e 100644 --- a/packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts +++ b/packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts @@ -1,5 +1,6 @@ import { IExecuteFunctions, + ILoadOptionsFunctions, } from 'n8n-core'; import { @@ -7,6 +8,7 @@ import { INodeExecutionData, INodeTypeDescription, INodeType, + INodePropertyOptions, } from 'n8n-workflow'; import { @@ -14,9 +16,9 @@ import { } from './GenericFunctions'; import { - fieldOperations, - fieldFields, -} from './FieldDescription'; + customFieldOperations, + customFieldFields, +} from './CustomFieldDescription'; import { formOperations, @@ -33,6 +35,11 @@ import { tagFields, } from './TagDescription'; +import { + tagSubscriberOperations, + tagSubscriberFields, +} from './TagSubscriberDescription'; + export class ConvertKit implements INodeType { description: INodeTypeDescription = { displayName: 'ConvertKit', @@ -61,8 +68,8 @@ export class ConvertKit implements INodeType { type: 'options', options: [ { - name: 'Field', - value: 'field', + name: 'Custom Field', + value: 'customField', }, { name: 'Form', @@ -76,15 +83,19 @@ export class ConvertKit implements INodeType { name: 'Tag', value: 'tag', }, + { + name: 'Tag Subscriber', + value: 'tagSubscriber', + }, ], - default: 'field', + default: 'customField', description: 'The resource to operate on.' }, //-------------------- // Field Description //-------------------- - ...fieldOperations, - ...fieldFields, + ...customFieldOperations, + ...customFieldFields, //-------------------- // FormDescription //-------------------- @@ -100,187 +111,373 @@ export class ConvertKit implements INodeType { //-------------------- ...tagOperations, ...tagFields, + //-------------------- + // Tag Subscriber Description + //-------------------- + ...tagSubscriberOperations, + ...tagSubscriberFields, ], }; + methods = { + loadOptions: { + // Get all the tags to display them to user so that he can + // select them easily + async getTags(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const { tags } = await convertKitApiRequest.call(this, 'GET', '/tags'); + for (const tag of tags) { + const tagName = tag.name; + const tagId = tag.id; + returnData.push({ + name: tagName, + value: tagId, + }); + } + + return returnData; + }, + // Get all the forms to display them to user so that he can + // select them easily + async getForms(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const { forms } = await convertKitApiRequest.call(this, 'GET', '/forms'); + for (const form of forms) { + const formName = form.name; + const formId = form.id; + returnData.push({ + name: formName, + value: formId, + }); + } + + return returnData; + }, + + // Get all the sequences to display them to user so that he can + // select them easily + async getSequences(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const { courses } = await convertKitApiRequest.call(this, 'GET', '/sequences'); + for (const course of courses) { + const courseName = course.name; + const courseId = course.id; + returnData.push({ + name: courseName, + value: courseId, + }); + } + + return returnData; + }, + } + }; + async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: IDataObject[] = []; - let method = ''; - let endpoint = ''; const qs: IDataObject = {}; let responseData; const resource = this.getNodeParameter('resource', 0) as string; const operation = this.getNodeParameter('operation', 0) as string; - const fullOperation = `${resource}/${operation}`; - for (let i = 0; i < items.length; i++) { - //-------------------- - // Field Operations - //-------------------- - if(resource === 'field') { - //--------- - // Update - //--------- - if(operation === 'update') { - qs.label = this.getNodeParameter('label', i) as string; + + if (resource === 'customField') { + if (operation === 'create') { + + const label = this.getNodeParameter('label', i) as string; + + responseData = await convertKitApiRequest.call(this, 'POST', '/custom_fields', { label }, qs); + } + if (operation === 'delete') { const id = this.getNodeParameter('id', i) as string; - method = 'PUT'; - endpoint = `/custom_fields/${id}`; - //--------- - // Get All - //--------- - } else if(operation === 'getAll') { - method = 'GET'; - endpoint = '/custom_fields'; - //--------- - // Create - //--------- - } else if(operation === 'create') { - qs.label = this.getNodeParameter('label', i) as string; + responseData = await convertKitApiRequest.call(this, 'DELETE', `/custom_fields/${id}`); + } + if (operation === 'get') { - method = 'POST'; - endpoint = '/custom_fields'; - //--------- - // Delete - //--------- - } else if(operation === 'delete') { const id = this.getNodeParameter('id', i) as string; - method = 'DELETE'; - endpoint = `/custom_fields/${id}`; - } else { - throw new Error(`The operation "${operation}" is not known!`); + responseData = await convertKitApiRequest.call(this, 'GET', `/custom_fields/${id}`); } - //-------------------------------------------- - // Form, Sequence, and Tag Operations - //-------------------------------------------- - } else if(['form', 'sequence', 'tag'].includes(resource)) { - //----------------- - // Add Subscriber - //----------------- - if(operation === 'addSubscriber') { - qs.email= this.getNodeParameter('email', i) as string; - const id = this.getNodeParameter('id', i); + if (operation === 'getAll') { - const additionalParams = this.getNodeParameter('additionalFields', 0) as IDataObject; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; - if(additionalParams.firstName) { - qs.first_name = additionalParams.firstName; + responseData = await convertKitApiRequest.call(this, 'GET', `/custom_fields`); + + responseData = responseData.custom_fields; + + if (!returnAll) { + + const limit = this.getNodeParameter('limit', i) as number; + + responseData = responseData.slice(0, limit); } - - if(additionalParams.fields !== undefined) { - const fields = {} as IDataObject; - const fieldsParams = additionalParams.fields as IDataObject; - const field = fieldsParams?.field as IDataObject[]; - - for(let j = 0; j < field.length; j++) { - const key = field[j].key as string; - const value = field[j].value as string; - - fields[key] = value; - } - - qs.fields = fields; - } - - if(resource === 'form') { - method = 'POST'; - endpoint = `/forms/${id}/subscribe`; - } else if(resource === 'sequence') { - method = 'POST'; - endpoint = `/sequences/${id}/subscribe`; - } else if(resource === 'tag') { - method = 'POST'; - endpoint = `/tags/${id}/subscribe`; - } - //----------------- - // Get All - //----------------- - } else if(operation === 'getAll') { - method = 'GET'; - if(resource === 'form') { - endpoint = '/forms'; - } else if(resource === 'tag') { - endpoint = '/tags'; - } else if(resource === 'sequence') { - endpoint = '/sequences'; - } - //-------------------- - // Get Subscriptions - //-------------------- - } else if(operation === 'getSubscriptions') { - const id = this.getNodeParameter('id', i); - const additionalParams = this.getNodeParameter('additionalFields', 0) as IDataObject; - if(additionalParams.subscriberState) { - qs.subscriber_state = additionalParams.subscriberState; - } - - method = 'GET'; - if(resource === 'form') { - endpoint = `/forms/${id}/subscriptions`; - } else if(resource === 'tag') { - endpoint = `/tags/${id}/subscriptions`; - } else if(resource === 'sequence') { - endpoint = `/sequences/${id}/subscriptions`; - } - //------------ - // Create Tag - //------------ - } else if(operation === 'create') { - const name = this.getNodeParameter('name', i); - qs.tag = { name, }; - - method = 'POST'; - endpoint = '/tags'; - //------------ - // Remove Tag - //------------ - } else if(operation === 'removeSubscriber') { - const id = this.getNodeParameter('id', i); - - qs.email = this.getNodeParameter('email', i); - - method = 'POST'; - endpoint = `/tags/${id}/unsubscribe`; - } else { - throw new Error(`The operation "${operation}" is not known!`); } - } else { - throw new Error(`The resource "${resource}" is not known!`); + if (operation === 'update') { + + const id = this.getNodeParameter('id', i) as string; + + const label = this.getNodeParameter('label', i) as string; + + responseData = await convertKitApiRequest.call(this, 'PUT', `/custom_fields/${id}`, { label }); + + responseData = { success: true }; + } } - responseData = await convertKitApiRequest.call(this, method, endpoint, {}, qs); + if (resource === 'form') { + if (operation === 'addSubscriber') { - if(fullOperation === 'field/getAll') { - responseData = responseData.custom_fields; - } else if(['form/addSubscriber', 'tag/addSubscriber', 'sequence/addSubscriber'].includes(fullOperation)) { - responseData = responseData.subscription; - } else if(fullOperation === 'form/getAll') { - responseData = responseData.forms; - } else if(['form/getSubscriptions', 'tag/getSubscriptions'].includes(fullOperation)) { - responseData = responseData.subscriptions; - } else if(fullOperation === 'tag/getAll') { - responseData = responseData.tags; - } else if(fullOperation === 'sequence/getAll') { - responseData = responseData.courses; + const email = this.getNodeParameter('email', i) as string; + + const formId = this.getNodeParameter('id', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + email, + }; + + if (additionalFields.firstName) { + body.first_name = additionalFields.firstName as string; + } + + if (additionalFields.tags) { + body.tags = additionalFields.tags as string[]; + } + + if (additionalFields.fieldsUi) { + const fieldValues = (additionalFields.fieldsUi as IDataObject).fieldsValues as IDataObject[]; + if (fieldValues) { + body.fields = {}; + for (const fieldValue of fieldValues) { + //@ts-ignore + body.fields[fieldValue.key] = fieldValue.value; + } + } + } + + const { subscription } = await convertKitApiRequest.call(this, 'POST', `/forms/${formId}/subscribe`, body); + + responseData = subscription; + } + if (operation === 'getAll') { + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + responseData = await convertKitApiRequest.call(this, 'GET', `/forms`); + + responseData = responseData.forms; + + if (!returnAll) { + + const limit = this.getNodeParameter('limit', i) as number; + + responseData = responseData.slice(0, limit); + } + } + if (operation === 'getSubscriptions') { + + const formId = this.getNodeParameter('id', i) as string; + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.subscriberState) { + qs.subscriber_state = additionalFields.subscriberState as string; + } + + responseData = await convertKitApiRequest.call(this, 'GET', `/forms/${formId}/subscriptions`, {}, qs); + + responseData = responseData.subscriptions; + + if (!returnAll) { + + const limit = this.getNodeParameter('limit', i) as number; + + responseData = responseData.slice(0, limit); + } + } + } + + if (resource === 'sequence') { + if (operation === 'addSubscriber') { + + const email = this.getNodeParameter('email', i) as string; + + const sequenceId = this.getNodeParameter('id', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + email, + }; + + if (additionalFields.firstName) { + body.first_name = additionalFields.firstName as string; + } + + if (additionalFields.tags) { + body.tags = additionalFields.tags as string[]; + } + + if (additionalFields.fieldsUi) { + const fieldValues = (additionalFields.fieldsUi as IDataObject).fieldsValues as IDataObject[]; + if (fieldValues) { + body.fields = {}; + for (const fieldValue of fieldValues) { + //@ts-ignore + body.fields[fieldValue.key] = fieldValue.value; + } + } + } + + const { subscription } = await convertKitApiRequest.call(this, 'POST', `/sequences/${sequenceId}/subscribe`, body); + + responseData = subscription; + } + if (operation === 'getAll') { + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + responseData = await convertKitApiRequest.call(this, 'GET', `/sequences`); + + responseData = responseData.courses; + + if (!returnAll) { + + const limit = this.getNodeParameter('limit', i) as number; + + responseData = responseData.slice(0, limit); + } + } + if (operation === 'getSubscriptions') { + + const sequenceId = this.getNodeParameter('id', i) as string; + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (additionalFields.subscriberState) { + qs.subscriber_state = additionalFields.subscriberState as string; + } + + responseData = await convertKitApiRequest.call(this, 'GET', `/sequences/${sequenceId}/subscriptions`, {}, qs); + + responseData = responseData.subscriptions; + + if (!returnAll) { + + const limit = this.getNodeParameter('limit', i) as number; + + responseData = responseData.slice(0, limit); + } + } + } + + if (resource === 'tag') { + if (operation === 'create') { + + const names = ((this.getNodeParameter('name', i) as string).split(',') as string[]).map((e) => ({ name: e })); + + const body: IDataObject = { + tag: names + }; + + responseData = await convertKitApiRequest.call(this, 'POST', '/tags', body); + } + + if (operation === 'getAll') { + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + responseData = await convertKitApiRequest.call(this, 'GET', `/tags`); + + responseData = responseData.tags; + + if (!returnAll) { + + const limit = this.getNodeParameter('limit', i) as number; + + responseData = responseData.slice(0, limit); + } + } + } + + if (resource === 'tagSubscriber') { + + if (operation === 'add') { + + const tagId = this.getNodeParameter('tagId', i) as string; + + const email = this.getNodeParameter('email', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + email, + }; + + if (additionalFields.firstName) { + body.first_name = additionalFields.firstName as string; + } + + if (additionalFields.fieldsUi) { + const fieldValues = (additionalFields.fieldsUi as IDataObject).fieldsValues as IDataObject[]; + if (fieldValues) { + body.fields = {}; + for (const fieldValue of fieldValues) { + //@ts-ignore + body.fields[fieldValue.key] = fieldValue.value; + } + } + } + + const { subscription } = await convertKitApiRequest.call(this, 'POST', `/tags/${tagId}/subscribe`, body); + + responseData = subscription; + } + + if (operation === 'getAll') { + + const tagId = this.getNodeParameter('tagId', i) as string; + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + responseData = await convertKitApiRequest.call(this, 'GET', `/tags/${tagId}/subscriptions`); + + responseData = responseData.subscriptions; + + if (!returnAll) { + + const limit = this.getNodeParameter('limit', i) as number; + + responseData = responseData.slice(0, limit); + } + } + + if (operation === 'delete') { + + const tagId = this.getNodeParameter('tagId', i) as string; + + const email = this.getNodeParameter('email', i) as string; + + responseData= await convertKitApiRequest.call(this, 'POST', `/tags/${tagId}>/unsubscribe`, { email }); + } } if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]); } else if (responseData !== undefined) { returnData.push(responseData as IDataObject); - } else { - if(method === 'GET') { - returnData.push( { } ); - } else { - returnData.push( { success: true } ); - } } } diff --git a/packages/nodes-base/nodes/ConvertKit/FieldDescription.ts b/packages/nodes-base/nodes/ConvertKit/CustomFieldDescription.ts similarity index 52% rename from packages/nodes-base/nodes/ConvertKit/FieldDescription.ts rename to packages/nodes-base/nodes/ConvertKit/CustomFieldDescription.ts index 8966e60efa..a8541c6493 100644 --- a/packages/nodes-base/nodes/ConvertKit/FieldDescription.ts +++ b/packages/nodes-base/nodes/ConvertKit/CustomFieldDescription.ts @@ -1,8 +1,8 @@ import { - INodeProperties + INodeProperties, } from 'n8n-workflow'; -export const fieldOperations = [ +export const customFieldOperations = [ { displayName: 'Operation', name: 'operation', @@ -10,7 +10,7 @@ export const fieldOperations = [ displayOptions: { show: { resource: [ - 'field', + 'customField', ], }, }, @@ -18,22 +18,22 @@ export const fieldOperations = [ { name: 'Create', value: 'create', - description: 'Create a field.', + description: 'Create a field', }, { name: 'Delete', value: 'delete', - description: 'Delete a field.', + description: 'Delete a field', }, { name: 'Get All', value: 'getAll', - description: `List all of your account's custom fields.`, + description: 'Get all fields', }, { name: 'Update', value: 'update', - description: 'Update a field.', + description: 'Update a field', }, ], default: 'update', @@ -41,7 +41,7 @@ export const fieldOperations = [ }, ] as INodeProperties[]; -export const fieldFields = [ +export const customFieldFields = [ { displayName: 'Field ID', name: 'id', @@ -50,7 +50,7 @@ export const fieldFields = [ displayOptions: { show: { resource: [ - 'field', + 'customField', ], operation: [ 'update', @@ -69,7 +69,7 @@ export const fieldFields = [ displayOptions: { show: { resource: [ - 'field', + 'customField', ], operation: [ 'update', @@ -80,4 +80,45 @@ export const fieldFields = [ default: '', description: 'The label of the custom field.', }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'customField', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'customField', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ConvertKit/FormDescription.ts b/packages/nodes-base/nodes/ConvertKit/FormDescription.ts index 3e266d18a5..f6e1a6f3e5 100644 --- a/packages/nodes-base/nodes/ConvertKit/FormDescription.ts +++ b/packages/nodes-base/nodes/ConvertKit/FormDescription.ts @@ -18,17 +18,17 @@ export const formOperations = [ { name: 'Add Subscriber', value: 'addSubscriber', - description: 'Add a subscriber.', + description: 'Add a subscriber', }, { name: 'Get All', value: 'getAll', - description: 'Get a list of all the forms for your account.', + description: 'Get all forms', }, { name: 'Get Subscriptions', value: 'getSubscriptions', - description: 'List subscriptions to a form including subscriber data.', + description: 'List subscriptions to a form including subscriber data', }, ], default: 'addSubscriber', @@ -38,7 +38,7 @@ export const formOperations = [ export const formFields = [ { - displayName: 'Email Address', + displayName: 'Email', name: 'email', type: 'string', required: true, @@ -58,7 +58,10 @@ export const formFields = [ { displayName: 'Form ID', name: 'id', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getForms', + }, required: true, displayOptions: { show: { @@ -91,16 +94,9 @@ export const formFields = [ }, }, options: [ - { - displayName: 'First Name', - name: 'firstName', - type: 'string', - default: '', - description: `The subscriber's first name.`, - }, { displayName: 'Custom Fields', - name: 'fields', + name: 'fieldsUi', placeholder: 'Add Custom Field', description: 'Object of key/value pairs for custom fields (the custom field must exist before you can use it here).', type: 'fixedCollection', @@ -110,7 +106,7 @@ export const formFields = [ default: {}, options: [ { - name: 'field', + name: 'fieldsValues', displayName: 'Custom Field', values: [ { @@ -133,8 +129,58 @@ export const formFields = [ }, ], }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: `The subscriber's first name.`, + }, ], }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + 'getSubscriptions', + ], + resource: [ + 'form', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + 'getSubscriptions', + ], + resource: [ + 'form', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, { displayName: 'Additional Fields', name: 'additionalFields', diff --git a/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts b/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts index 525aed043a..4844045e35 100644 --- a/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts +++ b/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts @@ -15,7 +15,9 @@ import { export async function convertKitApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IHookFunctions, method: string, endpoint: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any - const credentials = this.getCredentials('convertKitApi'); + + const credentials = this.getCredentials('convertKitApi'); + if (credentials === undefined) { throw new Error('No credentials got returned!'); } @@ -30,15 +32,29 @@ export async function convertKitApiRequest(this: IExecuteFunctions | IExecuteSin uri: uri ||`https://api.convertkit.com/v3${endpoint}`, json: true, }; + options = Object.assign({}, options, option); + if (Object.keys(options.body).length === 0) { delete options.body; } + + console.log(options); + try { + qs.api_secret = credentials.apiSecret; return await this.helpers.request!(options); + } catch (error) { - throw new Error(`ConvertKit error response: ${error.message}`); + + let errorMessage = error; + + if (error.response && error.response.body && error.response.body.message) { + errorMessage = error.response.body.message; + } + + throw new Error(`ConvertKit error response: ${errorMessage}`); } } diff --git a/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts b/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts index 1b82b741fa..493b713cda 100644 --- a/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts +++ b/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts @@ -1,5 +1,5 @@ import { - INodeProperties + INodeProperties, } from 'n8n-workflow'; export const sequenceOperations = [ @@ -23,7 +23,7 @@ export const sequenceOperations = [ { name: 'Get All', value: 'getAll', - description: 'Returns a list of sequences for the account.', + description: 'Get all sequences.', }, { name: 'Get Subscriptions', @@ -38,7 +38,7 @@ export const sequenceOperations = [ export const sequenceFields = [ { - displayName: 'Email Address', + displayName: 'Email', name: 'email', type: 'string', required: true, @@ -58,7 +58,10 @@ export const sequenceFields = [ { displayName: 'Sequence ID', name: 'id', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getSequences', + }, required: true, displayOptions: { show: { @@ -74,6 +77,49 @@ export const sequenceFields = [ default: '', description: 'Sequence ID.', }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + 'getSubscriptions', + ], + resource: [ + 'sequence', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + 'getSubscriptions', + ], + resource: [ + 'sequence', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, { displayName: 'Additional Fields', name: 'additionalFields', @@ -91,16 +137,9 @@ export const sequenceFields = [ }, }, options: [ - { - displayName: 'First Name', - name: 'firstName', - type: 'string', - default: '', - description: `The subscriber's first name.`, - }, { displayName: 'Custom Fields', - name: 'fields', + name: 'fieldsUi', placeholder: 'Add Custom Field', description: 'Object of key/value pairs for custom fields (the custom field must exist before you can use it here).', type: 'fixedCollection', @@ -110,7 +149,7 @@ export const sequenceFields = [ default: {}, options: [ { - name: 'field', + name: 'fieldsValues', displayName: 'Custom Field', values: [ { @@ -133,6 +172,23 @@ export const sequenceFields = [ }, ], }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: `The subscriber's first name.`, + }, + { + displayName: 'Tag IDs', + name: 'tags', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: [], + description: 'Tags', + }, ], }, { diff --git a/packages/nodes-base/nodes/ConvertKit/TagDescription.ts b/packages/nodes-base/nodes/ConvertKit/TagDescription.ts index a1a718ba3c..b0d18adcfc 100644 --- a/packages/nodes-base/nodes/ConvertKit/TagDescription.ts +++ b/packages/nodes-base/nodes/ConvertKit/TagDescription.ts @@ -1,5 +1,5 @@ import { - INodeProperties + INodeProperties, } from 'n8n-workflow'; export const tagOperations = [ @@ -18,27 +18,12 @@ export const tagOperations = [ { name: 'Create', value: 'create', - description: 'Create a tag.', + description: 'Create a tag', }, { name: 'Get All', value: 'getAll', - description: 'Returns a list of tags for the account.', - }, - { - name: 'Get Subscriptions', - value: 'getSubscriptions', - description: 'List subscriptions to a tag including subscriber data.', - }, - { - name: 'Remove Subscriber', - value: 'removeSubscriber', - description: 'Remove a tag from a subscriber.', - }, - { - name: 'Add Subscriber', - value: 'addSubscriber', - description: 'Add a tag to a subscriber.', + description: 'Get all tags', }, ], default: 'create', @@ -63,142 +48,47 @@ export const tagFields = [ }, }, default: '', - description: 'Tag name.', + description: 'Tag name, multiple can be added separated by comma', }, { - displayName: 'Email Address', - name: 'email', - type: 'string', - required: true, + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', displayOptions: { show: { + operation: [ + 'getAll', + ], resource: [ 'tag', ], - operation: [ - 'addSubscriber', - 'removeSubscriber', - ], }, }, - default: '', - description: 'Subscriber email address.', + default: false, + description: 'If all results should be returned or only up to a given limit.', }, { - displayName: 'Tag ID', - name: 'id', - type: 'string', - required: true, + displayName: 'Limit', + name: 'limit', + type: 'number', displayOptions: { show: { + operation: [ + 'getAll', + ], resource: [ 'tag', ], - operation: [ - 'addSubscriber', - 'removeSubscriber', - 'getSubscriptions', + returnAll: [ + false, ], }, }, - default: '', - description: 'Tag ID.', - }, - { - displayName: 'Additional Fields', - name: 'additionalFields', - type: 'collection', - placeholder: 'Add Field', - default: {}, - displayOptions: { - show: { - resource: [ - 'tag', - ], - operation: [ - 'addSubscriber', - ], - }, + typeOptions: { + minValue: 1, + maxValue: 500, }, - options: [ - { - displayName: 'First Name', - name: 'firstName', - type: 'string', - default: '', - description: 'Subscriber first name.', - }, - { - displayName: 'Custom Fields', - name: 'fields', - placeholder: 'Add Custom Field', - description: 'Object of key/value pairs for custom fields (the custom field must exist before you can use it here).', - type: 'fixedCollection', - typeOptions: { - multipleValues: true, - }, - default: {}, - options: [ - { - name: 'field', - displayName: 'Custom Field', - values: [ - { - displayName: 'Field Key', - name: 'key', - type: 'string', - default: '', - placeholder: 'last_name', - description: `The field's key.`, - }, - { - displayName: 'Field Value', - name: 'value', - type: 'string', - default: '', - placeholder: 'Doe', - description: 'Value of the field.', - }, - ], - }, - ], - }, - ], - }, - { - displayName: 'Additional Fields', - name: 'additionalFields', - type: 'collection', - placeholder: 'Add Field', - default: {}, - displayOptions: { - show: { - resource: [ - 'tag', - ], - operation: [ - 'getSubscriptions', - ], - }, - }, - options: [ - { - displayName: 'Subscriber State', - name: 'subscriberState', - type: 'options', - options: [ - { - name: 'Active', - value: 'active', - }, - { - name: 'Cancelled', - value: 'cancelled', - }, - ], - default: 'active', - }, - ], - description: 'Receive only active subscribers or cancelled subscribers.', + default: 100, + description: 'How many results to return.', }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/ConvertKit/TagSubscriberDescription.ts b/packages/nodes-base/nodes/ConvertKit/TagSubscriberDescription.ts new file mode 100644 index 0000000000..f625dfed83 --- /dev/null +++ b/packages/nodes-base/nodes/ConvertKit/TagSubscriberDescription.ts @@ -0,0 +1,219 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const tagSubscriberOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'tagSubscriber', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add a tag to a subscriber', + }, + { + name: 'Get All', + value: 'getAll', + description: 'List subscriptions to a tag including subscriber data', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a tag from a subscriber', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const tagSubscriberFields = [ + { + displayName: 'Tag ID', + name: 'tagId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + required: true, + displayOptions: { + show: { + resource: [ + 'tagSubscriber', + ], + operation: [ + 'add', + 'getAll', + 'delete', + ], + }, + }, + default: '', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'tagSubscriber', + ], + operation: [ + 'add', + 'delete', + ], + }, + }, + default: '', + description: 'Subscriber email address.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'tagSubscriber', + ], + operation: [ + 'add', + ], + }, + }, + options: [ + { + displayName: 'Custom Fields', + name: 'fields', + placeholder: 'Add Custom Field', + description: 'Object of key/value pairs for custom fields (the custom field must exist before you can use it here).', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'field', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field Key', + name: 'key', + type: 'string', + default: '', + placeholder: 'last_name', + description: `The field's key.`, + }, + { + displayName: 'Field Value', + name: 'value', + type: 'string', + default: '', + placeholder: 'Doe', + description: 'Value of the field.', + }, + ], + }, + ], + }, + { + displayName: 'First Name', + name: 'firstName', + type: 'string', + default: '', + description: 'Subscriber first name.', + }, + ], + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'tagSubscriber', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'tagSubscriber', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 500, + }, + default: 100, + description: 'How many results to return.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'tagSubscriber', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Subscriber State', + name: 'subscriberState', + type: 'options', + options: [ + { + name: 'Active', + value: 'active', + }, + { + name: 'Cancelled', + value: 'cancelled', + }, + ], + default: 'active', + }, + ], + description: 'Receive only active subscribers or cancelled subscribers.', + }, +] as INodeProperties[]; From 833001a9e46055fa3a805a390cb90c68b56c5e1a Mon Sep 17 00:00:00 2001 From: ricardo Date: Thu, 6 Aug 2020 13:33:43 -0400 Subject: [PATCH 03/41] :zap: Improvements to ConvertKit-Trigger --- .../ConvertKit/ConvertKitTrigger.node.ts | 274 ++++++++++++++++-- .../nodes/ConvertKit/GenericFunctions.ts | 13 +- .../nodes/ConvertKit/SequenceDescription.ts | 6 +- 3 files changed, 255 insertions(+), 38 deletions(-) diff --git a/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts b/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts index 5b1c6c2274..cb5bdbeb2c 100644 --- a/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts +++ b/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts @@ -8,12 +8,17 @@ import { INodeTypeDescription, INodeType, IWebhookResponseData, + ILoadOptionsFunctions, + INodePropertyOptions, } from 'n8n-workflow'; import { convertKitApiRequest, } from './GenericFunctions'; +import { + snakeCase, +} from 'change-case'; export class ConvertKitTrigger implements INodeType { description: INodeTypeDescription = { @@ -34,7 +39,7 @@ export class ConvertKitTrigger implements INodeType { { name: 'convertKitApi', required: true, - } + }, ], webhooks: [ { @@ -50,21 +55,86 @@ export class ConvertKitTrigger implements INodeType { name: 'event', type: 'options', required: true, - default: 'subscriberActivated', + default: '', description: 'The events that can trigger the webhook and whether they are enabled.', options: [ { - name: 'Subscriber Activated', - value: 'subscriberActivated', - description: 'Whether the webhook is triggered when a subscriber is activated.', + name: 'Form Subscribe', + value: 'formSubscribe', }, { - name: 'Link Clicked', - value: 'linkClicked', - description: 'Whether the webhook is triggered when a link is clicked.', + name: 'Link Click', + value: 'linkClick', + }, + { + name: 'Product Purchase', + value: 'productPurchase', + }, + { + name: 'Purchase Created', + value: 'purchaseCreate', + }, + { + name: 'Sequence Complete', + value: 'courseComplete', + }, + { + name: 'Sequence Subscribe', + value: 'courseSubscribe', + }, + { + name: 'Subscriber Activated', + value: 'subscriberActivate', + }, + { + name: 'Subscriber Unsubscribe', + value: 'subscriberUnsubscribe', + }, + { + name: 'Tag Add', + value: 'tagAdd', + }, + { + name: 'Tag Remove', + value: 'tagRemove', }, ], }, + { + displayName: 'Form ID', + name: 'formId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getForms', + }, + required: true, + default: '', + displayOptions: { + show: { + event: [ + 'formSubscribe', + ], + }, + }, + }, + { + displayName: 'Sequence ID', + name: 'courseId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getSequences', + }, + required: true, + default: '', + displayOptions: { + show: { + event: [ + 'courseSubscribe', + 'courseComplete', + ], + }, + }, + }, { displayName: 'Initiating Link', name: 'link', @@ -75,7 +145,39 @@ export class ConvertKitTrigger implements INodeType { displayOptions: { show: { event: [ - 'linkClicked', + 'linkClick', + ], + }, + }, + }, + { + displayName: 'Product ID', + name: 'productId', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + event: [ + 'productPurchase', + ], + }, + }, + }, + { + displayName: 'Tag ID', + name: 'tagId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + required: true, + default: '', + displayOptions: { + show: { + event: [ + 'tagAdd', + 'tagRemove', ], }, }, @@ -83,73 +185,181 @@ export class ConvertKitTrigger implements INodeType { ], }; + methods = { + loadOptions: { + // Get all the tags to display them to user so that he can + // select them easily + async getTags(this: ILoadOptionsFunctions): Promise { + + const returnData: INodePropertyOptions[] = []; + + const { tags } = await convertKitApiRequest.call(this, 'GET', '/tags'); + + for (const tag of tags) { + + const tagName = tag.name; + + const tagId = tag.id; + + returnData.push({ + name: tagName, + value: tagId, + }); + } + + return returnData; + }, + // Get all the forms to display them to user so that he can + // select them easily + async getForms(this: ILoadOptionsFunctions): Promise { + + const returnData: INodePropertyOptions[] = []; + + const { forms } = await convertKitApiRequest.call(this, 'GET', '/forms'); + + for (const form of forms) { + + const formName = form.name; + + const formId = form.id; + + returnData.push({ + name: formName, + value: formId, + }); + } + + return returnData; + }, + + // Get all the sequences to display them to user so that he can + // select them easily + async getSequences(this: ILoadOptionsFunctions): Promise { + + const returnData: INodePropertyOptions[] = []; + + const { courses } = await convertKitApiRequest.call(this, 'GET', '/sequences'); + + for (const course of courses) { + + const courseName = course.name; + + const courseId = course.id; + + returnData.push({ + name: courseName, + value: courseId, + }); + } + + return returnData; + }, + } + }; + // @ts-ignore (because of request) webhookMethods = { default: { async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + // THe API does not have an endpoint to list all webhooks + if(webhookData.webhookId) { return true; } + return false; }, async create(this: IHookFunctions): Promise { - let webhook; + const webhookUrl = this.getNodeWebhookUrl('default'); - const event = this.getNodeParameter('event', 0); + + let event = this.getNodeParameter('event', 0) as string; + const endpoint = '/automations/hooks'; - const qs: IDataObject = {}; + if (event === 'purchaseCreate') { - try { - qs.target_url = webhookUrl; + event = `purchase.${snakeCase(event)}`; - if(event === 'subscriberActivated') { - qs.event = { - name: 'subscriber.subscriber_activate', - }; - } else if(event === 'linkClicked') { - const link = this.getNodeParameter('link', 0) as string; - qs.event = { - name: 'subscriber.link_click', - initiator_value: link, - }; - } - webhook = await convertKitApiRequest.call(this, 'POST', endpoint, {}, qs); - } catch (error) { - throw error; + } else { + + event = `subscriber.${snakeCase(event)}`; } + const body: IDataObject = { + target_url: webhookUrl as string, + event: { + name: event + }, + }; + + if (event === 'subscriber.form_subscribe') { + //@ts-ignore + body.event['form_id'] = this.getNodeParameter('formId', 0); + } + + if (event === 'subscriber.course_subscribe' || event === 'subscriber.course_complete') { + //@ts-ignore + body.event['sequence_id'] = this.getNodeParameter('courseId', 0); + } + + if (event === 'subscriber.link_click') { + //@ts-ignore + body.event['initiator_value'] = this.getNodeParameter('link', 0); + } + + if (event === 'subscriber.product_purchase') { + //@ts-ignore + body.event['product_id'] = this.getNodeParameter('productId', 0); + } + + if (event === 'subscriber.tag_add' || event === 'subscriber.tag_remove"') { + //@ts-ignore + body.event['tag_id'] = this.getNodeParameter('tagId', 0); + } + + const webhook = await convertKitApiRequest.call(this, 'POST', endpoint, body); + if (webhook.rule.id === undefined) { return false; } const webhookData = this.getWorkflowStaticData('node'); + webhookData.webhookId = webhook.rule.id as string; - webhookData.events = event; + return true; }, async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId !== undefined) { + const endpoint = `/automations/hooks/${webhookData.webhookId}`; + try { - await convertKitApiRequest.call(this, 'DELETE', endpoint, {}, {}); + + await convertKitApiRequest.call(this, 'DELETE', endpoint); + } catch (error) { + return false; } + delete webhookData.webhookId; - delete webhookData.events; } + return true; }, }, }; - async webhook(this: IWebhookFunctions): Promise { const returnData: IDataObject[] = []; returnData.push(this.getBodyData()); diff --git a/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts b/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts index 4844045e35..81c8e43b32 100644 --- a/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts +++ b/packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts @@ -39,12 +39,19 @@ export async function convertKitApiRequest(this: IExecuteFunctions | IExecuteSin delete options.body; } - console.log(options); + // it's a webhook so include the api secret on the body + if ((options.uri as string).includes('/automations/hooks')) { + options.body['api_secret'] = credentials.apiSecret; + } else { + qs.api_secret = credentials.apiSecret; + } + + if (Object.keys(options.qs).length === 0) { + delete options.qs; + } try { - qs.api_secret = credentials.apiSecret; - return await this.helpers.request!(options); } catch (error) { diff --git a/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts b/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts index 493b713cda..5f1186cd55 100644 --- a/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts +++ b/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts @@ -18,17 +18,17 @@ export const sequenceOperations = [ { name: 'Add Subscriber', value: 'addSubscriber', - description: 'Add a subscriber.', + description: 'Add a subscriber', }, { name: 'Get All', value: 'getAll', - description: 'Get all sequences.', + description: 'Get all sequences', }, { name: 'Get Subscriptions', value: 'getSubscriptions', - description: 'List subscriptions to a sequence including subscriber data.', + description: 'Get all subscriptions to a sequence including subscriber data', }, ], default: 'addSubscriber', From b3a85871068c0df1c143eefbe4cd78a88d70a116 Mon Sep 17 00:00:00 2001 From: ricardo Date: Mon, 10 Aug 2020 09:41:10 -0400 Subject: [PATCH 04/41] :zap: Improvements --- .../credentials/ConvertKitApi.credentials.ts | 2 +- .../ConvertKit/ConvertKitTrigger.node.ts | 4 ++- .../nodes/ConvertKit/FormDescription.ts | 36 +++++++++---------- .../nodes/ConvertKit/SequenceDescription.ts | 36 +++++++++---------- 4 files changed, 40 insertions(+), 38 deletions(-) diff --git a/packages/nodes-base/credentials/ConvertKitApi.credentials.ts b/packages/nodes-base/credentials/ConvertKitApi.credentials.ts index 1685c11a24..d7e8697555 100644 --- a/packages/nodes-base/credentials/ConvertKitApi.credentials.ts +++ b/packages/nodes-base/credentials/ConvertKitApi.credentials.ts @@ -6,7 +6,7 @@ import { export class ConvertKitApi implements ICredentialType { name = 'convertKitApi'; - displayName = 'ConvertKit Api'; + displayName = 'ConvertKit API'; properties = [ { displayName: 'API Secret', diff --git a/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts b/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts index cb5bdbeb2c..0332facf72 100644 --- a/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts +++ b/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts @@ -297,6 +297,8 @@ export class ConvertKitTrigger implements INodeType { }, }; + console.log(event); + if (event === 'subscriber.form_subscribe') { //@ts-ignore body.event['form_id'] = this.getNodeParameter('formId', 0); @@ -317,7 +319,7 @@ export class ConvertKitTrigger implements INodeType { body.event['product_id'] = this.getNodeParameter('productId', 0); } - if (event === 'subscriber.tag_add' || event === 'subscriber.tag_remove"') { + if (event === 'subscriber.tag_add' || event === 'subscriber.tag_remove') { //@ts-ignore body.event['tag_id'] = this.getNodeParameter('tagId', 0); } diff --git a/packages/nodes-base/nodes/ConvertKit/FormDescription.ts b/packages/nodes-base/nodes/ConvertKit/FormDescription.ts index f6e1a6f3e5..a8496805bc 100644 --- a/packages/nodes-base/nodes/ConvertKit/FormDescription.ts +++ b/packages/nodes-base/nodes/ConvertKit/FormDescription.ts @@ -37,24 +37,6 @@ export const formOperations = [ ] as INodeProperties[]; export const formFields = [ - { - displayName: 'Email', - name: 'email', - type: 'string', - required: true, - displayOptions: { - show: { - resource: [ - 'form', - ], - operation: [ - 'addSubscriber', - ], - }, - }, - default: '', - description: `The subscriber's email address.`, - }, { displayName: 'Form ID', name: 'id', @@ -77,6 +59,24 @@ export const formFields = [ default: '', description: 'Form ID.', }, + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'form', + ], + operation: [ + 'addSubscriber', + ], + }, + }, + default: '', + description: `The subscriber's email address.`, + }, { displayName: 'Additional Fields', name: 'additionalFields', diff --git a/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts b/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts index 5f1186cd55..42437cc155 100644 --- a/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts +++ b/packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts @@ -37,24 +37,6 @@ export const sequenceOperations = [ ] as INodeProperties[]; export const sequenceFields = [ - { - displayName: 'Email', - name: 'email', - type: 'string', - required: true, - displayOptions: { - show: { - resource: [ - 'sequence', - ], - operation: [ - 'addSubscriber', - ], - }, - }, - default: '', - description: `The subscriber's email address.`, - }, { displayName: 'Sequence ID', name: 'id', @@ -77,6 +59,24 @@ export const sequenceFields = [ default: '', description: 'Sequence ID.', }, + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'sequence', + ], + operation: [ + 'addSubscriber', + ], + }, + }, + default: '', + description: `The subscriber's email address.`, + }, { displayName: 'Return All', name: 'returnAll', From 4568eb21cc0a43f90725a397a549de95e65bdff9 Mon Sep 17 00:00:00 2001 From: ricardo Date: Mon, 10 Aug 2020 09:43:21 -0400 Subject: [PATCH 05/41] :zap: small fix --- packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts b/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts index 0332facf72..580bab7d56 100644 --- a/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts +++ b/packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts @@ -297,8 +297,6 @@ export class ConvertKitTrigger implements INodeType { }, }; - console.log(event); - if (event === 'subscriber.form_subscribe') { //@ts-ignore body.event['form_id'] = this.getNodeParameter('formId', 0); From 05258cc645e85805e10ff4f5e6b1e6283eff73ef Mon Sep 17 00:00:00 2001 From: Rupenieks Date: Thu, 20 Aug 2020 14:47:59 +0200 Subject: [PATCH 06/41] :white_check_mark: Credentials doc help link --- .../src/components/CredentialsEdit.vue | 122 +++++++++++++++++- 1 file changed, 120 insertions(+), 2 deletions(-) diff --git a/packages/editor-ui/src/components/CredentialsEdit.vue b/packages/editor-ui/src/components/CredentialsEdit.vue index a69ad78e24..4bea56cc2b 100644 --- a/packages/editor-ui/src/components/CredentialsEdit.vue +++ b/packages/editor-ui/src/components/CredentialsEdit.vue @@ -1,7 +1,29 @@