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,198 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import dayjs from 'dayjs';
import {
apDayjs,
getCorrectedFormat,
optionalTimeFormats,
parseDate,
timeFormat,
timeFormatDescription,
timeParts,
timeZoneOptions,
} from '../common';
import { z } from 'zod';
import { propsValidation } from '@activepieces/pieces-common';
export const addSubtractDateAction = createAction({
name: 'add_subtract_date',
displayName: 'Add/Subtract Time',
description: 'Add or subtract time from a date',
errorHandlingOptions: {
continueOnFailure: {
hide: true,
},
retryOnFailure: {
hide: true,
},
},
props: {
inputDate: Property.ShortText({
displayName: 'Input Date',
description: 'Enter the input date',
required: true,
}),
inputDateFormat: Property.StaticDropdown({
displayName: 'From Time Format',
description: timeFormatDescription,
options: {
options: optionalTimeFormats,
},
required: true,
defaultValue: getCorrectedFormat(timeFormat.format00),
}),
outputFormat: Property.StaticDropdown({
displayName: 'To Time Format',
description: timeFormatDescription,
options: {
options: optionalTimeFormats,
},
required: true,
defaultValue: getCorrectedFormat(timeFormat.format00),
}),
expression: Property.LongText({
displayName: 'Expression',
description: `Provide an expression to add or subtract using the following units (year , month , day , hour , minute or second).
\nExamples:\n+ 2 second + 1 hour \n+ 1 year - 3 day - 2 month \n+ 5 minute`,
required: true,
}),
timeZone: Property.StaticDropdown<string>({
displayName: 'Time Zone',
description: 'Optional: Set a timezone for the calculation to handle DST correctly',
options: {
options: timeZoneOptions,
},
required: false,
}),
setTime: Property.ShortText({
displayName: 'Set Time To (24h format)',
description: 'Optional: Set the result to a specific time (e.g., "10:00" or "14:30"). This allows scheduling at a specific time after adding/subtracting dates. Leave empty to keep the calculated time.',
required: false,
}),
useCurrentTime: Property.Checkbox({
displayName: 'Use Current Time',
description: 'If checked, the current time will be used instead of the time from "Set Time To" field.',
required: false,
defaultValue: false,
}),
},
async run(context) {
// Ensure all dayjs plugins are properly extended
const inputDate = context.propsValue.inputDate;
const inputDateFormat = getCorrectedFormat(context.propsValue.inputDateFormat);
const outputFormat = getCorrectedFormat(context.propsValue.outputFormat);
const expression = context.propsValue.expression;
const timeZone = context.propsValue.timeZone as string | undefined;
const setTime = context.propsValue.setTime as string | undefined;
const useCurrentTime = context.propsValue.useCurrentTime as boolean;
if (setTime && setTime.trim() !== '') {
await propsValidation.validateZod({ time: setTime }, {
time: z.string().regex(/^\d\d:\d\d$/),
});
}
const BeforeDate = parseDate(inputDate, inputDateFormat);
let AfterDate = addSubtractTime(BeforeDate.toDate(), expression, timeZone);
if (timeZone && (setTime || useCurrentTime)) {
let timeToSet = setTime;
if (useCurrentTime) {
const now = apDayjs().tz(timeZone);
timeToSet = `${now.hour().toString().padStart(2, '0')}:${now.minute().toString().padStart(2, '0')}`;
}
if (timeToSet && timeToSet.trim() !== '') {
const [hours, minutes] = timeToSet.split(':').map(Number);
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
throw new Error(
`Invalid time value - hours: ${hours} (must be 0-23), minutes: ${minutes} (must be 0-59)`
);
}
AfterDate = AfterDate.tz(timeZone).hour(hours).minute(minutes).second(0).millisecond(0);
}
}
if (timeZone) {
return { result: AfterDate.tz(timeZone).format(outputFormat) };
} else {
return { result: AfterDate.format(outputFormat) };
}
},
});
function addSubtractTime(date: Date, expression: string, timeZone?: string): dayjs.Dayjs {
// remove all the spaces and line breaks from the expression
expression = expression.replace(/(\r\n|\n|\r)/gm, '').replace(/ /g, '');
const parts = expression.split(/(\+|-)/);
let sign = 1;
const numbers = [];
const units = [];
for (let i = 0; i < parts.length; i++) {
if (parts[i] === '+') sign = 1;
else if (parts[i] === '-') sign = -1;
else if (parts[i] === '') continue;
let number = '';
let unit = '';
for (let j = 0; j < parts[i].length; j++) {
if (parts[i][j] === ' ') continue;
if (parts[i][j] >= '0' && parts[i][j] <= '9') {
if (unit !== '') {
numbers.push(sign * parseInt(number));
units.push(unit);
number = '';
unit = '';
}
number += parts[i][j];
} else {
if (number === '') continue;
unit += parts[i][j];
}
}
if (unit !== '') {
numbers.push(sign * parseInt(number));
units.push(unit);
}
}
// Create timezone-aware dayjs object if timezone is provided
let dayjsDate = timeZone ? apDayjs(date).tz(timeZone) : apDayjs(date);
for (let i = 0; i < numbers.length; i++) {
const val = units[i].toLowerCase() as timeParts;
switch (val) {
case timeParts.year:
dayjsDate = dayjsDate.add(numbers[i], 'year');
break;
case timeParts.month:
dayjsDate = dayjsDate.add(numbers[i], 'month');
break;
case timeParts.day:
dayjsDate = dayjsDate.add(numbers[i], 'day');
break;
case timeParts.hour:
dayjsDate = dayjsDate.add(numbers[i], 'hour');
break;
case timeParts.minute:
dayjsDate = dayjsDate.add(numbers[i], 'minute');
break;
case timeParts.second:
dayjsDate = dayjsDate.add(numbers[i], 'second');
break;
case timeParts.dayOfWeek:
case timeParts.monthName:
case timeParts.unix_time:
break;
default: {
const nvr: never = val;
console.error(nvr, 'unhandled case was reached');
}
}
}
return dayjsDate;
}

