Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,34 @@
import { createAction } from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
HttpRequest,
} from '@activepieces/pieces-common';
import { mauticCommon } from '../common';
import { mauticAuth } from '../..';
export const createCompany = createAction({
auth: mauticAuth,
description: 'Creates a new company in Mautic CRM', // Must be a unique across the piece, this shouldn't be changed.
displayName: 'Create Company',
name: 'create_mautic_company',
props: {
fields: mauticCommon.companyFields,
},
run: async function (context) {
const { base_url, username, password } = context.auth.props;
const request: HttpRequest = {
method: HttpMethod.POST,
url:
(base_url.endsWith('/') ? base_url : base_url + '/') +
'api/companies/new',
body: JSON.stringify(context.propsValue.fields),
headers: {
Authorization:
'Basic ' + Buffer.from(`${username}:${password}`).toString('base64'),
'Content-Type': 'application/json',
},
};
return await httpClient.sendRequest(request);
},
});

View File

@@ -0,0 +1,34 @@
import { createAction } from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
HttpRequest,
} from '@activepieces/pieces-common';
import { mauticCommon } from '../common';
import { mauticAuth } from '../..';
export const createContact = createAction({
auth: mauticAuth,
description: 'Creates a new contact in Mautic CRM', // Must be a unique across the piece, this shouldn't be changed.
displayName: 'Create Contact',
name: 'create_mautic_contact',
props: {
fields: mauticCommon.contactFields,
},
run: async function (context) {
const { base_url, username, password } = context.auth.props;
const request: HttpRequest = {
method: HttpMethod.POST,
url:
(base_url.endsWith('/') ? base_url : base_url + '/') +
'api/contacts/new',
body: JSON.stringify(context.propsValue.fields),
headers: {
Authorization:
'Basic ' + Buffer.from(`${username}:${password}`).toString('base64'),
'Content-Type': 'application/json',
},
};
return await httpClient.sendRequest(request);
},
});

View File

@@ -0,0 +1,6 @@
export { createContact } from './create-contact';
export { searchContact } from './search-contact';
export { updateContact } from './update-contact';
export { createCompany } from './create-company';
export { searchCompany } from './search-company';
export { updateCompany } from './update-company';

View File

@@ -0,0 +1,28 @@
import { createAction } from '@activepieces/pieces-framework';
import { mauticCommon, searchEntity } from '../common';
import { mauticAuth } from '../..';
export const searchCompany = createAction({
auth: mauticAuth,
description: 'Search for a company in Mautic CRM', // Must be a unique across the piece, this shouldn't be changed.
displayName: 'Search Company',
name: 'search_mautic_company',
props: {
fields: mauticCommon.companyFields,
},
run: async function (context) {
const { base_url, username, password } = context.auth.props;
const url =
(base_url.endsWith('/') ? base_url : base_url + '/') + 'api/companies';
const fields = context.propsValue.fields;
const keys = Object.keys(fields);
let searchParams = '?';
for (const key of keys) {
if (fields[key]) {
searchParams += `search=${key}:${fields[key]}&`;
}
}
const response = await searchEntity(url, searchParams, username, password);
return Object.values(response.body.companies)[0];
},
});

View File

@@ -0,0 +1,30 @@
import { createAction } from '@activepieces/pieces-framework';
import { mauticCommon, searchEntity } from '../common';
import { mauticAuth } from '../..';
export const searchContact = createAction({
auth: mauticAuth,
description: 'Search for a contact in Mautic CRM', // Must be a unique across the piece, this shouldn't be changed.
displayName: 'Search Contact',
name: 'search_mautic_contact',
props: {
fields: mauticCommon.contactFields,
},
run: async function (context) {
const { base_url, username, password } = context.auth.props;
const url =
(base_url.endsWith('/') ? base_url : base_url + '/') + 'api/contacts';
const fields = context.propsValue.fields;
const keys = Object.keys(fields);
let count = 0;
let searchParams = '?';
for (const key of keys) {
if (fields[key]) {
searchParams += `where[${count}][col]=${key}&where[${count}][expr]=eq&where[${count}][val]=${fields[key]}&`;
++count;
}
}
const response = await searchEntity(url, searchParams, username, password);
return Object.values(response.body.contacts)[0];
},
});

