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,232 @@
import {
HttpMethod,
httpClient,
HttpRequest,
AuthenticationType,
} from '@activepieces/pieces-common';
export const API_URL = 'https://acuityscheduling.com/api/v1';
export async function fetchAvailableDates(
accessToken: string,
appointmentTypeId: number,
month: string,
timezone?: string,
calendarId?: number,
) {
const queryParams: Record<string, string> = {
month,
appointmentTypeID: appointmentTypeId.toString(),
};
if (timezone) queryParams['timezone'] = timezone;
if (calendarId) queryParams['calendarID'] = calendarId.toString();
const response = await httpClient.sendRequest<Array<{ date: string }>>({
method: HttpMethod.GET,
url: `${API_URL}/availability/dates`,
queryParams,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
});
if (Array.isArray(response.body)) {
return response.body.map((item) => item.date);
}
return [];
}
export async function fetchAvailableTimes(
accessToken: string,
appointmentTypeId: number,
date: string,
timezone?: string,
calendarId?: number,
ignoreAppointmentIDs?: number[],
) {
const params = new URLSearchParams();
params.append('date', date);
params.append('appointmentTypeID', appointmentTypeId.toString());
if (timezone) params.append('timezone', timezone);
if (calendarId) params.append('calendarID', calendarId.toString());
if (ignoreAppointmentIDs && ignoreAppointmentIDs.length > 0) {
ignoreAppointmentIDs.forEach((id) => params.append('ignoreAppointmentIDs[]', id.toString()));
}
const response = await httpClient.sendRequest<Array<{ time: string; datetime: string }>>({
method: HttpMethod.GET,
url: `${API_URL}/availability/times?${params.toString()}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
});
return response.body;
}
// Helper function to get full appointment details
export async function getAppointmentDetails(appointmentId: string, accessToken: string) {
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${API_URL}/appointments/${appointmentId}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
};
const response = await httpClient.sendRequest(request);
return response.body;
}
export async function fetchAppointmentTypes(accessToken: string, includeDeleted = false) {
const queryParams: Record<string, string> = {};
if (includeDeleted) {
queryParams['includeDeleted'] = 'true';
}
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${API_URL}/appointment-types`,
queryParams,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
};
const response = await httpClient.sendRequest<
Array<{ id: number; name: string; active: boolean | string }>
>(request);
if (Array.isArray(response.body)) {
// Filter for active types unless includeDeleted is true, and map to dropdown options
return response.body
.filter((type) => includeDeleted || type.active === true || type.active === 'true')
.map((type) => ({ label: type.name, value: type.id }));
}
return [];
}
export async function fetchCalendars(accessToken: string) {
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${API_URL}/calendars`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
};
const response = await httpClient.sendRequest<Array<{ id: number; name: string }>>(request);
if (Array.isArray(response.body)) {
return response.body.map((calendar) => ({ label: calendar.name, value: calendar.id }));
}
return [];
}
export async function fetchFormFields(accessToken: string) {
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${API_URL}/forms`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
};
const response = await httpClient.sendRequest<
Array<{ id: number; name: string; fields: Array<{ id: number; name: string }> }>
>(request);
if (Array.isArray(response.body)) {
const formFields: Array<{ label: string; value: number }> = [];
response.body.forEach((form) => {
if (Array.isArray(form.fields)) {
form.fields.forEach((field) => {
formFields.push({ label: `${form.name} - ${field.name}`, value: field.id });
});
}
});
return formFields;
}
return [];
}
export async function fetchAddons(accessToken: string, appointmentTypeId?: number) {
// First, fetch all addons
const allAddonsRequest: HttpRequest = {
method: HttpMethod.GET,
url: `${API_URL}/appointment-addons`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
};
const allAddonsResponse = await httpClient.sendRequest<Array<{ id: number; name: string }>>(
allAddonsRequest,
);
if (!Array.isArray(allAddonsResponse.body)) {
return [];
}
let compatibleAddonIds: number[] | null = null;
// If appointmentTypeId is provided, fetch the specific appointment type to get its compatible addonIDs
if (appointmentTypeId) {
const appointmentTypeRequest: HttpRequest = {
method: HttpMethod.GET,
url: `${API_URL}/appointment-types/${appointmentTypeId}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
};
try {
const appointmentTypeResponse = await httpClient.sendRequest<{ addonIDs: number[] }>(
appointmentTypeRequest,
);
if (appointmentTypeResponse.body && Array.isArray(appointmentTypeResponse.body.addonIDs)) {
compatibleAddonIds = appointmentTypeResponse.body.addonIDs;
}
} catch (e) {
// Log error or handle if type not found, but still proceed with all addons if necessary
console.warn(
`Could not fetch compatible addons for appointment type ${appointmentTypeId}, returning all addons. Error: ${e}`,
);
}
}
const allAddons = allAddonsResponse.body.map((addon) => ({ label: addon.name, value: addon.id }));
if (compatibleAddonIds) {
return allAddons.filter((addon) => compatibleAddonIds.includes(addon.value));
}
return allAddons;
}
export async function fetchLabels(accessToken: string) {
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${API_URL}/labels`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
};
const response = await httpClient.sendRequest<Array<{ id: number; name: string; color: string }>>(
request,
);
if (Array.isArray(response.body)) {
return response.body.map((label) => ({
label: `${label.name} (${label.color})`,
value: label.id,
}));
}
return [];
}

View File

@@ -0,0 +1,105 @@
import { OAuth2PropertyValue, Property } from '@activepieces/pieces-framework';
import { fetchAddons, fetchAppointmentTypes, fetchCalendars, fetchLabels } from '.';
import { acuitySchedulingAuth } from '../..';
interface DropdownParams {
displayName: string;
description?: string;
required: boolean;
}
export const appointmentTypeIdDropdown = (params: DropdownParams) =>
Property.Dropdown({
auth: acuitySchedulingAuth,
displayName: params.displayName,
description: params.description,
required: params.required,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please authenticate first',
options: [],
};
}
const { access_token } = auth as OAuth2PropertyValue;
return {
disabled: false,
options: await fetchAppointmentTypes(access_token),
};
},
});
export const calendarIdDropdown = (params: DropdownParams) =>
Property.Dropdown({
auth: acuitySchedulingAuth,
displayName: params.displayName,
description: params.description,
required: params.required,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please authenticate first',
options: [],
};
}
const { access_token } = auth as OAuth2PropertyValue;
return {
disabled: false,
options: await fetchCalendars(access_token),
};
},
});
export const addonIdsDropdown = (params: DropdownParams) =>
Property.MultiSelectDropdown({
auth: acuitySchedulingAuth,
displayName: params.displayName,
description: params.description,
required: params.required,
refreshers: ['appointmentTypeID'],
options: async ({ auth, appointmentTypeID }) => {
if (!auth || !appointmentTypeID) {
return {
disabled: true,
placeholder: 'Please authenticate first',
options: [],
};
}
const { access_token } = auth as OAuth2PropertyValue;
return {
disabled: false,
options: await fetchAddons(access_token, appointmentTypeID as number),
};
},
});
export const labelIdDropdown = (params: DropdownParams) =>
Property.Dropdown({
auth: acuitySchedulingAuth,
displayName: params.displayName,
description: params.description,
required: params.required,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please authenticate first',
options: [],
};
}
const { access_token } = auth as OAuth2PropertyValue;
return {
disabled: false,
options: await fetchLabels(access_token),
};
},
});