View File

@@ -0,0 +1,112 @@
import { Property, createAction } from '@activepieces/pieces-framework'
import {
optionalTimeFormats,
timeFormat,
timeParts,
timeFormatDescription,
parseDate,
getCorrectedFormat,
apDayjs,
} from '../common';
export const dateDifferenceAction = createAction({
name: 'date_difference',
displayName: 'Date Difference',
description: 'Get the difference between two dates',
errorHandlingOptions: {
continueOnFailure: {
hide: true,
},
retryOnFailure: {
hide: true,
},
},
props: {
startDate: Property.ShortText({
displayName: 'Starting Date',
description: 'Enter the starting date',
required: true,
}),
startDateFormat: Property.StaticDropdown({
displayName: 'Starting date format',
description: timeFormatDescription,
options: {
options: optionalTimeFormats,
},
required: true,
defaultValue: timeFormat.format00,
}),
endDate: Property.ShortText({
displayName: 'Ending Date',
description: 'Enter the ending date',
required: true,
}),
endDateFormat: Property.StaticDropdown({
displayName: 'Ending date format',
description: timeFormatDescription,
options: {
options: optionalTimeFormats,
},
required: true,
defaultValue: timeFormat.format00,
}),
unitDifference: Property.StaticMultiSelectDropdown({
displayName: 'Unit',
description: 'Select the unit of difference between the two dates',
options: {
options: [
{ label: 'Year', value: timeParts.year },
{ label: 'Month', value: timeParts.month },
{ label: 'Day', value: timeParts.day },
{ label: 'Hour', value: timeParts.hour },
{ label: 'Minute', value: timeParts.minute },
{ label: 'Second', value: timeParts.second },
],
},
required: true,
defaultValue: [timeParts.year],
}),
},
async run(context) {
const inputStartDate = context.propsValue.startDate;
const startDateFormat = getCorrectedFormat(context.propsValue.startDateFormat);
const inputEndDate = context.propsValue.endDate;
const endDateFormat = getCorrectedFormat(context.propsValue.endDateFormat);
const startDate = parseDate(inputStartDate, startDateFormat);
const endDate = parseDate(inputEndDate, endDateFormat);
const unitDifference = context.propsValue.unitDifference;
const difference = apDayjs.duration(endDate.diff(startDate));
const outputresponse: Record<string, number> = {};
for (let i = 0; i < unitDifference.length; i++) {
switch (unitDifference[i]) {
case timeParts.year:
outputresponse[timeParts.year] = difference.years();
break;
case timeParts.month:
outputresponse[timeParts.month] = difference.months();
break;
case timeParts.day:
outputresponse[timeParts.day] = difference.days();
break;
case timeParts.hour:
outputresponse[timeParts.hour] = difference.hours();
break;
case timeParts.minute:
outputresponse[timeParts.minute] = difference.minutes();
break;
case timeParts.second:
outputresponse[timeParts.second] = difference.seconds();
break;
default:
throw new Error(
`Invalid unit :\n${JSON.stringify(unitDifference[i])}`
);
}
}
return outputresponse;
},
});

