Add Lemlist node (#1506)

* 🎉 Register node and credentials

*  Add credentials file

* 🎨 Add SVG icon

*  Add generic functions file

*  Add preliminary node stub

*  Add resource description stubs

*  Extract get CSV from getAll

*  Implement lead:create

*  Implement all lead operations

*  Implement unsubscribe:create and delete

*  Preload campaigns

* 🔥 Remove logging

* 🔥 Remove operation per feedback

* 🎨 Prettify error message

*  Implement unsubscribe:getAll

*  Add trigger and small improvements

*  Minor improvement and fixes

Co-authored-by: ricardo <ricardoespinoza105@gmail.com>
Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
Iván Ovejero 2021-03-04 06:25:47 -03:00 committed by GitHub
parent c49fcdeed6
commit 7be61e2f23
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1257 additions and 0 deletions

View file

@ -0,0 +1,18 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class LemlistApi implements ICredentialType {
name = 'lemlistApi';
displayName = 'Lemlist API';
documentationUrl = 'lemlist';
properties = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View file

@ -0,0 +1,93 @@
import {
IExecuteFunctions,
IHookFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
} from 'n8n-workflow';
import {
OptionsWithUri,
} from 'request';
/**
* Make an authenticated API request to Lemlist.
*/
export async function lemlistApiRequest(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
method: string,
endpoint: string,
body: IDataObject = {},
qs: IDataObject = {},
option: IDataObject = {},
) {
const { apiKey } = this.getCredentials('lemlistApi') as {
apiKey: string,
};
const encodedApiKey = Buffer.from(':' + apiKey).toString('base64');
const options: OptionsWithUri = {
headers: {
'user-agent': 'n8n',
'Authorization': `Basic ${encodedApiKey}`,
},
method,
uri: `https://api.lemlist.com/api${endpoint}`,
qs,
body,
json: true,
};
if (!Object.keys(body).length) {
delete options.body;
}
if (!Object.keys(qs).length) {
delete options.qs;
}
if (Object.keys(option)) {
Object.assign(options, option);
}
try {
return await this.helpers.request!(options);
} catch (error) {
if (error?.response?.body) {
throw new Error(`Lemlist error response [${error.statusCode}]: ${error?.response?.body}`);
}
throw error;
}
}
/**
* Make an authenticated API request to Lemlist and return all results.
*/
export async function lemlistApiRequestAllItems(
this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions,
method: string,
endpoint: string,
) {
const returnData: IDataObject[] = [];
let responseData;
const qs: IDataObject = {};
qs.limit = 100;
qs.offset = 0;
do {
responseData = await lemlistApiRequest.call(this, method, endpoint, {}, qs);
returnData.push(...responseData);
qs.offset += qs.limit;
} while (
responseData.length !== 0
);
return returnData;
}

View file

@ -0,0 +1,337 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
activityFields,
activityOperations,
campaignFields,
campaignOperations,
leadFields,
leadOperations,
teamFields,
teamOperations,
unsubscribeFields,
unsubscribeOperations,
} from './descriptions';
import {
lemlistApiRequest,
lemlistApiRequestAllItems,
} from './GenericFunctions';
import {
isEmpty,
omit,
} from 'lodash';
export class Lemlist implements INodeType {
description: INodeTypeDescription = {
displayName: 'Lemlist',
name: 'lemlist',
icon: 'file:lemlist.svg',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume the Lemlist API',
defaults: {
name: 'Lemlist',
color: '#4d19ff',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'lemlistApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Activity',
value: 'activity',
},
{
name: 'Campaign',
value: 'campaign',
},
{
name: 'Lead',
value: 'lead',
},
{
name: 'Team',
value: 'team',
},
{
name: 'Unsubscribes',
value: 'unsubscribe',
},
],
default: 'activity',
description: 'Resource to consume',
},
...activityOperations,
...activityFields,
...campaignOperations,
...campaignFields,
...leadOperations,
...leadFields,
...teamOperations,
...teamFields,
...unsubscribeOperations,
...unsubscribeFields,
],
};
methods = {
loadOptions: {
async getCampaigns(this: ILoadOptionsFunctions) {
const campaigns = await lemlistApiRequest.call(this, 'GET', '/campaigns');
return campaigns.map(({ _id, name }: { _id: string, name: string }) => ({
name,
value: _id,
}));
},
},
};
async execute(this: IExecuteFunctions) {
const items = this.getInputData();
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
let responseData;
const returnData: IDataObject[] = [];
for (let i = 0; i < items.length; i++) {
try {
if (resource === 'activity') {
// *********************************************************************
// activity
// *********************************************************************
if (operation === 'getAll') {
// ----------------------------------
// activity: getAll
// ----------------------------------
// https://developer.lemlist.com/#activities
const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
const qs = {} as IDataObject;
const filters = this.getNodeParameter('filters', i);
if (!isEmpty(filters)) {
Object.assign(qs, filters);
}
responseData = await lemlistApiRequest.call(this, 'GET', '/activities', {}, qs);
if (returnAll === false) {
const limit = this.getNodeParameter('limit', 0) as number;
responseData = responseData.slice(0, limit);
}
}
} else if (resource === 'campaign') {
// *********************************************************************
// campaign
// *********************************************************************
if (operation === 'getAll') {
// ----------------------------------
// campaign: getAll
// ----------------------------------
// https://developer.lemlist.com/#list-all-campaigns
responseData = await lemlistApiRequest.call(this, 'GET', '/campaigns');
const returnAll = this.getNodeParameter('returnAll', i);
if (!returnAll) {
const limit = this.getNodeParameter('limit', i);
responseData = responseData.slice(0, limit);
}
}
} else if (resource === 'lead') {
// *********************************************************************
// lead
// *********************************************************************
if (operation === 'create') {
// ----------------------------------
// lead: create
// ----------------------------------
// https://developer.lemlist.com/#add-a-lead-in-a-campaign
const qs = {} as IDataObject;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.deduplicate !== undefined) {
qs.deduplicate = additionalFields.deduplicate;
}
const body = {} as IDataObject;
const remainingAdditionalFields = omit(additionalFields, 'deduplicate');
if (!isEmpty(remainingAdditionalFields)) {
Object.assign(body, remainingAdditionalFields);
}
const campaignId = this.getNodeParameter('campaignId', i);
const email = this.getNodeParameter('email', i);
const endpoint = `/campaigns/${campaignId}/leads/${email}`;
responseData = await lemlistApiRequest.call(this, 'POST', endpoint, body, qs);
} else if (operation === 'delete') {
// ----------------------------------
// lead: delete
// ----------------------------------
// https://developer.lemlist.com/#delete-a-lead-from-a-campaign
const campaignId = this.getNodeParameter('campaignId', i);
const email = this.getNodeParameter('email', i);
const endpoint = `/campaigns/${campaignId}/leads/${email}`;
responseData = await lemlistApiRequest.call(this, 'DELETE', endpoint, {}, { action: 'remove' });
} else if (operation === 'get') {
// ----------------------------------
// lead: get
// ----------------------------------
// https://developer.lemlist.com/#get-a-specific-lead-by-email
const email = this.getNodeParameter('email', i);
responseData = await lemlistApiRequest.call(this, 'GET', `/leads/${email}`);
} else if (operation === 'unsubscribe') {
// ----------------------------------
// lead: unsubscribe
// ----------------------------------
// https://developer.lemlist.com/#unsubscribe-a-lead-from-a-campaign
const campaignId = this.getNodeParameter('campaignId', i);
const email = this.getNodeParameter('email', i);
const endpoint = `/campaigns/${campaignId}/leads/${email}`;
responseData = await lemlistApiRequest.call(this, 'DELETE', endpoint);
}
} else if (resource === 'team') {
// *********************************************************************
// team
// *********************************************************************
if (operation === 'get') {
// ----------------------------------
// team: get
// ----------------------------------
// https://developer.lemlist.com/#team
responseData = await lemlistApiRequest.call(this, 'GET', '/team');
}
} else if (resource === 'unsubscribe') {
// *********************************************************************
// unsubscribe
// *********************************************************************
if (operation === 'add') {
// ----------------------------------
// unsubscribe: Add
// ----------------------------------
// https://developer.lemlist.com/#add-an-email-address-in-the-unsubscribes
const email = this.getNodeParameter('email', i);
responseData = await lemlistApiRequest.call(this, 'POST', `/unsubscribes/${email}`);
} else if (operation === 'delete') {
// ----------------------------------
// unsubscribe: delete
// ----------------------------------
// https://developer.lemlist.com/#delete-an-email-address-from-the-unsubscribes
const email = this.getNodeParameter('email', i);
responseData = await lemlistApiRequest.call(this, 'DELETE', `/unsubscribes/${email}`);
} else if (operation === 'getAll') {
// ----------------------------------
// unsubscribe: getAll
// ----------------------------------
// https://developer.lemlist.com/#list-all-unsubscribes
const returnAll = this.getNodeParameter('returnAll', i);
if (returnAll) {
responseData = await lemlistApiRequestAllItems.call(this, 'GET', '/unsubscribes');
} else {
const qs = {
limit: this.getNodeParameter('limit', i) as number,
};
responseData = await lemlistApiRequest.call(this, 'GET', '/unsubscribes', {}, qs);
}
}
}
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ error: error.toString() });
continue;
}
throw error;
}
Array.isArray(responseData)
? returnData.push(...responseData)
: returnData.push(responseData);
}
return [this.helpers.returnJsonArray(returnData)];
}
}

