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,4 @@
{
"name": "@activepieces/piece-smoothschedule",
"version": "0.0.1"
}

View File

@@ -0,0 +1,60 @@
{
"name": "pieces-smoothschedule",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/pieces/community/smoothschedule/src",
"projectType": "library",
"release": {
"version": {
"currentVersionResolver": "git-tag",
"preserveLocalDependencyProtocols": false,
"manifestRootsToUpdate": [
"dist/{projectRoot}"
]
}
},
"tags": [],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": [
"{options.outputPath}"
],
"options": {
"outputPath": "dist/packages/pieces/community/smoothschedule",
"tsConfig": "packages/pieces/community/smoothschedule/tsconfig.lib.json",
"packageJson": "packages/pieces/community/smoothschedule/package.json",
"main": "packages/pieces/community/smoothschedule/src/index.ts",
"assets": [
"packages/pieces/community/smoothschedule/*.md"
],
"buildableProjectDepsInPackageJsonType": "dependencies",
"updateBuildableProjectDepsInPackageJson": true
},
"dependsOn": [
"^build",
"prebuild"
]
},
"nx-release-publish": {
"options": {
"packageRoot": "dist/{projectRoot}"
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": [
"{options.outputFile}"
]
},
"prebuild": {
"executor": "nx:run-commands",
"options": {
"cwd": "packages/pieces/community/smoothschedule",
"command": "bun install --no-save --silent"
},
"dependsOn": [
"^build"
]
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,54 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { smoothScheduleAuth, SmoothScheduleAuth } from '../../index';
import { makeRequest } from '../common';
export const cancelEventAction = createAction({
auth: smoothScheduleAuth,
name: 'cancel_event',
displayName: 'Cancel Event',
description: 'Cancels an event in SmoothSchedule.',
props: {
eventId: Property.ShortText({
displayName: 'Event ID',
description: 'The ID of the event to cancel',
required: true,
}),
cancellationReason: Property.LongText({
displayName: 'Cancellation Reason',
description: 'Reason for cancellation',
required: false,
}),
sendNotification: Property.Checkbox({
displayName: 'Send Cancellation Notice',
description: 'Send cancellation email to customer',
required: false,
defaultValue: true,
}),
},
async run(context) {
const auth = context.auth as SmoothScheduleAuth;
const props = context.propsValue;
const body: Record<string, unknown> = {
status: 'CANCELLED',
};
if (props.cancellationReason) {
body['cancellation_reason'] = props.cancellationReason;
}
if (props.sendNotification !== undefined) {
body['send_notification'] = props.sendNotification;
}
const response = await makeRequest<Record<string, unknown>>(
auth,
HttpMethod.PATCH,
`/events/${props.eventId}/`,
body
);
return response;
},
});

View File

@@ -0,0 +1,131 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { smoothScheduleAuth, SmoothScheduleAuth } from '../../index';
import { makeRequest, formatDateTime } from '../common';
import { resourceMultiSelectDropdown, serviceDropdown, customerDropdown } from '../common/props';
export const createEventAction = createAction({
auth: smoothScheduleAuth,
name: 'create_event',
displayName: 'Create Event',
description: 'Creates a new appointment or event in SmoothSchedule.',
props: {
title: Property.ShortText({
displayName: 'Title',
description: 'Event title (optional - will be auto-generated from service if not provided)',
required: false,
}),
startTime: Property.DateTime({
displayName: 'Start Time',
description: 'When the event starts',
required: true,
}),
endTime: Property.DateTime({
displayName: 'End Time',
description: 'When the event ends (optional - will be calculated from service duration if not provided)',
required: false,
}),
serviceId: serviceDropdown({
displayName: 'Service',
description: 'The service for this appointment',
required: false,
}),
resourceIds: resourceMultiSelectDropdown({
displayName: 'Resources',
description: 'Staff, rooms, or equipment for this event',
required: false,
}),
customerId: customerDropdown({
displayName: 'Customer',
description: 'The customer for this appointment',
required: false,
}),
customerEmail: Property.ShortText({
displayName: 'Customer Email',
description: 'Customer email (used if Customer not selected)',
required: false,
}),
customerFirstName: Property.ShortText({
displayName: 'Customer First Name',
description: 'Customer first name (used if Customer not selected)',
required: false,
}),
customerLastName: Property.ShortText({
displayName: 'Customer Last Name',
description: 'Customer last name (used if Customer not selected)',
required: false,
}),
customerPhone: Property.ShortText({
displayName: 'Customer Phone',
description: 'Customer phone number',
required: false,
}),
notes: Property.LongText({
displayName: 'Notes',
description: 'Internal notes for this event',
required: false,
}),
status: Property.StaticDropdown({
displayName: 'Status',
description: 'Event status',
required: false,
defaultValue: 'CONFIRMED',
options: {
options: [
{ label: 'Confirmed', value: 'CONFIRMED' },
{ label: 'Pending', value: 'PENDING' },
],
},
}),
sendConfirmation: Property.Checkbox({
displayName: 'Send Confirmation',
description: 'Send confirmation email to customer',
required: false,
defaultValue: true,
}),
},
async run(context) {
const auth = context.auth as SmoothScheduleAuth;
const props = context.propsValue;
const body: Record<string, unknown> = {
start_time: formatDateTime(props.startTime),
status: props.status || 'CONFIRMED',
};
if (props.title) body['title'] = props.title;
if (props.endTime) body['end_time'] = formatDateTime(props.endTime);
if (props.serviceId) body['service'] = props.serviceId;
if (props.notes) body['notes'] = props.notes;
// Handle resources as participants
if (props.resourceIds && props.resourceIds.length > 0) {
body['resource_ids'] = props.resourceIds;
}
// Handle customer - either by ID or by creating inline
if (props.customerId) {
body['customer_id'] = props.customerId;
} else if (props.customerEmail) {
body['customer'] = {
email: props.customerEmail,
first_name: props.customerFirstName || '',
last_name: props.customerLastName || '',
phone: props.customerPhone || '',
};
}
if (props.sendConfirmation !== undefined) {
body['send_confirmation'] = props.sendConfirmation;
}
const response = await makeRequest<Record<string, unknown>>(
auth,
HttpMethod.POST,
'/events/',
body
);
return response;
},
});

View File

@@ -0,0 +1,84 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { smoothScheduleAuth, SmoothScheduleAuth } from '../../index';
import { makeRequest } from '../common';
import { resourceDropdown, serviceDropdown, eventStatusDropdown } from '../common/props';
export const findEventsAction = createAction({
auth: smoothScheduleAuth,
name: 'find_events',
displayName: 'Find Events',
description: 'Search for events in SmoothSchedule.',
props: {
startDate: Property.DateTime({
displayName: 'Start Date',
description: 'Find events starting from this date',
required: false,
}),
endDate: Property.DateTime({
displayName: 'End Date',
description: 'Find events up to this date',
required: false,
}),
resourceId: resourceDropdown({
displayName: 'Resource',
description: 'Filter by resource',
required: false,
}),
serviceId: serviceDropdown({
displayName: 'Service',
description: 'Filter by service',
required: false,
}),
status: eventStatusDropdown,
customerEmail: Property.ShortText({
displayName: 'Customer Email',
description: 'Filter by customer email',
required: false,
}),
limit: Property.Number({
displayName: 'Limit',
description: 'Maximum number of events to return',
required: false,
defaultValue: 50,
}),
},
async run(context) {
const auth = context.auth as SmoothScheduleAuth;
const props = context.propsValue;
const queryParams: Record<string, string> = {};
if (props.startDate) {
queryParams['start_date'] = new Date(props.startDate).toISOString().split('T')[0];
}
if (props.endDate) {
queryParams['end_date'] = new Date(props.endDate).toISOString().split('T')[0];
}
if (props.resourceId) {
queryParams['resource'] = props.resourceId.toString();
}
if (props.serviceId) {
queryParams['service'] = props.serviceId.toString();
}
if (props.status) {
queryParams['status'] = props.status;
}
if (props.customerEmail) {
queryParams['customer_email'] = props.customerEmail;
}
if (props.limit) {
queryParams['limit'] = props.limit.toString();
}
const response = await makeRequest<Record<string, unknown>>(
auth,
HttpMethod.GET,
'/events/',
undefined,
queryParams
);
return response;
},
});

View File

@@ -0,0 +1,7 @@
export * from './create-event';
export * from './update-event';
export * from './cancel-event';
export * from './find-events';
export * from './list-resources';
export * from './list-services';
export * from './list-inactive-customers';

View File

@@ -0,0 +1,62 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { smoothScheduleAuth, SmoothScheduleAuth } from '../../index';
import { makeRequest } from '../common';
interface InactiveCustomer {
id: number;
email: string;
first_name: string;
last_name: string;
name: string;
phone: string | null;
last_appointment_at: string | null;
days_since_last_appointment: number | null;
}
export const listInactiveCustomersAction = createAction({
auth: smoothScheduleAuth,
name: 'list_inactive_customers',
displayName: 'List Inactive Customers',
description: 'Get customers who haven\'t booked an appointment in a specified number of days. Perfect for win-back email campaigns when used with a Schedule trigger.',
props: {
inactiveDays: Property.Number({
displayName: 'Days Inactive',
description: 'Number of days without an appointment to consider a customer inactive (1-365)',
required: true,
defaultValue: 30,
}),
limit: Property.Number({
displayName: 'Maximum Results',
description: 'Maximum number of customers to return (1-500)',
required: false,
defaultValue: 100,
}),
},
async run(context) {
const auth = context.auth as SmoothScheduleAuth;
const props = context.propsValue;
const days = Math.max(1, Math.min(props.inactiveDays || 30, 365));
const limit = Math.max(1, Math.min(props.limit || 100, 500));
const queryParams: Record<string, string> = {
days: days.toString(),
limit: limit.toString(),
};
const customers = await makeRequest<InactiveCustomer[]>(
auth,
HttpMethod.GET,
'/customers/inactive/',
undefined,
queryParams
);
return {
customers,
count: customers.length,
days_threshold: days,
};
},
});

View File

@@ -0,0 +1,55 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { smoothScheduleAuth, SmoothScheduleAuth } from '../../index';
import { makeRequest } from '../common';
export const listResourcesAction = createAction({
auth: smoothScheduleAuth,
name: 'list_resources',
displayName: 'List Resources',
description: 'Get a list of resources (staff, rooms, equipment) from SmoothSchedule.',
props: {
type: Property.StaticDropdown({
displayName: 'Resource Type',
description: 'Filter by resource type',
required: false,
options: {
options: [
{ label: 'All Types', value: '' },
{ label: 'Staff', value: 'STAFF' },
{ label: 'Room', value: 'ROOM' },
{ label: 'Equipment', value: 'EQUIPMENT' },
],
},
}),
activeOnly: Property.Checkbox({
displayName: 'Active Only',
description: 'Only return active resources',
required: false,
defaultValue: true,
}),
},
async run(context) {
const auth = context.auth as SmoothScheduleAuth;
const props = context.propsValue;
const queryParams: Record<string, string> = {};
if (props.type) {
queryParams['type'] = props.type;
}
if (props.activeOnly) {
queryParams['is_active'] = 'true';
}
const response = await makeRequest<Record<string, unknown>[]>(
auth,
HttpMethod.GET,
'/resources/',
undefined,
queryParams
);
return { resources: response };
},
});

View File

@@ -0,0 +1,39 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { smoothScheduleAuth, SmoothScheduleAuth } from '../../index';
import { makeRequest } from '../common';
export const listServicesAction = createAction({
auth: smoothScheduleAuth,
name: 'list_services',
displayName: 'List Services',
description: 'Get a list of services from SmoothSchedule.',
props: {
activeOnly: Property.Checkbox({
displayName: 'Active Only',
description: 'Only return active services',
required: false,
defaultValue: true,
}),
},
async run(context) {
const auth = context.auth as SmoothScheduleAuth;
const props = context.propsValue;
const queryParams: Record<string, string> = {};
if (props.activeOnly) {
queryParams['is_active'] = 'true';
}
const response = await makeRequest<Record<string, unknown>[]>(
auth,
HttpMethod.GET,
'/services/',
undefined,
queryParams
);
return { services: response };
},
});

View File

@@ -0,0 +1,86 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { smoothScheduleAuth, SmoothScheduleAuth } from '../../index';
import { makeRequest, formatDateTime } from '../common';
import { resourceMultiSelectDropdown, serviceDropdown, eventStatusDropdown } from '../common/props';
export const updateEventAction = createAction({
auth: smoothScheduleAuth,
name: 'update_event',
displayName: 'Update Event',
description: 'Updates an existing event in SmoothSchedule.',
props: {
eventId: Property.ShortText({
displayName: 'Event ID',
description: 'The ID of the event to update',
required: true,
}),
title: Property.ShortText({
displayName: 'Title',
description: 'New event title',
required: false,
}),
startTime: Property.DateTime({
displayName: 'Start Time',
description: 'New start time',
required: false,
}),
endTime: Property.DateTime({
displayName: 'End Time',
description: 'New end time',
required: false,
}),
serviceId: serviceDropdown({
displayName: 'Service',
description: 'New service',
required: false,
}),
resourceIds: resourceMultiSelectDropdown({
displayName: 'Resources',
description: 'New resources (replaces existing)',
required: false,
}),
status: eventStatusDropdown,
notes: Property.LongText({
displayName: 'Notes',
description: 'New notes',
required: false,
}),
sendNotification: Property.Checkbox({
displayName: 'Send Notification',
description: 'Send update notification to customer',
required: false,
defaultValue: false,
}),
},
async run(context) {
const auth = context.auth as SmoothScheduleAuth;
const props = context.propsValue;
const body: Record<string, unknown> = {};
if (props.title !== undefined) body['title'] = props.title;
if (props.startTime) body['start_time'] = formatDateTime(props.startTime);
if (props.endTime) body['end_time'] = formatDateTime(props.endTime);
if (props.serviceId) body['service'] = props.serviceId;
if (props.status) body['status'] = props.status;
if (props.notes !== undefined) body['notes'] = props.notes;
if (props.resourceIds && props.resourceIds.length > 0) {
body['resource_ids'] = props.resourceIds;
}
if (props.sendNotification !== undefined) {
body['send_notification'] = props.sendNotification;
}
const response = await makeRequest<Record<string, unknown>>(
auth,
HttpMethod.PATCH,
`/events/${props.eventId}/`,
body
);
return response;
},
});

View File

@@ -0,0 +1,183 @@
import {
HttpMethod,
httpClient,
HttpRequest,
} from '@activepieces/pieces-common';
import { SmoothScheduleAuth } from '../../index';
export const API_URL = '/v1';
/**
* Make an authenticated request to the SmoothSchedule API
*
* Uses Bearer token authentication with ss_live_* or ss_test_* tokens.
* Authorization: Bearer ss_live_xxxxx
*/
export async function makeRequest<T>(
auth: SmoothScheduleAuth,
method: HttpMethod,
endpoint: string,
body?: Record<string, unknown>,
queryParams?: Record<string, string>
): Promise<T> {
// When running inside Docker, the baseUrl might be "http://django:8000" (internal Docker network).
// Django-tenants requires a valid Host header for tenant resolution.
// Map Docker internal hostname to the external hostname that Django recognizes.
const url = new URL(auth.props.baseUrl);
let hostHeader = `${url.hostname}${url.port ? ':' + url.port : ''}`;
// Map docker hostname to lvh.me (which Django recognizes)
if (url.hostname === 'django') {
hostHeader = `lvh.me${url.port ? ':' + url.port : ''}`;
}
const request: HttpRequest = {
method,
url: `${auth.props.baseUrl}${API_URL}${endpoint}`,
body,
queryParams,
headers: {
'X-Tenant': auth.props.subdomain,
'Authorization': `Bearer ${auth.props.apiToken}`,
'Host': hostHeader,
},
};
const response = await httpClient.sendRequest<T>(request);
return response.body;
}
/**
* Fetch resources (staff, rooms, equipment) for dropdown options
*/
export async function fetchResources(auth: SmoothScheduleAuth, type?: string) {
interface ResourceType {
id: string;
name: string;
category: string;
}
interface Resource {
id: string;
name: string;
resource_type: ResourceType;
is_active: boolean;
}
interface PaginatedResponse {
results: Resource[];
count: number;
}
const queryParams: Record<string, string> = {};
if (type) {
queryParams['type'] = type;
}
const response = await makeRequest<PaginatedResponse>(
auth,
HttpMethod.GET,
'/resources/',
undefined,
queryParams
);
const resources = response.results || [];
return resources
.filter((r) => r.is_active)
.map((r) => ({
label: `${r.name} (${r.resource_type?.name || 'Unknown'})`,
value: r.id,
}));
}
/**
* Fetch services for dropdown options
*/
export async function fetchServices(auth: SmoothScheduleAuth) {
interface Service {
id: string;
name: string;
duration: number;
is_active: boolean;
}
interface PaginatedResponse {
results: Service[];
count: number;
}
const response = await makeRequest<PaginatedResponse>(
auth,
HttpMethod.GET,
'/services/'
);
const services = response.results || [];
return services
.filter((s) => s.is_active)
.map((s) => ({
label: `${s.name} (${s.duration} min)`,
value: s.id,
}));
}
/**
* Fetch customers for dropdown options
*/
export async function fetchCustomers(auth: SmoothScheduleAuth, search?: string) {
interface Customer {
id: string;
email: string;
first_name: string;
last_name: string;
}
interface PaginatedResponse {
results: Customer[];
count: number;
}
const queryParams: Record<string, string> = {};
if (search) {
queryParams['search'] = search;
}
const response = await makeRequest<PaginatedResponse>(
auth,
HttpMethod.GET,
'/customers/',
undefined,
queryParams
);
const customers = response.results || [];
return customers.map((c) => ({
label: `${c.first_name} ${c.last_name} (${c.email})`,
value: c.id,
}));
}
/**
* Fetch appointment/event details by ID
*/
export async function getEventDetails(auth: SmoothScheduleAuth, eventId: string) {
return makeRequest<Record<string, unknown>>(
auth,
HttpMethod.GET,
`/appointments/${eventId}/`
);
}
/**
* Format ISO datetime string
*/
export function formatDateTime(date: string | Date): string {
if (typeof date === 'string') {
return date;
}
return date.toISOString();
}

View File

@@ -0,0 +1,187 @@
import { Property, DropdownOption } from '@activepieces/pieces-framework';
import { smoothScheduleAuth, SmoothScheduleAuth } from '../../index';
import { fetchResources, fetchServices, fetchCustomers } from './index';
/**
* Resource dropdown property factory
*/
export function resourceDropdown(options: {
displayName: string;
description?: string;
required: boolean;
resourceType?: 'STAFF' | 'ROOM' | 'EQUIPMENT';
}) {
return Property.Dropdown({
auth: smoothScheduleAuth,
displayName: options.displayName,
description: options.description,
required: options.required,
refreshers: ['auth'],
options: async (propsValue) => {
const auth = propsValue.auth as SmoothScheduleAuth;
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect your SmoothSchedule account first',
options: [],
};
}
try {
const resources = await fetchResources(auth, options.resourceType);
return {
disabled: false,
options: resources as DropdownOption<string>[],
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to load resources. Check your connection.',
options: [],
};
}
},
});
}
/**
* Multi-select resource dropdown for selecting multiple resources
*/
export function resourceMultiSelectDropdown(options: {
displayName: string;
description?: string;
required: boolean;
resourceType?: 'STAFF' | 'ROOM' | 'EQUIPMENT';
}) {
return Property.MultiSelectDropdown({
auth: smoothScheduleAuth,
displayName: options.displayName,
description: options.description,
required: options.required,
refreshers: ['auth'],
options: async (propsValue) => {
const auth = propsValue.auth as SmoothScheduleAuth;
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect your SmoothSchedule account first',
options: [],
};
}
try {
const resources = await fetchResources(auth, options.resourceType);
return {
disabled: false,
options: resources as DropdownOption<string>[],
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to load resources. Check your connection.',
options: [],
};
}
},
});
}
/**
* Service dropdown property factory
*/
export function serviceDropdown(options: {
displayName: string;
description?: string;
required: boolean;
}) {
return Property.Dropdown({
auth: smoothScheduleAuth,
displayName: options.displayName,
description: options.description,
required: options.required,
refreshers: ['auth'],
options: async (propsValue) => {
const auth = propsValue.auth as SmoothScheduleAuth;
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect your SmoothSchedule account first',
options: [],
};
}
try {
const services = await fetchServices(auth);
return {
disabled: false,
options: services as DropdownOption<string>[],
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to load services. Check your connection.',
options: [],
};
}
},
});
}
/**
* Customer dropdown with search capability
*/
export function customerDropdown(options: {
displayName: string;
description?: string;
required: boolean;
}) {
return Property.Dropdown({
auth: smoothScheduleAuth,
displayName: options.displayName,
description: options.description,
required: options.required,
refreshers: ['auth'],
options: async (propsValue) => {
const auth = propsValue.auth as SmoothScheduleAuth;
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect your SmoothSchedule account first',
options: [],
};
}
try {
const customers = await fetchCustomers(auth);
return {
disabled: false,
options: customers as DropdownOption<string>[],
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to load customers. Check your connection.',
options: [],
};
}
},
});
}
/**
* Event status dropdown
*/
export const eventStatusDropdown = Property.StaticDropdown({
displayName: 'Status',
description: 'Event status',
required: false,
options: {
options: [
{ label: 'Confirmed', value: 'CONFIRMED' },
{ label: 'Pending', value: 'PENDING' },
{ label: 'Cancelled', value: 'CANCELLED' },
{ label: 'Completed', value: 'COMPLETED' },
{ label: 'No Show', value: 'NO_SHOW' },
],
},
});