View File

@@ -0,0 +1,103 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import {
optionalTimeFormats,
timeFormat,
timeParts,
timeFormatDescription,
parseDate,
getCorrectedFormat,
} from '../common';
export const extractDateParts = createAction({
name: 'extract_date_parts',
displayName: 'Extract Date Units',
description:
'Extract date units ( year , month , day , hour , minute , second , day of week , month name ) from a date',
errorHandlingOptions: {
continueOnFailure: {
hide: true,
},
retryOnFailure: {
hide: true,
},
},
props: {
inputDate: Property.ShortText({
displayName: 'Input Date',
description: 'Enter the input date',
required: true,
}),
inputFormat: Property.StaticDropdown({
displayName: 'From Time Format',
description: timeFormatDescription,
options: {
options: optionalTimeFormats,
},
required: true,
defaultValue: timeFormat.format00,
}),
unitExtract: Property.StaticMultiSelectDropdown({
displayName: 'Unit to Extract',
description: 'Select the unit to extract from the date',
options: {
options: [
{ label: 'Year', value: timeParts.year },
{ label: 'Month', value: timeParts.month },
{ label: 'Day', value: timeParts.day },
{ label: 'Hour', value: timeParts.hour },
{ label: 'Minute', value: timeParts.minute },
{ label: 'Second', value: timeParts.second },
{ label: 'Day of Week', value: timeParts.dayOfWeek },
{ label: 'Month name', value: timeParts.monthName },
],
},
required: true,
defaultValue: [timeParts.year],
}),
},
async run(context) {
const inputDate = context.propsValue.inputDate;
const inputFormat = getCorrectedFormat(context.propsValue.inputFormat);
const unitExtract = context.propsValue.unitExtract;
const BeforeDate = parseDate(inputDate, inputFormat);
const outputresponse: Record<string, any> = {};
for (let i = 0; i < unitExtract.length; i++) {
switch (unitExtract[i]) {
case timeParts.year:
outputresponse[timeParts.year] = BeforeDate.year();
break;
case timeParts.month:
// dayjs months are 0-indexed
outputresponse[timeParts.month] = BeforeDate.month() + 1;
break;
case timeParts.day:
outputresponse[timeParts.day] = BeforeDate.date();
break;
case timeParts.hour:
outputresponse[timeParts.hour] = BeforeDate.hour();
break;
case timeParts.minute:
outputresponse[timeParts.minute] = BeforeDate.minute();
break;
case timeParts.second:
outputresponse[timeParts.second] = BeforeDate.second();
break;
case timeParts.dayOfWeek:
outputresponse[timeParts.dayOfWeek] = BeforeDate.format('dddd');
break;
case timeParts.monthName:
outputresponse[timeParts.monthName] = BeforeDate.format('MMMM');
break;
case timeParts.unix_time:
default:
throw new Error(
`Invalid unit to extract :\n${JSON.stringify(unitExtract[i])}`
);
}
}
return outputresponse;
},
});

View File

@@ -0,0 +1,96 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import {
optionalTimeFormats,
parseDate,
timeFormat,
timeFormatDescription,
timeZoneOptions,
getCorrectedFormat,
} from '../common';
export const formatDateAction = createAction({
name: 'format_date',
displayName: 'Format Date',
description: 'Converts a date from one format to another',
errorHandlingOptions: {
continueOnFailure: {
hide: true,
},
retryOnFailure: {
hide: true,
},
},
props: {
inputDate: Property.ShortText({
displayName: 'Input Date',
description: 'Enter the input date',
required: true,
}),
inputFormat: Property.StaticDropdown({
displayName: 'From Time Format',
description: timeFormatDescription,
options: {
options: optionalTimeFormats,
},
required: true,
defaultValue: timeFormat.format00,
}),
inputTimeZone: Property.StaticDropdown<string>({
displayName: 'From Time Zone',
options: {
options: timeZoneOptions,
},
required: true,
defaultValue: 'UTC',
}),
outputFormat: Property.StaticDropdown({
displayName: 'To Time Format',
description: timeFormatDescription,
options: {
options: optionalTimeFormats,
},
required: true,
defaultValue: timeFormat.format00,
}),
outputTimeZone: Property.StaticDropdown<string>({
displayName: 'To Time Zone',
options: {
options: timeZoneOptions,
},
required: true,
defaultValue: 'UTC',
}),
},
async run(context) {
const inputDate = context.propsValue.inputDate;
const inputFormat = getCorrectedFormat(context.propsValue.inputFormat);
const inputTimeZone = context.propsValue.inputTimeZone as string;
const outputFormat = getCorrectedFormat(context.propsValue.outputFormat);
const outputTimeZone = context.propsValue.outputTimeZone as string;
return {
result: changeDateFormat(
inputDate,
inputFormat,
inputTimeZone,
outputFormat,
outputTimeZone
),
};
},
});
function changeDateFormat(
inputDate: string,
inputFormat: string,
inputTimeZone: string,
outputFormat: string,
outputTimeZone: string
): string {
const parsedDate = parseDate(inputDate, inputFormat);
return parsedDate.tz(inputTimeZone, true).tz(outputTimeZone).format(outputFormat);
}