View File

@@ -0,0 +1,44 @@
import { createAction } from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
HttpRequest,
} from '@activepieces/pieces-common';
import { mauticCommon } from '../common';
import { mauticAuth } from '../..';
export const updateCompany = createAction({
auth: mauticAuth,
description: 'Update a company in Mautic CRM', // Must be a unique across the piece, this shouldn't be changed.
displayName: 'Update Company With Contact Id',
name: 'update_mautic_company',
props: {
id: mauticCommon.id,
fields: mauticCommon.companyFields,
},
run: async function (context) {
const { base_url, username, password } = context.auth.props;
// This is intentionally done because for `null` data Mautic doesn't change data for contacts but
// for the same it changes data for companies. This step is taken to ensure both behave the same.
const fields = context.propsValue.fields;
const keys = Object.keys(fields);
for (const key of keys) {
if (!fields[key]) {
delete fields[key];
}
}
const request: HttpRequest = {
method: HttpMethod.PATCH,
url: `${
base_url.endsWith('/') ? base_url : base_url + '/'
}api/companies/${context.propsValue.id}/edit`,
body: JSON.stringify(fields),
headers: {
Authorization:
'Basic ' + Buffer.from(`${username}:${password}`).toString('base64'),
'Content-Type': 'application/json',
},
};
return await httpClient.sendRequest(request);
},
});

View File

@@ -0,0 +1,35 @@
import { createAction } from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
HttpRequest,
} from '@activepieces/pieces-common';
import { mauticCommon } from '../common';
import { mauticAuth } from '../..';
export const updateContact = createAction({
auth: mauticAuth,
description: 'Update a contact in Mautic CRM', // Must be a unique across the piece, this shouldn't be changed.
displayName: 'Update Contact With Contact Id',
name: 'update_mautic_contact',
props: {
id: mauticCommon.id,
fields: mauticCommon.contactFields,
},
run: async function (context) {
const { base_url, username, password } = context.auth.props;
const request: HttpRequest = {
method: HttpMethod.PATCH,
url: `${base_url.endsWith('/') ? base_url : base_url + '/'}api/contacts/${
context.propsValue.id
}/edit`,
body: JSON.stringify(context.propsValue.fields),
headers: {
Authorization:
'Basic ' + Buffer.from(`${username}:${password}`).toString('base64'),
'Content-Type': 'application/json',
},
};
return await httpClient.sendRequest(request);
},
});

View File