View file

@ -0,0 +1,175 @@
import {
IHookFunctions,
IWebhookFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeType,
INodeTypeDescription,
IWebhookResponseData,
} from 'n8n-workflow';
import {
lemlistApiRequest,
} from './GenericFunctions';
export class LemlistTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Lemlist Trigger',
name: 'lemlistTrigger',
icon: 'file:lemlist.svg',
group: ['trigger'],
version: 1,
subtitle: '={{$parameter["event"]}}',
description: 'Handle Lemlist events via webhooks',
defaults: {
name: 'Lemlist Trigger',
color: '#4d19ff',
},
inputs: [],
outputs: ['main'],
credentials: [
{
name: 'lemlistApi',
required: true,
},
],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName: 'Event',
name: 'event',
type: 'options',
required: true,
default: '',
options: [
{
name: 'Email Bounced',
value: 'emailsBounced',
},
{
name: 'Email Clicked',
value: 'emailsClicked',
},
{
name: 'Email Opened',
value: 'emailsOpened',
},
{
name: 'Email Replied',
value: 'emailsReplied',
},
{
name: 'Email Send Failed',
value: 'emailsSendFailed',
},
{
name: 'Email Sent',
value: 'emailsSent',
},
{
name: 'Email Unsubscribed',
value: 'emailsUnsubscribed',
},
],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
options: [
{
displayName: 'Campaing ID',
name: 'campaignId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getCampaigns',
},
default: '',
description: ` We'll call this hook only for this campaignId.`,
},
{
displayName: 'Is First',
name: 'isFirst',
type: 'boolean',
default: false,
description: `We'll call this hook only the first time this activity happened.`,
},
],
},
],
};
methods = {
loadOptions: {
async getCampaigns(this: ILoadOptionsFunctions) {
const campaigns = await lemlistApiRequest.call(this, 'GET', '/campaigns');
return campaigns.map(({ _id, name }: { _id: string, name: string }) => ({
name,
value: _id,
}));
},
},
};
// @ts-ignore
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
const webhookUrl = this.getNodeWebhookUrl('default');
const webhooks = await lemlistApiRequest.call(this, 'GET', '/hooks');
for (const webhook of webhooks) {
if (webhook.targetUrl === webhookUrl) {
await lemlistApiRequest.call(this, 'DELETE', `/hooks/${webhookData.webhookId}`);
return false;
}
}
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default');
const webhookData = this.getWorkflowStaticData('node');
const options = this.getNodeParameter('options') as IDataObject;
const event = this.getNodeParameter('event') as string[];
const body: IDataObject = {
targetUrl: webhookUrl,
event,
};
Object.assign(body, options);
const webhook = await lemlistApiRequest.call(this, 'POST', '/hooks', body);
webhookData.webhookId = webhook._id;
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
try {
await lemlistApiRequest.call(this, 'DELETE', `/hooks/${webhookData.webhookId}`);
} catch (error) {
return false;
}
delete webhookData.webhookId;
return true;
},
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const req = this.getRequestObject();
return {
workflowData: [
this.helpers.returnJsonArray(req.body),
],
};
}
}