View File

@@ -0,0 +1,117 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { smoothScheduleAuth, SmoothScheduleAuth } from '../../index';
import { makeRequest } from '../common';
import { resourceDropdown, serviceDropdown } from '../common/props';
const TRIGGER_KEY = 'last_cancelled_check';
export const eventCancelledTrigger = createTrigger({
auth: smoothScheduleAuth,
name: 'event_cancelled',
displayName: 'Event Cancelled',
description: 'Triggers when an event is cancelled in SmoothSchedule.',
props: {
resourceId: resourceDropdown({
displayName: 'Resource',
description: 'Only trigger for events with this resource (optional)',
required: false,
}),
serviceId: serviceDropdown({
displayName: 'Service',
description: 'Only trigger for events with this service (optional)',
required: false,
}),
},
type: TriggerStrategy.POLLING,
async onEnable(context) {
// Store the current timestamp as the starting point
await context.store.put(TRIGGER_KEY, new Date().toISOString());
},
async onDisable(context) {
await context.store.delete(TRIGGER_KEY);
},
async test(context) {
const auth = context.auth as SmoothScheduleAuth;
const { resourceId, serviceId } = context.propsValue;
const queryParams: Record<string, string> = {
limit: '5',
status: 'CANCELLED',
ordering: '-updated_at',
};
if (resourceId) {
queryParams['resource'] = resourceId.toString();
}
if (serviceId) {
queryParams['service'] = serviceId.toString();
}
const events = await makeRequest<Array<Record<string, unknown>>>(
auth,
HttpMethod.GET,
'/events/',
undefined,
queryParams
);
return events;
},
async run(context) {
const auth = context.auth as SmoothScheduleAuth;
const { resourceId, serviceId } = context.propsValue;
const lastCheck = await context.store.get<string>(TRIGGER_KEY) || new Date(0).toISOString();
// Look for events that were updated recently and have CANCELLED status
const queryParams: Record<string, string> = {
status: 'CANCELLED',
ordering: 'updated_at',
updated_at__gt: lastCheck,
};
if (resourceId) {
queryParams['resource'] = resourceId.toString();
}
if (serviceId) {
queryParams['service'] = serviceId.toString();
}
const events = await makeRequest<Array<{ updated_at: string } & Record<string, unknown>>>(
auth,
HttpMethod.GET,
'/events/',
undefined,
queryParams
);
// Update the last check timestamp
await context.store.put(TRIGGER_KEY, new Date().toISOString());
return events;
},
sampleData: {
id: 12345,
title: 'Consultation',
start_time: '2024-12-01T10:00:00Z',
end_time: '2024-12-01T11:00:00Z',
status: 'CANCELLED',
cancellation_reason: 'Customer requested cancellation',
service: {
id: 1,
name: 'Consultation',
},
customer: {
id: 100,
first_name: 'John',
last_name: 'Doe',
email: 'john.doe@example.com',
},
resources: [
{ id: 1, name: 'Dr. Smith', type: 'STAFF' },
],
created_at: '2024-11-28T14:30:00Z',
updated_at: '2024-11-30T11:00:00Z',
},
});