@@ -0,0 +1,126 @@
import { DynamicPropsValue, Property } from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
HttpRequest,
} from '@activepieces/pieces-common';
import { mauticAuth } from '../..';
// Function Section
export const mapMauticToActivepiecesProperty = (
type: string,
fieldMetadata: {
displayName: string;
required: boolean;
},
properties: object
) => {
switch (type) {
case 'lookup':
case 'text':
case 'email':
case 'tel':
case 'region':
case 'country':
case 'locale':
case 'timezone':
case 'url':
return Property.ShortText(fieldMetadata);
case 'date':
case 'datetime':
return Property.DateTime(fieldMetadata);
case 'number':
return Property.Number(fieldMetadata);
case 'boolean':
return Property.StaticDropdown({
...fieldMetadata,
options: {
options: [
{ value: 'no', label: 'No' },
{ value: 'yes', label: 'Yes' },
],
},
});
case 'multiselect':
return Property.StaticMultiSelectDropdown({
...fieldMetadata,
options: {
options: Object.values(properties)[0],
},
});
case 'select':
return Property.StaticDropdown({
...fieldMetadata,
options: {
options: Object.values(properties)[0],
},
});
default:
console.error(`No support of type ${type}`);
return null;
}
};
export const fetchDynamicFieldsFromMetadata = async (
baseUrl: string,
username: string,
password: string,
type: 'contact' | 'company' | 'lead'
) => {
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${
baseUrl.endsWith('/') ? baseUrl : baseUrl + '/'
}api/fields/${type}?limit=1000`,
headers: {
Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(
'base64'
)}`,
'Content-Type': 'application/json',
},
};
const result = await httpClient.sendRequest(request);
if (result.status == 200) {
return Object.values(result.body.fields).reduce(
(fields: DynamicPropsValue, field) => {
const {
label: displayName,
alias,
type,
properties,
} = field as Record<string, any>;
const fieldMetadata = {
displayName,
required: false,
};
if (!type) return {};
const f = mapMauticToActivepiecesProperty(
type,
fieldMetadata,
properties
);
if (f) {
fields[alias] = f;
}
return fields;
},
{}
);
}
throw Error(`Unable to fetch ${type} metadata`);
};
export const getFields = (type: 'contact' | 'company' | 'lead') =>
Property.DynamicProperties({
auth: mauticAuth,
displayName: 'All Fields',
description: 'List of all possible fields present',
required: true,
refreshers: [],
props: async ({ auth }) => {
if (!auth) return {};
const { base_url, username, password } = auth.props;
return fetchDynamicFieldsFromMetadata(base_url, username, password, type);
},
});

View File

@@ -0,0 +1,41 @@
import { Property } from '@activepieces/pieces-framework';
import { getFields } from './helper';
import {
httpClient,
HttpMethod,
HttpRequest,
} from '@activepieces/pieces-common';
export const mauticCommon = {
contactFields: { ...getFields('contact'), ...getFields('lead') },
companyFields: getFields('company'),
id: Property.ShortText({
displayName: 'Id of the entity',
required: true,
}),
};
export const searchEntity = async (
url: string,
searchParams: string,
username: string,
password: string
) => {
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${url}${searchParams}`,
headers: {
Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(
'base64'
)}`,
'Content-Type': 'application/json',
},
};
const response: Record<string, any> = await httpClient.sendRequest(request);
const length = response.body.total;
if (!length || length != 1)
throw Error(
'The query is not perfect enough to get single result. Please refine'
);
return response;
};

View File