View file

@ -0,0 +1,140 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const activityOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
default: 'getAll',
description: 'Operation to perform',
options: [
{
name: 'Get All',
value: 'getAll',
},
],
displayOptions: {
show: {
resource: [
'activity',
],
},
},
},
] as INodeProperties[];
export const activityFields = [
// ----------------------------------
// activity: getAll
// ----------------------------------
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
description: 'Return all results.',
displayOptions: {
show: {
resource: [
'activity',
],
operation: [
'getAll',
],
},
},
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 5,
description: 'The number of results to return.',
typeOptions: {
minValue: 1,
maxValue: 1000,
},
displayOptions: {
show: {
resource: [
'activity',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
placeholder: 'Add Filter',
default: {},
displayOptions: {
show: {
resource: [
'activity',
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Campaign ID',
name: 'campaignId',
type: 'options',
required: true,
default: '',
typeOptions: {
loadOptionsMethod: 'getCampaigns',
},
description: 'ID of the campaign to retrieve activity for.',
},
{
displayName: 'Type',
name: 'type',
type: 'options',
default: 'emailsOpened',
description: 'Type of activity to retrieve.',
options: [
{
name: 'Emails Bounced',
value: 'emailsBounced',
},
{
name: 'Emails Clicked',
value: 'emailsClicked',
},
{
name: 'Emails Opened',
value: 'emailsOpened',
},
{
name: 'Emails Replied',
value: 'emailsReplied',
},
{
name: 'Emails Send Failed',
value: 'emailsSendFailed',
},
{
name: 'Emails Sent',
value: 'emailsSent',
},
{
name: 'Emails Unsubscribed',
value: 'emailsUnsubscribed',
},
],
},
],
},
] as INodeProperties[];

View file

@ -0,0 +1,73 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const campaignOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
default: 'getAll',
description: 'Operation to perform',
options: [
{
name: 'Get All',
value: 'getAll',
},
],
displayOptions: {
show: {
resource: [
'campaign',
],
},
},
},
] as INodeProperties[];
export const campaignFields = [
// ----------------------------------
// campaign: getAll
// ----------------------------------
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
description: 'Return all results.',
displayOptions: {
show: {
resource: [
'campaign',
],
operation: [
'getAll',
],
},
},
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 5,
description: 'The number of results to return.',
typeOptions: {
minValue: 1,
maxValue: 1000,
},
displayOptions: {
show: {
resource: [
'campaign',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
},
] as INodeProperties[];

View file

@ -0,0 +1,234 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const leadOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
default: 'create',
description: 'Operation to perform',
options: [
{
name: 'Create',
value: 'create',
},
{
name: 'Delete',
value: 'delete',
},
{
name: 'Get',
value: 'get',
},
{
name: 'Unsubscribe',
value: 'unsubscribe',
},
],
displayOptions: {
show: {
resource: [
'lead',
],
},
},
},
] as INodeProperties[];
export const leadFields = [
// ----------------------------------
// lead: create
// ----------------------------------
{
displayName: 'Campaign ID',
name: 'campaignId',
type: 'options',
required: true,
default: [],
typeOptions: {
loadOptionsMethod: 'getCampaigns',
},
description: 'ID of the campaign to create the lead under.',
displayOptions: {
show: {
resource: [
'lead',
],
operation: [
'create',
],
},
},
},
{
displayName: 'Email',
name: 'email',
type: 'string',
default: '',
description: 'Email of the lead to create.',
displayOptions: {
show: {
resource: [
'lead',
],
operation: [
'create',
],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'lead',
],
operation: [
'create',
],
},
},
options: [
{
displayName: 'Company Name',
name: 'companyName',
type: 'string',
default: '',
description: 'Company name of the lead to create.',
},
{
displayName: 'Deduplicate',
name: 'deduplicate',
type: 'boolean',
default: false,
description: 'Do not insert if this email is already present in another campaign.',
},
{
displayName: 'First Name',
name: 'firstName',
type: 'string',
default: '',
description: 'First name of the lead to create.',
},
{
displayName: 'Last Name',
name: 'lastName',
type: 'string',
default: '',
description: 'Last name of the lead to create.',
},
],
},
// ----------------------------------
// lead: delete
// ----------------------------------
{
displayName: 'Campaign ID',
name: 'campaignId',
type: 'options',
required: true,
default: [],
typeOptions: {
loadOptionsMethod: 'getCampaigns',
},
description: 'ID of the campaign to remove the lead from.',
displayOptions: {
show: {
resource: [
'lead',
],
operation: [
'delete',
],
},
},
},
{
displayName: 'Email',
name: 'email',
type: 'string',
default: '',
description: 'Email of the lead to delete.',
displayOptions: {
show: {
resource: [
'lead',
],
operation: [
'delete',
],
},
},
},
// ----------------------------------
// lead: get
// ----------------------------------
{
displayName: 'Email',
name: 'email',
type: 'string',
default: '',
description: 'Email of the lead to retrieve.',
displayOptions: {
show: {
resource: [
'lead',
],
operation: [
'get',
],
},
},
},
// ----------------------------------
// lead: unsubscribe
// ----------------------------------
{
displayName: 'Campaign ID',
name: 'campaignId',
type: 'options',
required: true,
default: [],
typeOptions: {
loadOptionsMethod: 'getCampaigns',
},
description: 'ID of the campaign to unsubscribe the lead from.',
displayOptions: {
show: {
resource: [
'lead',
],
operation: [
'unsubscribe',
],
},
},
},
{
displayName: 'Email',
name: 'email',
type: 'string',
default: '',
description: 'Email of the lead to unsubscribe.',
displayOptions: {
show: {
resource: [
'lead',
],
operation: [
'unsubscribe',
],
},
},
},
] as INodeProperties[];