View File

@@ -0,0 +1,133 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { smoothScheduleAuth, SmoothScheduleAuth } from '../../index';
import { makeRequest } from '../common';
import { resourceDropdown, serviceDropdown } from '../common/props';
const TRIGGER_KEY = 'last_event_created_id';
export const eventCreatedTrigger = createTrigger({
auth: smoothScheduleAuth,
name: 'event_created',
displayName: 'Event Created',
description: 'Triggers when a new event is created in SmoothSchedule.',
props: {
resourceId: resourceDropdown({
displayName: 'Resource',
description: 'Only trigger for events with this resource (optional)',
required: false,
}),
serviceId: serviceDropdown({
displayName: 'Service',
description: 'Only trigger for events with this service (optional)',
required: false,
}),
},
type: TriggerStrategy.POLLING,
async onEnable(context) {
// Get the most recent event ID to start from
const auth = context.auth as SmoothScheduleAuth;
try {
const events = await makeRequest<Array<{ id: number }>>(
auth,
HttpMethod.GET,
'/events/',
undefined,
{ limit: '1', ordering: '-created_at' }
);
if (events.length > 0) {
await context.store.put(TRIGGER_KEY, events[0].id);
} else {
await context.store.put(TRIGGER_KEY, 0);
}
} catch (error) {
await context.store.put(TRIGGER_KEY, 0);
}
},
async onDisable(context) {
await context.store.delete(TRIGGER_KEY);
},
async test(context) {
const auth = context.auth as SmoothScheduleAuth;
const { resourceId, serviceId } = context.propsValue;
const queryParams: Record<string, string> = {
limit: '5',
ordering: '-created_at',
};
if (resourceId) {
queryParams['resource'] = resourceId.toString();
}
if (serviceId) {
queryParams['service'] = serviceId.toString();
}
const events = await makeRequest<Array<Record<string, unknown>>>(
auth,
HttpMethod.GET,
'/events/',
undefined,
queryParams
);
return events;
},
async run(context) {
const auth = context.auth as SmoothScheduleAuth;
const { resourceId, serviceId } = context.propsValue;
const lastEventId = await context.store.get<number>(TRIGGER_KEY) || 0;
const queryParams: Record<string, string> = {
ordering: 'created_at',
id__gt: lastEventId.toString(),
};
if (resourceId) {
queryParams['resource'] = resourceId.toString();
}
if (serviceId) {
queryParams['service'] = serviceId.toString();
}
const events = await makeRequest<Array<{ id: number } & Record<string, unknown>>>(
auth,
HttpMethod.GET,
'/events/',
undefined,
queryParams
);
if (events.length > 0) {
// Update the last event ID
const maxId = Math.max(...events.map((e) => e.id));
await context.store.put(TRIGGER_KEY, maxId);
}
return events;
},
sampleData: {
id: 12345,
title: 'Consultation',
start_time: '2024-12-01T10:00:00Z',
end_time: '2024-12-01T11:00:00Z',
status: 'CONFIRMED',
service: {
id: 1,
name: 'Consultation',
},
customer: {
id: 100,
first_name: 'John',
last_name: 'Doe',
email: 'john.doe@example.com',
},
resources: [
{ id: 1, name: 'Dr. Smith', type: 'STAFF' },
],
created_at: '2024-11-28T14:30:00Z',
},
});