View File

@@ -0,0 +1,47 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import {
optionalTimeFormats,
timeFormat,
timeFormatDescription,
timeZoneOptions,
getCorrectedFormat,
apDayjs
} from '../common';
export const getCurrentDate = createAction({
name: 'get_current_date',
displayName: 'Get Current Date',
description: 'Get the current date',
errorHandlingOptions: {
continueOnFailure: {
hide: true,
},
retryOnFailure: {
hide: true,
},
},
props: {
timeFormat: Property.StaticDropdown({
displayName: 'To Time Format',
description: timeFormatDescription,
options: {
options: optionalTimeFormats,
},
required: true,
defaultValue: timeFormat.format00,
}),
timeZone: Property.StaticDropdown<string>({
displayName: 'Time Zone',
options: {
options: timeZoneOptions,
},
required: true,
defaultValue: 'UTC',
}),
},
async run(context) {
const timeFormat = getCorrectedFormat(context.propsValue.timeFormat);
const timeZone = context.propsValue.timeZone;
return { result: apDayjs().tz(timeZone).format(timeFormat) };
},
});

View File

@@ -0,0 +1,127 @@
import {
Property,
createAction,
} from '@activepieces/pieces-framework';
import {
optionalTimeFormats,
timeFormat,
timeFormatDescription,
timeZoneOptions,
getCorrectedFormat,
apDayjs,
} from '../common';
import { z } from 'zod';
import { propsValidation } from '@activepieces/pieces-common';
export const nextDayofWeek = createAction({
name: 'next_day_of_week',
displayName: 'Next Day of Week',
description: 'Get the date and time of the next day of the week',
errorHandlingOptions: {
continueOnFailure: {
hide: true,
},
retryOnFailure: {
hide: true,
},
},
props: {
weekday: Property.StaticDropdown({
displayName: 'Weekday',
description:
'The weekday that you would like to get the date and time of.',
options: {
options: [
{ label: 'Sunday', value: 0 },
{ label: 'Monday', value: 1 },
{ label: 'Tuesday', value: 2 },
{ label: 'Wednesday', value: 3 },
{ label: 'Thursday', value: 4 },
{ label: 'Friday', value: 5 },
{ label: 'Saturday', value: 6 },
],
},
required: true,
}),
time: Property.ShortText({
displayName: '24h Time',
description:
'The time that you would like to get the date and time of. This must be in 24h format.',
required: false,
defaultValue: '00:00',
}),
currentTime: Property.Checkbox({
displayName: 'Use Current Time',
description:
'If checked, the current time will be used instead of the time specified above.',
required: false,
defaultValue: false,
}),
timeFormat: Property.StaticDropdown({
displayName: 'To Time Format',
description: timeFormatDescription,
options: {
options: optionalTimeFormats,
},
required: true,
defaultValue: timeFormat.format00,
}),
timeZone: Property.StaticDropdown<string>({
displayName: 'Time Zone',
options: {
options: timeZoneOptions,
},
required: true,
defaultValue: 'UTC',
}),
},
async run(context) {
await propsValidation.validateZod(context.propsValue, {
time: z.string().regex(/^\d\d:\d\d$/),
});
const timeFormat = getCorrectedFormat(context.propsValue.timeFormat);
const timeZone = context.propsValue.timeZone as string;
const dayIndex = context.propsValue.weekday as number;
const currentTime = context.propsValue.currentTime as boolean;
let time = context.propsValue.time as string;
let nextOccurrence = apDayjs().tz(timeZone);
if (currentTime === true) {
time = `${nextOccurrence.hour()}:${nextOccurrence.minute()}`;
}
const [hours, minutes] = time.split(':').map(Number);
// Validate inputs
if (
dayIndex < 0 ||
dayIndex > 6 ||
hours < 0 ||
hours > 23 ||
minutes < 0 ||
minutes > 59
) {
throw new Error(
`Invalid input \ndayIndex: ${dayIndex} \nhours: ${hours} \nminutes: ${minutes}`
);
}
// Set the time
nextOccurrence = nextOccurrence.hour(hours).minute(minutes).second(0).millisecond(0);
// Calculate the day difference
let dayDiff = dayIndex - nextOccurrence.day();
if (
dayDiff < 0 ||
(dayDiff === 0 && nextOccurrence.isBefore(apDayjs().tz(timeZone)))
) {
// If it's a past day in the week or today but past time, move to next week
dayDiff += 7;
}
// Set the date to the next occurrence of the given day
nextOccurrence = nextOccurrence.add(dayDiff, 'day');
return { result: nextOccurrence.format(timeFormat) };
},
});

