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,127 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { getBaseUrl, instasentAuth } from '../../index';
import { BOOLEAN_OPTIONS, IGNORED_ATTRIBUTES, LONG_TEXT_TYPES } from '../common/constants';
const PROPERTY_ITERATIONS = [
{ check: (spec: any) => spec.requiredInWebhook },
{ check: (spec: any) => spec.important },
{ check: (spec: any) => spec.visible !== false },
{ check: (spec: any) => !spec.custom },
{ check: () => true }
];
export const addOrUpdateContact = createAction({
name: 'add_or_update_contact',
displayName: 'Add/Update contact',
description: 'Add or update a single contact',
auth: instasentAuth,
props: {
contact: Property.DynamicProperties({
auth: instasentAuth,
displayName: 'Contact Properties',
description: 'Enter the contact properties, the User ID is mandatory',
required: true,
refreshers: ['authentication'],
props: async ({ auth }) => {
if (!auth) {
return {};
}
const authData = auth;
const baseUrl = getBaseUrl({
projectId: authData.props.projectId,
datasourceId: authData.props.datasourceId
});
try {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${baseUrl}/stream/specs/attributes`,
headers: {
'Authorization': `Bearer ${authData.props.apiKey}`
}
});
const properties: Record<string, any> = {};
for (const iteration of PROPERTY_ITERATIONS) {
for (const spec of response.body.specs) {
// Ignored properties
if (spec.readOnly || properties[spec.uid] || IGNORED_ATTRIBUTES.includes(spec.uid)) {
continue;
}
// Multiple iterations to sort the properties based on their attributes
if (!iteration.check(spec)) {
continue;
}
const displayLabel = spec.displayLabel;
let description = spec.description;
if (spec.dataType === 'date') {
description += ' (ISO 8601 format YYYY-MM-DD)';
}
if (spec.multivalue > 1) {
properties[spec.uid] = Property.Array({
displayName: displayLabel,
description: `${description} (Max ${spec.multivalue} values)`,
required: spec.requiredInWebhook
});
} else if (spec.dataType === 'bool') {
properties[spec.uid] = Property.StaticDropdown({
displayName: displayLabel,
description: `${description} [0=false|1=true|null=unknown]`,
required: spec.requiredInWebhook,
options: {
options: BOOLEAN_OPTIONS
}
});
} else {
const PropType = LONG_TEXT_TYPES.includes(spec.dataType) ? Property.LongText : Property.ShortText;
properties[spec.uid] = PropType({
displayName: displayLabel,
description: description,
required: spec.requiredInWebhook
});
}
}
}
return properties;
} catch (error) {
throw new Error('Failed to load contact properties');
}
}
}),
instant: Property.Checkbox({
displayName: 'Instant',
description: 'Process contact immediately instead of queuing. Only enable this when you need to add an event for this contact in the next step. Not recommended for high-volume operations.',
required: false,
defaultValue: false
})
},
async run(context) {
const contact = context.propsValue.contact;
const instant = context.propsValue.instant;
const auth = context.auth;
const baseUrl = getBaseUrl({ projectId: auth.props.projectId, datasourceId: auth.props.datasourceId });
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${baseUrl}/stream/contacts${instant ? '?_sync' : ''}`,
headers: {
'Authorization': `Bearer ${auth.props.apiKey}`,
'Content-Type': 'application/json'
},
body: [contact]
});
if (response.body.entitiesSuccess !== 1) {
throw new Error(`Failed to add or update contact: ${JSON.stringify(response.body.errors)}`);
}
return response.body;
}
});

View File

@@ -0,0 +1,162 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { getBaseUrl, instasentAuth } from '../..';
import { ApiResponse, EventParameter, EventSpec, InstasentAuthType } from '../common/types';
import { BOOLEAN_OPTIONS } from '../common/constants';
export const createEvent = createAction({
name: 'add_event',
displayName: 'Add Event',
description: 'Add a contact event',
auth: instasentAuth,
props: {
user_id: Property.ShortText({
displayName: 'User ID',
description: 'Unique identifier of the user',
required: true
}),
event_id: Property.ShortText({
displayName: 'Event ID',
description: 'Unique identifier for this event. Used for deduplication.',
required: true
}),
event_date: Property.ShortText({
displayName: 'Event Date',
description: 'Date and time when the event occurred, will default to now (ISO 8601 format YYYY-MM-DDTHH:MM:SS.SSSZ)',
required: false
}),
event_type: Property.Dropdown({
auth: instasentAuth,
displayName: 'Event Type',
description: 'Select the type of event to create',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account first',
};
}
const authData = auth;
const baseUrl = getBaseUrl({
projectId: authData.props.projectId,
datasourceId: authData.props.datasourceId
});
const response = await httpClient.sendRequest<{ specs: EventSpec[] }>({
method: HttpMethod.GET,
url: `${baseUrl}/stream/specs/events`,
headers: {
'Authorization': `Bearer ${authData.props.apiKey}`
}
});
return {
options: response.body.specs.map(spec => ({
label: `${spec.emoji} ${spec.name}`,
value: spec.uid
}))
};
}
}),
event_parameters: Property.DynamicProperties({
auth: instasentAuth,
displayName: 'Event Parameters',
description: 'Parameters for the selected event type',
required: true,
refreshers: ['event_type'],
props: async ({ auth, event_type }) => {
if (!auth || !event_type) return {};
const authData = auth;
const baseUrl = getBaseUrl({
projectId: authData.props.projectId,
datasourceId: authData.props.datasourceId
});
const response = await httpClient.sendRequest<{ specs: EventParameter[] }>({
method: HttpMethod.GET,
url: `${baseUrl}/stream/specs/event-parameters/${event_type}`,
headers: {
'Authorization': `Bearer ${authData.props.apiKey}`
}
});
const props: Record<string, any> = {};
response.body.specs.forEach(param => {
if (param.multiValue > 1) {
props[param.parameter] = Property.Array({
displayName: param.title,
description: `${param.description} (Max ${param.multiValue} values)`,
required: param.required
});
} else {
// Convert API parameter specs to ActivePieces properties
switch (param.dataType) {
case 'bool':
props[param.parameter] = Property.StaticDropdown({
displayName: param.title,
description: `${param.description} [0=false|1=true|null=unknown]`,
required: param.required,
options: {
options: BOOLEAN_OPTIONS
}
})
break;
case "string":
case "payload":
props[param.parameter] = Property.LongText({
displayName: param.title,
description: param.description,
required: param.required
});
break;
default:
props[param.parameter] = Property.ShortText({
displayName: param.title,
description: param.description,
required: param.required
});
break;
}
}
});
return props;
}
})
},
async run({ auth, propsValue }) {
const authData = auth as InstasentAuthType;
const baseUrl = getBaseUrl({
projectId: authData.props.projectId,
datasourceId: authData.props.datasourceId
});
const eventData = {
_user_id: propsValue.user_id,
_event_id: propsValue.event_id,
_event_type: propsValue.event_type,
_event_date: propsValue.event_date,
_event_parameters: propsValue.event_parameters
};
const response = await httpClient.sendRequest<ApiResponse<typeof eventData>>({
method: HttpMethod.POST,
url: `${baseUrl}/stream/events`,
headers: {
'Authorization': `Bearer ${authData.props.apiKey}`,
'Content-Type': 'application/json'
},
body: [eventData]
});
if (response.body.entitiesSuccess !== 1) {
throw new Error(`Failed to create event: ${JSON.stringify(response.body.errors)}`);
}
return response.body;
}
});

View File

@@ -0,0 +1,33 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { getBaseUrl, instasentAuth } from '../../index';
export const deleteContact = createAction({
name: 'delete_contact',
displayName: 'Delete Contact',
description: 'Delete a single contact by User ID',
auth: instasentAuth,
props: {
userId: Property.ShortText({
displayName: 'User ID',
description: 'Unique identifier of the contact to delete',
required: true
})
},
async run(context) {
const { userId } = context.propsValue;
const auth = context.auth;
const baseUrl = getBaseUrl({ projectId: auth.props.projectId, datasourceId: auth.props.datasourceId });
const response = await httpClient.sendRequest({
method: HttpMethod.DELETE,
url: `${baseUrl}/stream/contacts/${userId}`,
headers: {
'Authorization': `Bearer ${auth.props.apiKey}`
}
});
return response.body;
}
});