@@ -0,0 +1,453 @@
import { HttpMethod, HttpRequest, httpClient } from '@activepieces/pieces-common';
import { mauticAuth } from '../../index';
import { Property, TriggerStrategy, createTrigger } from "@activepieces/pieces-framework";
const contactTestData = {
"contact": {
"id": 38186,
"points": 0,
"color": null,
"fields": {
"core": {
"points": {
"id": "9",
"label": "Points",
"alias": "points",
"type": "number",
"group": "core",
"object": "lead",
"is_fixed": "1",
"value": "0"
},
"title": {
"id": "1",
"label": "Title",
"alias": "title",
"type": "lookup",
"group": "core",
"object": "lead",
"is_fixed": "1",
"value": ""
},
"firstname": {
"id": "2",
"label": "First Name",
"alias": "firstname",
"type": "text",
"group": "core",
"object": "lead",
"is_fixed": "1",
"value": "Test"
},
"lastname": {
"id": "3",
"label": "Last Name",
"alias": "lastname",
"type": "text",
"group": "core",
"object": "lead",
"is_fixed": "1",
"value": "Contact"
},
"company": {
"id": "4",
"label": "Company",
"alias": "company",
"type": "text",
"group": "core",
"object": "lead",
"is_fixed": "1",
"value": ""
},
"position": {
"id": "5",
"label": "Position",
"alias": "position",
"type": "text",
"group": "core",
"object": "lead",
"is_fixed": "1",
"value": ""
},
"email": {
"id": "6",
"label": "Email",
"alias": "email",
"type": "email",
"group": "core",
"object": "lead",
"is_fixed": "1",
"value": "Test@email.com"
},
"phone": {
"id": "8",
"label": "Phone",
"alias": "phone",
"type": "tel",
"group": "core",
"object": "lead",
"is_fixed": "1",
"value": ""
},
"mobile": {
"id": "7",
"label": "Mobile",
"alias": "mobile",
"type": "tel",
"group": "core",
"object": "lead",
"is_fixed": "1",
"value": ""
},
"address1": {
"id": "11",
"label": "Address Line 1",
"alias": "address1",
"type": "text",
"group": "core",
"object": "lead",
"is_fixed": "1",
"value": ""
},
"address2": {
"id": "12",
"label": "Address Line 2",
"alias": "address2",
"type": "text",
"group": "core",
"object": "lead",
"is_fixed": "1",
"value": ""
},
"city": {
"id": "13",
"label": "City",
"alias": "city",
"type": "text",
"group": "core",
"object": "lead",
"is_fixed": "1",
"value": ""
},
"state": {
"id": "14",
"label": "State",
"alias": "state",
"type": "region",
"group": "core",
"object": "lead",
"is_fixed": "1",
"value": ""
},
"zipcode": {
"id": "15",
"label": "Zip Code",
"alias": "zipcode",
"type": "text",
"group": "core",
"object": "lead",
"is_fixed": "1",
"value": ""
},
"country": {
"id": "16",
"label": "Country",
"alias": "country",
"type": "country",
"group": "core",
"object": "lead",
"is_fixed": "1",
"value": ""
},
"fax": {
"id": "10",
"label": "Fax",
"alias": "fax",
"type": "tel",
"group": "core",
"object": "lead",
"is_fixed": "0",
"value": null
},
"preferred_locale": {
"id": "17",
"label": "Preferred Locale",
"alias": "preferred_locale",
"type": "locale",
"group": "core",
"object": "lead",
"is_fixed": "1",
"value": null
},
"attribution_date": {
"id": "18",
"label": "Attribution Date",
"alias": "attribution_date",
"type": "datetime",
"group": "core",
"object": "lead",
"is_fixed": "1",
"value": null
},
"attribution": {
"id": "19",
"label": "Attribution",
"alias": "attribution",
"type": "number",
"group": "core",
"object": "lead",
"is_fixed": "1",
"value": null
},
"website": {
"id": "20",
"label": "Website",
"alias": "website",
"type": "url",
"group": "core",
"object": "lead",
"is_fixed": "0",
"value": null
},
"boolean": {
"id": "43",
"label": "Boolean",
"alias": "boolean",
"type": "boolean",
"group": "core",
"object": "lead",
"is_fixed": "0",
"value": null
},
"multiple_contact": {
"id": "44",
"label": "Multiple Contact",
"alias": "multiple_contact",
"type": "multiselect",
"group": "core",
"object": "lead",
"is_fixed": "0",
"value": null
}
},
"social": {
"facebook": {
"id": "21",
"label": "Facebook",
"alias": "facebook",
"type": "text",
"group": "social",
"object": "lead",
"is_fixed": "0",
"value": null
},
"foursquare": {
"id": "22",
"label": "Foursquare",
"alias": "foursquare",
"type": "text",
"group": "social",
"object": "lead",
"is_fixed": "0",
"value": null
},
"instagram": {
"id": "24",
"label": "Instagram",
"alias": "instagram",
"type": "text",
"group": "social",
"object": "lead",
"is_fixed": "0",
"value": null
},
"linkedin": {
"id": "25",
"label": "LinkedIn",
"alias": "linkedin",
"type": "text",
"group": "social",
"object": "lead",
"is_fixed": "0",
"value": null
},
"skype": {
"id": "26",
"label": "Skype",
"alias": "skype",
"type": "text",
"group": "social",
"object": "lead",
"is_fixed": "0",
"value": null
},
"twitter": {
"id": "27",
"label": "Twitter",
"alias": "twitter",
"type": "text",
"group": "social",
"object": "lead",
"is_fixed": "0",
"value": null
}
},
"personal": [],
"professional": []
}
},
"channel": "email",
"old_status": "contactable",
"new_status": "manual",
"timestamp": "2017-12-01T00:05:18-06:00"
}
export const triggers = [
{
name: "lead_post_save_update",
displayName: "Contact Updated",
description: "Triggers when a contact is updated.",
sampleData: {
"mautic.lead_post_save_update": [contactTestData]
},
eventType: "mautic.lead_post_save_update",
},
{
name: "lead_company_change",
displayName: "Contact Company Subscription Change",
description: "Triggers when a commpany is added or removed to/from contact.",
sampleData: {
"mautic.lead_company_change": [contactTestData]
},
eventType: "mautic.lead_company_change",
},
{
name: "lead_channel_subscription_changed",
displayName: "Contact Channel Subscription Change",
description: "Triggers when a contact's channel subscription status changes.",
sampleData: {
"mautic.lead_channel_subscription_changed": [contactTestData]
},
eventType: "mautic.lead_channel_subscription_changed",
},
{
name: "lead_post_save_new",
displayName: "New Contact",
description: "Triggers when a new contact is created.",
sampleData: {
"mautic.lead_post_save_new": [contactTestData]
},
eventType: "mautic.lead_post_save_new",
},
]
.map((props) => registerTrigger(props));
function registerTrigger({
name,
displayName,
eventType,
description,
sampleData
}: {
name: string;
displayName: string;
eventType: string;
description: string;
sampleData: unknown;
}) {
return createTrigger({
auth: mauticAuth,
name: `mautic_${name}_trigger`,
displayName,
description,
props: {
name: Property.ShortText({
displayName: "Webhook Name",
description: "The name the webhook will be searchable by in mautic the webhooks page.",
required: true
}),
description: Property.LongText({
displayName: "Description",
description: "A short description of the webhook",
required: true
})
},
sampleData,
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
const { base_url, username, password } = context.auth.props
const request: HttpRequest = {
method: HttpMethod.POST,
url: `${(base_url.endsWith('/') ? base_url : base_url + '/')}api/hooks/new`,
body: {
name: context.propsValue.name,
description: context.propsValue.description,
webhookUrl: context.webhookUrl,
eventsOrderbyDir: "ASC",
triggers: [eventType]
},
headers: {
'Content-Type': 'application/json',
'Authorization':
'Basic ' + Buffer.from(`${username}:${password}`).toString('base64'),
},
queryParams: {},
}
const response = await httpClient.sendRequest<WebhookInformation>(request);
await context.store.put<WebhookInformation>(`mautic_${name}_trigger`, response.body);
},
async onDisable(context) {
const { base_url, username, password } = context.auth.props
const webhook = await context.store.get<WebhookInformation>(`mautic_${name}_trigger`);
if (webhook != null) {
const request: HttpRequest = {
method: HttpMethod.DELETE,
url: `${(base_url.endsWith('/') ? base_url : base_url + '/')}api/hooks/${webhook.hook.id}/delete`,
headers: {
'Content-Type': 'application/json',
'Authorization':
'Basic ' + Buffer.from(`${username}:${password}`).toString('base64'),
},
};
const response = await httpClient.sendRequest(request);
console.debug(`mautic.trigger.onDisable`, response);
}
},
async run(context) {
return [context.payload.body];
},
});
}
interface WebhookInformation {
hook: {
isPublished: boolean
dateAdded: string
dateModified: string
createdBy: number
createdByUser: string
modifiedBy: unknown | null
modifiedByUser: string
id: number
name: string
description: string
webhookUrl: string
secret: string
eventsOrderbyDir: string
category: {
id: number
createdByUser: string
modifiedByUser: string
title: string
alias: string
description: string | null
color: string | null
bundle: string
}
triggers: string[]
}
}