View File

@@ -0,0 +1,119 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { smoothScheduleAuth, SmoothScheduleAuth } from '../../index';
import { makeRequest } from '../common';
import { resourceDropdown, serviceDropdown } from '../common/props';
const TRIGGER_KEY = 'last_event_updated_at';
export const eventUpdatedTrigger = createTrigger({
auth: smoothScheduleAuth,
name: 'event_updated',
displayName: 'Event Updated',
description: 'Triggers when an event is updated in SmoothSchedule.',
props: {
resourceId: resourceDropdown({
displayName: 'Resource',
description: 'Only trigger for events with this resource (optional)',
required: false,
}),
serviceId: serviceDropdown({
displayName: 'Service',
description: 'Only trigger for events with this service (optional)',
required: false,
}),
},
type: TriggerStrategy.POLLING,
async onEnable(context) {
// Store the current timestamp as the starting point
await context.store.put(TRIGGER_KEY, new Date().toISOString());
},
async onDisable(context) {
await context.store.delete(TRIGGER_KEY);
},
async test(context) {
const auth = context.auth as SmoothScheduleAuth;
const { resourceId, serviceId } = context.propsValue;
const queryParams: Record<string, string> = {
limit: '5',
ordering: '-updated_at',
};
if (resourceId) {
queryParams['resource'] = resourceId.toString();
}
if (serviceId) {
queryParams['service'] = serviceId.toString();
}
const events = await makeRequest<Array<Record<string, unknown>>>(
auth,
HttpMethod.GET,
'/events/',
undefined,
queryParams
);
return events;
},
async run(context) {
const auth = context.auth as SmoothScheduleAuth;
const { resourceId, serviceId } = context.propsValue;
const lastUpdatedAt = await context.store.get<string>(TRIGGER_KEY) || new Date(0).toISOString();
const queryParams: Record<string, string> = {
ordering: 'updated_at',
updated_at__gt: lastUpdatedAt,
};
if (resourceId) {
queryParams['resource'] = resourceId.toString();
}
if (serviceId) {
queryParams['service'] = serviceId.toString();
}
const events = await makeRequest<Array<{ updated_at: string } & Record<string, unknown>>>(
auth,
HttpMethod.GET,
'/events/',
undefined,
queryParams
);
if (events.length > 0) {
// Update the last updated timestamp
const maxUpdatedAt = events.reduce((max, e) =>
e.updated_at > max ? e.updated_at : max,
lastUpdatedAt
);
await context.store.put(TRIGGER_KEY, maxUpdatedAt);
}
return events;
},
sampleData: {
id: 12345,
title: 'Consultation',
start_time: '2024-12-01T10:00:00Z',
end_time: '2024-12-01T11:00:00Z',
status: 'CONFIRMED',
service: {
id: 1,
name: 'Consultation',
},
customer: {
id: 100,
first_name: 'John',
last_name: 'Doe',
email: 'john.doe@example.com',
},
resources: [
{ id: 1, name: 'Dr. Smith', type: 'STAFF' },
],
created_at: '2024-11-28T14:30:00Z',
updated_at: '2024-11-29T09:15:00Z',
},
});

View File

@@ -0,0 +1,3 @@
export * from './event-created';
export * from './event-updated';
export * from './event-cancelled';

View File

@@ -0,0 +1,19 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noPropertyAccessFromIndexSignature": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}

View File

@@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "../../../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
"include": ["src/**/*.ts"]
}