Add Emelia nodes (#1455)

* Emelia node added

* Minor improvements on Emelia nodes

* Fix nodes and credentials listing

* Fix multi-line imports

* Apply cosmetic changes to node description

* Apply cosmetic changes to node execute method

* Fix linting details

* Apply cosmetic changes to trigger node

* Replace PNG with SVG icon

* Bring generic functions in line with codebase

* Refactor resources and add operations

* Fix typo in GraphQL call function

* Add campaign description

* Add contact list description

*  Improvements

*  Minor improvements to Emelia Nodes

Co-authored-by: Charles LECALIER <clecalie@student.42.fr>
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-02-20 17:12:55 -03:00 committed by GitHub
parent 4dce9e2cd1
commit 4d4ab7943b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 1240 additions and 0 deletions

View file

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

View file

@ -0,0 +1,326 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const campaignOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
default: 'get',
description: 'Operation to perform',
options: [
{
name: 'Add Contact',
value: 'addContact',
},
{
name: 'Create',
value: 'create',
},
{
name: 'Get',
value: 'get',
},
{
name: 'Get All',
value: 'getAll',
},
{
name: 'Pause',
value: 'pause',
},
{
name: 'Start',
value: 'start',
},
],
displayOptions: {
show: {
resource: [
'campaign',
],
},
},
},
] as INodeProperties[];
export const campaignFields = [
// ----------------------------------
// campaign: addContact
// ----------------------------------
{
displayName: 'Campaign ID',
name: 'campaignId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getCampaigns',
},
default: [],
required: true,
description: 'The ID of the campaign to add the contact to.',
displayOptions: {
show: {
resource: [
'campaign',
],
operation: [
'addContact',
],
},
},
},
{
displayName: 'Contact Email',
name: 'contactEmail',
type: 'string',
required: true,
default: '',
description: 'The email of the contact to add to the campaign.',
displayOptions: {
show: {
resource: [
'campaign',
],
operation: [
'addContact',
],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'campaign',
],
operation: [
'addContact',
],
},
},
options: [
{
displayName: 'Custom Fields',
name: 'customFieldsUi',
placeholder: 'Add Custom Field',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
description: 'Filter by custom fields ',
default: {},
options: [
{
name: 'customFieldsValues',
displayName: 'Custom Field',
values: [
{
displayName: 'Field Name',
name: 'fieldName',
type: 'string',
default: '',
description: 'The name of the field to add custom field to.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'The value to set on custom field.',
},
],
},
],
},
{
displayName: 'First Name',
name: 'firstName',
type: 'string',
default: '',
description: 'First name of the contact to add.',
},
{
displayName: 'Last Name',
name: 'lastName',
type: 'string',
default: '',
description: 'Last name of the contact to add.',
},
{
displayName: 'Last Contacted',
name: 'lastContacted',
type: 'string',
default: '',
description: 'Last contacted date of the contact to add.',
},
{
displayName: 'Last Open',
name: 'lastOpen',
type: 'string',
default: '',
description: 'Last opened date of the contact to add.',
},
{
displayName: 'Last Replied',
name: 'lastReplied',
type: 'string',
default: '',
description: 'Last replied date of the contact to add.',
},
{
displayName: 'Mails Sent',
name: 'mailsSent',
type: 'number',
default: 0,
description: 'Number of emails sent to the contact to add.',
},
{
displayName: 'Phone Number',
name: 'phoneNumber',
type: 'string',
default: '',
description: 'Phone number of the contact to add.',
},
],
},
// ----------------------------------
// campaign: create
// ----------------------------------
{
displayName: 'Campaign Name',
name: 'campaignName',
type: 'string',
required: true,
default: '',
description: 'The name of the campaign to create.',
displayOptions: {
show: {
resource: [
'campaign',
],
operation: [
'create',
],
},
},
},
// ----------------------------------
// campaign: get
// ----------------------------------
{
displayName: 'Campaign ID',
name: 'campaignId',
type: 'string',
default: '',
required: true,
description: 'The ID of the campaign to retrieve.',
displayOptions: {
show: {
resource: [
'campaign',
],
operation: [
'get',
],
},
},
},
// ----------------------------------
// 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: 100,
description: 'The number of results to return.',
typeOptions: {
minValue: 1,
maxValue: 100,
},
displayOptions: {
show: {
resource: [
'campaign',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
},
// ----------------------------------
// campaign: pause
// ----------------------------------
{
displayName: 'Campaign ID',
name: 'campaignId',
type: 'string',
default: '',
required: true,
description: 'The ID of the campaign to pause.<br>The campaign must be in RUNNING mode.',
displayOptions: {
show: {
resource: [
'campaign',
],
operation: [
'pause',
],
},
},
},
// ----------------------------------
// campaign: start
// ----------------------------------
{
displayName: 'Campaign ID',
name: 'campaignId',
type: 'string',
default: '',
required: true,
description: 'The ID of the campaign to start.<br>Email provider and contacts must be set.',
displayOptions: {
show: {
resource: [
'campaign',
],
operation: [
'start',
],
},
},
},
] as INodeProperties[];

View file

@ -0,0 +1,221 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const contactListOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
default: 'get',
description: 'Operation to perform',
options: [
{
name: 'Add',
value: 'add',
},
{
name: 'Get All',
value: 'getAll',
},
],
displayOptions: {
show: {
resource: [
'contactList',
],
},
},
},
] as INodeProperties[];
export const contactListFields = [
// ----------------------------------
// contactList: add
// ----------------------------------
{
displayName: 'Contact List ID',
name: 'contactListId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getContactLists',
},
default: [],
required: true,
description: 'The ID of the contact list to add the contact to.',
displayOptions: {
show: {
resource: [
'contactList',
],
operation: [
'add',
],
},
},
},
{
displayName: 'Contact Email',
name: 'contactEmail',
type: 'string',
required: true,
default: '',
description: 'The email of the contact to add to the contact list.',
displayOptions: {
show: {
resource: [
'contactList',
],
operation: [
'add',
],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'contactList',
],
operation: [
'add',
],
},
},
options: [
{
displayName: 'Custom Fields',
name: 'customFieldsUi',
placeholder: 'Add Custom Field',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
description: 'Filter by custom fields ',
default: {},
options: [
{
name: 'customFieldsValues',
displayName: 'Custom Field',
values: [
{
displayName: 'Field Name',
name: 'fieldName',
type: 'string',
default: '',
description: 'The name of the field to add custom field to.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'The value to set on custom field.',
},
],
},
],
},
{
displayName: 'First Name',
name: 'firstName',
type: 'string',
default: '',
description: 'First name of the contact to add.',
},
{
displayName: 'Last Name',
name: 'lastName',
type: 'string',
default: '',
description: 'Last name of the contact to add.',
},
{
displayName: 'Last Contacted',
name: 'lastContacted',
type: 'dateTime',
default: '',
description: 'Last contacted date of the contact to add.',
},
{
displayName: 'Last Open',
name: 'lastOpen',
type: 'dateTime',
default: '',
description: 'Last opened date of the contact to add.',
},
{
displayName: 'Last Replied',
name: 'lastReplied',
type: 'dateTime',
default: '',
description: 'Last replied date of the contact to add.',
},
{
displayName: 'Mails Sent',
name: 'mailsSent',
type: 'number',
default: 0,
description: 'Number of emails sent to the contact to add.',
},
{
displayName: 'Phone Number',
name: 'phoneNumber',
type: 'string',
default: '',
description: 'Phone number of the contact to add.',
},
],
},
// ----------------------------------
// contactList: getAll
// ----------------------------------
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
description: 'Return all results.',
displayOptions: {
show: {
resource: [
'contactList',
],
operation: [
'getAll',
],
},
},
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 100,
description: 'The number of results to return.',
typeOptions: {
minValue: 1,
maxValue: 100,
},
displayOptions: {
show: {
resource: [
'contactList',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
},
] as INodeProperties[];

View file

@ -0,0 +1,387 @@
import {
IExecuteFunctions
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription
} from 'n8n-workflow';
import {
emeliaGraphqlRequest,
loadResource,
} from './GenericFunctions';
import {
campaignFields,
campaignOperations,
} from './CampaignDescription';
import {
contactListFields,
contactListOperations,
} from './ContactListDescription';
import {
isEmpty,
} from 'lodash';
export class Emelia implements INodeType {
description: INodeTypeDescription = {
displayName: 'Emelia',
name: 'emelia',
icon: 'file:emelia.svg',
group: ['input'],
version: 1,
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
description: 'Consume the Emelia API',
defaults: {
name: 'Emelia',
color: '#e18063',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'emeliaApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Campaign',
value: 'campaign',
},
{
name: 'Contact List',
value: 'contactList',
},
],
default: 'campaign',
required: true,
description: 'The resource to operate on.',
},
...campaignOperations,
...campaignFields,
...contactListOperations,
...contactListFields,
],
};
methods = {
loadOptions: {
async getCampaigns(this: ILoadOptionsFunctions) {
return loadResource.call(this, 'campaign');
},
async getContactLists(this: ILoadOptionsFunctions) {
return loadResource.call(this, 'contactList');
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
for (let i = 0; i < items.length; i++) {
try {
if (resource === 'campaign') {
// **********************************
// campaign
// **********************************
if (operation === 'addContact') {
// ----------------------------------
// campaign: addContact
// ----------------------------------
const contact = {
email: this.getNodeParameter('contactEmail', i) as string,
};
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (!isEmpty(additionalFields)) {
Object.assign(contact, additionalFields);
}
if (additionalFields.customFieldsUi) {
const customFields = (additionalFields.customFieldsUi as IDataObject || {}).customFieldsValues as IDataObject[] || [];
const data = customFields.reduce((obj, value) => Object.assign(obj, { [`${value.fieldName}`]: value.value }), {});
Object.assign(contact, data);
//@ts-ignore
delete contact.customFieldsUi;
}
const responseData = await emeliaGraphqlRequest.call(this, {
query: `
mutation AddContactToCampaignHook($id: ID!, $contact: JSON!) {
addContactToCampaignHook(id: $id, contact: $contact)
}`,
operationName: 'AddContactToCampaignHook',
variables: {
id: this.getNodeParameter('campaignId', i),
contact,
},
});
returnData.push({ contactId: responseData.data.addContactToCampaignHook });
} else if (operation === 'create') {
// ----------------------------------
// campaign: create
// ----------------------------------
const responseData = await emeliaGraphqlRequest.call(this, {
operationName: 'createCampaign',
query: `
mutation createCampaign($name: String!) {
createCampaign(name: $name) {
_id
name
status
createdAt
provider
startAt
estimatedEnd
}
}`,
variables: {
name: this.getNodeParameter('campaignName', i),
},
});
returnData.push(responseData.data.createCampaign);
} else if (operation === 'get') {
// ----------------------------------
// campaign: get
// ----------------------------------
const responseData = await emeliaGraphqlRequest.call(this, {
query: `
query campaign($id: ID!){
campaign(id: $id){
_id
name
status
createdAt
schedule{
dailyContact
dailyLimit
minInterval
maxInterval
trackLinks
trackOpens
timeZone
days
start
end
eventToStopMails
}
provider
startAt
recipients{
total_count
}
estimatedEnd
}
}`,
operationName: 'campaign',
variables: {
id: this.getNodeParameter('campaignId', i),
},
});
returnData.push(responseData.data.campaign);
} else if (operation === 'getAll') {
// ----------------------------------
// campaign: getAll
// ----------------------------------
const responseData = await emeliaGraphqlRequest.call(this, {
query: `
query all_campaigns {
all_campaigns {
_id
name
status
createdAt
stats {
mailsSent
uniqueOpensPercent
opens
linkClickedPercent
repliedPercent
bouncedPercent
unsubscribePercent
progressPercent
}
}
}`,
operationName: 'all_campaigns',
});
let campaigns = responseData.data.all_campaigns;
const returnAll = this.getNodeParameter('returnAll', i);
if (!returnAll) {
const limit = this.getNodeParameter('limit', i) as number;
campaigns = campaigns.slice(0, limit);
}
returnData.push(...campaigns);
} else if (operation === 'pause') {
// ----------------------------------
// campaign: pause
// ----------------------------------
await emeliaGraphqlRequest.call(this, {
query: `
mutation pauseCampaign($id: ID!) {
pauseCampaign(id: $id)
}`,
operationName: 'pauseCampaign',
variables: {
id: this.getNodeParameter('campaignId', i),
},
});
returnData.push({ success: true });
} else if (operation === 'start') {
// ----------------------------------
// campaign: start
// ----------------------------------
await emeliaGraphqlRequest.call(this, {
query: `
mutation startCampaign($id: ID!) {
startCampaign(id: $id)
}`,
operationName: 'startCampaign',
variables: {
id: this.getNodeParameter('campaignId', i),
},
});
returnData.push({ success: true });
}
} else if (resource === 'contactList') {
// **********************************
// ContactList
// **********************************
if (operation === 'add') {
// ----------------------------------
// contactList: add
// ----------------------------------
const contact = {
email: this.getNodeParameter('contactEmail', i) as string,
};
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (!isEmpty(additionalFields)) {
Object.assign(contact, additionalFields);
}
if (additionalFields.customFieldsUi) {
const customFields = (additionalFields.customFieldsUi as IDataObject || {}).customFieldsValues as IDataObject[] || [];
const data = customFields.reduce((obj, value) => Object.assign(obj, { [`${value.fieldName}`]: value.value }), {});
Object.assign(contact, data);
//@ts-ignore
delete contact.customFieldsUi;
}
const responseData = await emeliaGraphqlRequest.call(this, {
query: `
mutation AddContactsToListHook($id: ID!, $contact: JSON!) {
addContactsToListHook(id: $id, contact: $contact)
}`,
operationName: 'AddContactsToListHook',
variables: {
id: this.getNodeParameter('contactListId', i),
contact,
},
});
returnData.push({ contactId: responseData.data.addContactsToListHook });
} else if (operation === 'getAll') {
// ----------------------------------
// contactList: getAll
// ----------------------------------
const responseData = await emeliaGraphqlRequest.call(this, {
query: `
query contact_lists{
contact_lists{
_id
name
contactCount
fields
usedInCampaign
}
}`,
operationName: 'contact_lists',
});
let contactLists = responseData.data.contact_lists;
const returnAll = this.getNodeParameter('returnAll', i);
if (!returnAll) {
const limit = this.getNodeParameter('limit', i) as number;
contactLists = contactLists.slice(0, limit);
}
returnData.push(...contactLists);
}
}
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ error: error.message });
continue;
}
throw error;
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

View file

@ -0,0 +1,180 @@
import {
IHookFunctions,
ILoadOptionsFunctions,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
IWebhookFunctions,
IWebhookResponseData,
} from 'n8n-workflow';
import {
emeliaApiRequest,
emeliaGraphqlRequest,
} from './GenericFunctions';
interface Campaign {
_id: string;
name: string;
}
export class EmeliaTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Emelia Trigger',
name: 'emeliaTrigger',
icon: 'file:emelia.svg',
group: ['trigger'],
version: 1,
description: 'Handle Emelia campaign activity events via webhooks',
defaults: {
name: 'Emelia Trigger',
color: '#e18063',
},
inputs: [],
outputs: ['main'],
credentials: [
{
name: 'emeliaApi',
required: true,
},
],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName: 'Campaign',
name: 'campaignId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getCampaigns',
},
required: true,
default: '',
},
{
displayName: 'Events',
name: 'events',
type: 'multiOptions',
required: true,
default: [],
options: [
{
name: 'Email Bounced',
value: 'bounced',
},
{
name: 'Email Opened',
value: 'opened',
},
{
name: 'Email Replied',
value: 'replied',
},
{
name: 'Email Sent',
value: 'sent',
},
{
name: 'Link Clicked',
value: 'clicked',
},
{
name: 'Unsubscribed Contact',
value: 'unsubscribed',
},
],
},
],
};
methods = {
loadOptions: {
async getCampaigns(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const responseData = await emeliaGraphqlRequest.call(this, {
query: `
query GetCampaigns {
campaigns {
_id
name
}
}`,
operationName: 'GetCampaigns',
variables: '{}',
});
return responseData.data.campaigns.map(
(campaign: Campaign) => ({
name: campaign.name,
value: campaign._id,
}),
);
},
},
};
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default') as string;
const campaignId = this.getNodeParameter('campaignId') as string;
const { webhooks } = await emeliaApiRequest.call(this, 'GET', '/webhook');
for (const webhook of webhooks) {
if (webhook.url === webhookUrl && webhook.campaignId === campaignId) {
return true;
}
}
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default') as string;
const webhookData = this.getWorkflowStaticData('node');
const events = this.getNodeParameter('events') as string[];
const campaignId = this.getNodeParameter('campaignId') as string;
const body = {
hookUrl: webhookUrl,
events: events.map(e => e.toUpperCase()),
campaignId,
};
const { webhookId } = await emeliaApiRequest.call(this, 'POST', '/webhook/webhook', body);
webhookData.webhookId = webhookId;
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
const webhookUrl = this.getNodeWebhookUrl('default') as string;
const campaignId = this.getNodeParameter('campaignId') as string;
try {
const body = {
hookUrl: webhookUrl,
campaignId,
};
await emeliaApiRequest.call(this, 'DELETE', '/webhook/webhook', body);
} 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,104 @@
import {
IExecuteFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IHookFunctions,
INodePropertyOptions,
} from 'n8n-workflow';
/**
* Make an authenticated GraphQL request to Emelia.
*/
export async function emeliaGraphqlRequest(
this: IExecuteFunctions | ILoadOptionsFunctions,
body: object = {},
) {
const response = await emeliaApiRequest.call(this, 'POST', '/graphql', body);
if (response.errors) {
throw new Error(`Emelia error message: ${response.errors[0].message}`);
}
return response;
}
/**
* Make an authenticated REST API request to Emelia, used for trigger node.
*/
export async function emeliaApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions,
method: string,
endpoint: string,
body: object = {},
qs: object = {},
) {
const { apiKey } = this.getCredentials('emeliaApi') as { apiKey: string };
const options = {
headers: {
Authorization: apiKey,
},
method,
body,
qs,
uri: `https://graphql.emelia.io${endpoint}`,
json: true,
};
try {
return await this.helpers.request!.call(this, options);
} catch (error) {
if (error?.response?.body?.error) {
const { error: errorMessage } = error.response.body;
throw new Error(
`Emelia error response [${error.statusCode}]: ${errorMessage}`,
);
}
throw error;
}
}
/**
* Load resources so that the user can select them easily.
*/
export async function loadResource(
this: ILoadOptionsFunctions,
resource: 'campaign' | 'contactList',
): Promise<INodePropertyOptions[]> {
const mapping: { [key in 'campaign' | 'contactList']: { query: string, key: string } } = {
campaign: {
query: `
query GetCampaigns {
campaigns {
_id
name
}
}`,
key: 'campaigns',
},
contactList: {
query: `
query GetContactLists {
contact_lists {
_id
name
}
}`,
key: 'contact_lists',
},
};
const responseData = await emeliaGraphqlRequest.call(this, { query: mapping[resource].query });
return responseData.data[mapping[resource].key].map((campaign: { name: string, _id: string }) => ({
name: campaign.name,
value: campaign._id,
}));
}