View File

@@ -0,0 +1,130 @@
import {
Property,
createAction,
} from '@activepieces/pieces-framework';
import {
optionalTimeFormats,
timeFormat,
timeFormatDescription,
timeZoneOptions,
getCorrectedFormat,
apDayjs,
} from '../common';
import { z } from 'zod';
import { propsValidation } from '@activepieces/pieces-common';
export const nextDayofYear = createAction({
name: 'next_day_of_year',
displayName: 'Next Day of Year',
description: 'Get the date and time of the next day of the year',
errorHandlingOptions: {
continueOnFailure: {
hide: true,
},
retryOnFailure: {
hide: true,
},
},
props: {
month: Property.StaticDropdown({
displayName: 'Month',
description: 'The month that you would like to get the date and time of.',
options: {
options: [
{ label: 'January', value: 1 },
{ label: 'February', value: 2 },
{ label: 'March', value: 3 },
{ label: 'April', value: 4 },
{ label: 'May', value: 5 },
{ label: 'June', value: 6 },
{ label: 'July', value: 7 },
{ label: 'August', value: 8 },
{ label: 'September', value: 9 },
{ label: 'October', value: 10 },
{ label: 'November', value: 11 },
{ label: 'December', value: 12 },
],
},
required: true,
}),
day: Property.Number({
displayName: 'Day of Month',
description:
'The day of the month that you would like to get the date and time of.',
required: true,
defaultValue: 1,
}),
time: Property.ShortText({
displayName: '24h Time',
description:
'The time that you would like to get the date and time of. This must be in 24h format.',
required: false,
defaultValue: '00:00',
}),
currentTime: Property.Checkbox({
displayName: 'Use Current Time',
description:
'If checked, the current time will be used instead of the time specified above.',
required: false,
defaultValue: false,
}),
timeFormat: Property.StaticDropdown({
displayName: 'To Time Format',
description: timeFormatDescription,
options: {
options: optionalTimeFormats,
},
required: true,
defaultValue: timeFormat.format00,
}),
timeZone: Property.StaticDropdown<string>({
displayName: 'Time Zone',
options: {
options: timeZoneOptions,
},
required: true,
defaultValue: 'UTC',
}),
},
async run(context) {
await propsValidation.validateZod(context.propsValue, {
day: z.number().min(1).max(31),
time: z.string().regex(/^\d\d:\d\d$/),
});
const timeFormat = getCorrectedFormat(context.propsValue.timeFormat);
const timeZone = context.propsValue.timeZone as string;
const currentTime = context.propsValue.currentTime as boolean;
const month = context.propsValue.month as number;
const day = context.propsValue.day as number;
let time = context.propsValue.time as string;
let nextOccurrence = apDayjs().tz(timeZone);
if (currentTime === true) {
time = `${nextOccurrence.hour()}:${nextOccurrence.minute()}`;
}
const [hours, minutes] = time.split(':').map(Number);
if (month < 1 || month > 12 || day < 1 || day > 31) {
throw new Error(`Invalid input \nmonth: ${month} \nday: ${day}`);
}
const currentYear = nextOccurrence.year();
nextOccurrence = apDayjs().tz(timeZone)
.year(currentYear)
.month(month - 1)
.date(day)
.hour(hours)
.minute(minutes)
.second(0)
.millisecond(0);
if (nextOccurrence.isBefore(apDayjs().tz(timeZone))) {
nextOccurrence = nextOccurrence.add(1, 'year');
}
return { result: nextOccurrence.format(timeFormat) };
},
});

File diff suppressed because it is too large Load Diff