View file

@ -0,0 +1,33 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const teamOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
default: 'get',
description: 'Operation to perform',
options: [
{
name: 'Get',
value: 'get',
},
],
displayOptions: {
show: {
resource: [
'team',
],
},
},
},
] as INodeProperties[];
export const teamFields = [
// ----------------------------------
// team: get
// ----------------------------------
] as INodeProperties[];

View file

@ -0,0 +1,123 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const unsubscribeOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
default: 'add',
description: 'Operation to perform',
options: [
{
name: 'Add',
value: 'add',
},
{
name: 'Delete',
value: 'delete',
},
{
name: 'Get All',
value: 'getAll',
},
],
displayOptions: {
show: {
resource: [
'unsubscribe',
],
},
},
},
] as INodeProperties[];
export const unsubscribeFields = [
// ----------------------------------
// unsubscribe: add
// ----------------------------------
{
displayName: 'Email',
name: 'email',
type: 'string',
default: '',
description: 'Email to add to the unsubscribes.',
displayOptions: {
show: {
resource: [
'unsubscribe',
],
operation: [
'add',
],
},
},
},
// ----------------------------------
// unsubscribe: delete
// ----------------------------------
{
displayName: 'Email',
name: 'email',
type: 'string',
default: '',
description: 'Email to delete from the unsubscribes.',
displayOptions: {
show: {
resource: [
'unsubscribe',
],
operation: [
'delete',
],
},
},
},
// ----------------------------------
// unsubscribe: getAll
// ----------------------------------
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
description: 'Return all results.',
displayOptions: {
show: {
resource: [
'unsubscribe',
],
operation: [
'getAll',
],
},
},
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 5,
description: 'The number of results to return.',
typeOptions: {
minValue: 1,
maxValue: 1000,
},
displayOptions: {
show: {
resource: [
'unsubscribe',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
},
] as INodeProperties[];

View file

@ -0,0 +1,5 @@
export * from './ActivityDescription';
export * from './CampaignDescription';
export * from './LeadDescription';
export * from './TeamDescription';
export * from './UnsubscribeDescription';

View file

@ -0,0 +1,23 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="200.000000pt" height="200.000000pt" viewBox="0 0 200.000000 200.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,200.000000) scale(0.100000,-0.100000)"
fill="#4d19ff" stroke="none">
<path d="M315 1986 c-127 -40 -218 -119 -274 -236 l-36 -75 0 -415 c0 -403 1
-418 23 -500 27 -100 93 -242 152 -331 24 -35 82 -102 130 -149 135 -133 296
-215 520 -264 64 -15 131 -16 465 -14 l390 3 70 34 c88 43 163 119 207 208
l33 68 3 655 c3 697 3 696 -46 795 -52 105 -122 167 -236 210 l-66 25 -648 -1
c-494 0 -656 -3 -687 -13z m402 -590 l28 -24 5 -249 c8 -377 -3 -366 371 -371
229 -3 248 -4 268 -23 27 -25 35 -60 24 -96 -17 -51 -34 -54 -323 -51 l-265 3
-53 30 c-73 40 -147 123 -169 188 -13 41 -17 101 -21 285 -4 252 0 276 46 312
33 26 55 25 89 -4z m654 9 c57 -30 62 -112 9 -140 -23 -12 -71 -15 -218 -15
-176 0 -191 1 -216 21 -48 38 -29 119 34 142 8 3 93 6 188 6 138 1 179 -2 203
-14z m-61 -345 c13 -13 20 -33 20 -60 0 -27 -7 -47 -20 -60 -18 -18 -33 -20
-182 -20 -152 0 -163 1 -185 22 -30 28 -31 83 -1 115 21 22 27 23 185 23 150
0 165 -2 183 -20z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -128,6 +128,7 @@
"dist/credentials/JotFormApi.credentials.js",
"dist/credentials/Kafka.credentials.js",
"dist/credentials/KeapOAuth2Api.credentials.js",
"dist/credentials/LemlistApi.credentials.js",
"dist/credentials/LineNotifyOAuth2Api.credentials.js",
"dist/credentials/LingvaNexApi.credentials.js",
"dist/credentials/LinkedInOAuth2Api.credentials.js",
@ -387,6 +388,8 @@
"dist/nodes/Kafka/KafkaTrigger.node.js",
"dist/nodes/Keap/Keap.node.js",
"dist/nodes/Keap/KeapTrigger.node.js",
"dist/nodes/Lemlist/Lemlist.node.js",
"dist/nodes/Lemlist/LemlistTrigger.node.js",
"dist/nodes/Line/Line.node.js",
"dist/nodes/LingvaNex/LingvaNex.node.js",
"dist/nodes/LinkedIn/LinkedIn.node.js",