View file

@ -0,0 +1 @@
<svg viewBox="-2 2 45 45" xmlns="http://www.w3.org/2000/svg"><path d="M29.714 6.539H9.078a2.109 2.109 0 00-2.007 2.762l5.484 16.863 4.219-1.817a2.69 2.69 0 01-.024-.34 2.641 2.641 0 111.023 2.081 2416.71 2416.71 0 01-4.415 2.545l5.156 15.852a.865.865 0 001.645 0l11.48-35.293a2.026 2.026 0 00-1.925-2.653z" opacity="0.9" fill="#f4b454"></path><path d="M37.154 21.261L1.071 12.577a.865.865 0 00-.861 1.4l10.909 12.8 5.656-2.436a2.694 2.694 0 01-.024-.34 2.641 2.641 0 111.023 2.081 2178.15 2178.15 0 01-4.86 2.8l11.291 13.255a2.11 2.11 0 003.4-.264l10.8-17.586a2.026 2.026 0 00-1.251-3.026z" fill="#ef6d4a" opacity="0.9"></path><path d="M37.636 12.577L1.553 21.261A2.025 2.025 0 00.301 24.29l3.472 5.656 13-5.6a2.689 2.689 0 01-.024-.34 2.641 2.641 0 111.023 2.081c-3.1 1.791-9.073 5.234-12.108 6.94l5.432 8.848a2.11 2.11 0 003.4.264l23.995-28.161a.865.865 0 00-.855-1.401z" fill="#20354c" opacity="0.9"></path></svg>

After

Width:  |  Height:  |  Size: 917 B

View file

@ -70,6 +70,7 @@
"dist/credentials/DropboxApi.credentials.js",
"dist/credentials/DropboxOAuth2Api.credentials.js",
"dist/credentials/EgoiApi.credentials.js",
"dist/credentials/EmeliaApi.credentials.js",
"dist/credentials/EventbriteApi.credentials.js",
"dist/credentials/EventbriteOAuth2Api.credentials.js",
"dist/credentials/FacebookGraphApi.credentials.js",
@ -317,6 +318,8 @@
"dist/nodes/Egoi/Egoi.node.js",
"dist/nodes/EmailReadImap.node.js",
"dist/nodes/EmailSend.node.js",
"dist/nodes/Emelia/Emelia.node.js",
"dist/nodes/Emelia/EmeliaTrigger.node.js",
"dist/nodes/ErrorTrigger.node.js",
"dist/nodes/Eventbrite/EventbriteTrigger.node.js",
"dist/nodes/ExecuteCommand.node.js",