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,92 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
HttpRequest,
HttpMethod,
AuthenticationType,
httpClient,
} from '@activepieces/pieces-common';
import { googleContactsCommon } from '../common';
import { googleContactsAuth } from '../../';
export const googleContactsAddContactAction = createAction({
auth: googleContactsAuth,
name: 'add_contact',
description: 'Add a contact to a Google Contacts account',
displayName: 'Add Contact',
props: {
firstName: Property.ShortText({
displayName: 'First Name',
description: 'The first name of the contact',
required: true,
}),
middleName: Property.ShortText({
displayName: 'Middle Name',
description: 'The middle name of the contact',
required: false,
}),
lastName: Property.ShortText({
displayName: 'Last Name',
description: 'The last name of the contact',
required: true,
}),
jobTitle: Property.ShortText({
displayName: 'Job Title',
description: 'The job title of the contact',
required: false,
}),
company: Property.ShortText({
displayName: 'Company',
description: 'The company of the contact',
required: false,
}),
email: Property.ShortText({
displayName: 'Email',
description: 'The email address of the contact',
required: false,
}),
phoneNumber: Property.ShortText({
displayName: 'Phone Number',
description: 'The phone number of the contact',
required: false,
}),
},
async run(context) {
let requestBody = {
names: [
{
givenName: context.propsValue['firstName'],
middleName: context.propsValue['middleName'],
familyName: context.propsValue['lastName'],
},
],
};
const contact: Record<string, unknown> = {};
if (context.propsValue['email']) {
contact['emailAddresses'] = [{ value: context.propsValue['email'] }];
}
if (context.propsValue['phoneNumber']) {
contact['phoneNumbers'] = [{ value: context.propsValue['phoneNumber'] }];
}
if (context.propsValue['company'] || context.propsValue['jobTitle']) {
contact['organizations'] = [
{
name: context.propsValue['company'] || undefined,
title: context.propsValue['jobTitle'] || undefined,
},
];
}
requestBody = { ...requestBody, ...contact };
const request: HttpRequest<Record<string, unknown>> = {
method: HttpMethod.POST,
url: `${googleContactsCommon.baseUrl}:createContact`,
body: requestBody,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
};
return (await httpClient.sendRequest(request)).body;
},
});

View File

@@ -0,0 +1,88 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
HttpRequest,
HttpMethod,
AuthenticationType,
httpClient,
QueryParams,
} from '@activepieces/pieces-common';
import { googleContactsCommon } from '../common';
import { googleContactsAuth } from '../../';
export const googleContactsSearchContactsAction = createAction({
auth: googleContactsAuth,
name: 'search_contact',
description: 'Search contacts in Google Contacts account.',
displayName: 'Search Contacts',
props: {
query: Property.ShortText({
displayName: 'Query',
description: `The plain-text query for the request.The query is used to match prefix phrases of the fields on a person. For example, a person with name "foo name" matches queries such as "f", "fo", "foo", "foo n", "nam", etc., but not "oo n".`,
required: true,
}),
readMask: Property.StaticMultiSelectDropdown({
displayName: 'Read Mask',
description:
'A field mask to restrict which fields on each person are returned.',
required: true,
options: {
options: [
{ label: 'addresses', value: 'addresses' },
{ label: 'ageRanges', value: 'ageRanges' },
{ label: 'biographies', value: 'biographies' },
{ label: 'birthdays', value: 'birthdays' },
{ label: 'calendarUrls', value: 'calendarUrls' },
{ label: 'clientData', value: 'clientData' },
{ label: 'coverPhotos', value: 'coverPhotos' },
{ label: 'emailAddresses', value: 'emailAddresses' },
{ label: 'events', value: 'events' },
{ label: 'externalIds', value: 'externalIds' },
{ label: 'genders', value: 'genders' },
{ label: 'imClients', value: 'imClients' },
{ label: 'interests', value: 'interests' },
{ label: 'locales', value: 'locales' },
{ label: 'locations', value: 'locations' },
{ label: 'memberships', value: 'memberships' },
{ label: 'metadata', value: 'metadata' },
{ label: 'miscKeywords', value: 'miscKeywords' },
{ label: 'names', value: 'names' },
{ label: 'nicknames', value: 'nicknames' },
{ label: 'occupations', value: 'occupations' },
{ label: 'organizations', value: 'organizations' },
{ label: 'phoneNumbers', value: 'phoneNumbers' },
{ label: 'photos', value: 'photos' },
{ label: 'relations', value: 'relations' },
{ label: 'sipAddresses', value: 'sipAddresses' },
{ label: 'skills', value: 'skills' },
{ label: 'urls', value: 'urls' },
{ label: 'userDefined', value: 'userDefined' },
],
},
defaultValue: ['names', 'emailAddresses'],
}),
pageSize: Property.Number({
displayName: 'Page Size',
description: 'The number of results to return. Maximum 30.',
required: false,
}),
},
async run(context) {
const qs: QueryParams = {
query: context.propsValue['query'],
readMask: context.propsValue['readMask'].join(','),
};
if (context.propsValue['pageSize']) {
qs['pageSize'] = String(context.propsValue['pageSize']);
}
const request: HttpRequest<Record<string, unknown>> = {
method: HttpMethod.GET,
url: `${googleContactsCommon.baseUrl}:searchContacts`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
queryParams: qs,
};
return (await httpClient.sendRequest(request)).body;
},
});

View File

@@ -0,0 +1,133 @@
import {
createAction,
Property,
} from '@activepieces/pieces-framework';
import {
HttpRequest,
HttpMethod,
AuthenticationType,
httpClient,
QueryParams,
} from '@activepieces/pieces-common';
import { googleContactsCommon } from '../common';
import { googleContactsAuth } from '../../';
export const googleContactsUpdateContactAction = createAction({
auth: googleContactsAuth,
name: 'update_contact',
description: 'Update a contact in Google Contacts account.',
displayName: 'Update Contact',
props: {
resourceName: Property.ShortText({
displayName: 'Resource Name',
description:
'The resource name for the person, assigned by the server. An ASCII string in the form of people/{person_id}.',
required: true,
}),
etag: Property.ShortText({
displayName: 'Etag',
description:
"The `etag` ensures contact updates only apply if the contact hasn't changed since last retrieved.",
required: true,
}),
updatePersonFields: Property.StaticMultiSelectDropdown({
displayName: 'Update Field Mask',
description:
'A field mask to restrict which fields on the person are updated.',
required: true,
options: {
options: [
{ label: 'Names', value: 'names' },
{ label: 'Email', value: 'emailAddresses' },
{ label: 'Phone Number', value: 'phoneNumbers' },
{ label: 'Job Title / Company', value: 'organizations' },
],
},
defaultValue: ['names', 'emailAddresses'],
}),
firstName: Property.ShortText({
displayName: 'First Name',
description: 'The first name of the contact',
required: false,
}),
middleName: Property.ShortText({
displayName: 'Middle Name',
description: 'The middle name of the contact',
required: false,
}),
lastName: Property.ShortText({
displayName: 'Last Name',
description: 'The last name of the contact',
required: false,
}),
jobTitle: Property.ShortText({
displayName: 'Job Title',
description: 'The job title of the contact',
required: false,
}),
company: Property.ShortText({
displayName: 'Company',
description: 'The company of the contact',
required: false,
}),
email: Property.ShortText({
displayName: 'Email',
description: 'The email address of the contact',
required: false,
}),
phoneNumber: Property.ShortText({
displayName: 'Phone Number',
description: 'The phone number of the contact',
required: false,
}),
},
async run(context) {
const resourceName = context.propsValue['resourceName'].substring(6);
const requestBody: Record<string, unknown> = {
etag: context.propsValue['etag'],
};
const qs: QueryParams = {
updatePersonFields: context.propsValue['updatePersonFields'].join(','),
};
if (
context.propsValue['firstName'] ||
context.propsValue['middleName'] ||
context.propsValue['lastName']
) {
requestBody['names'] = [
{
givenName: context.propsValue['firstName'] || undefined,
middleName: context.propsValue['middleName'] || undefined,
familyName: context.propsValue['lastName'] || undefined,
},
];
}
if (context.propsValue['email']) {
requestBody['emailAddresses'] = [{ value: context.propsValue['email'] }];
}
if (context.propsValue['phoneNumber']) {
requestBody['phoneNumbers'] = [
{ value: context.propsValue['phoneNumber'] },
];
}
if (context.propsValue['company'] || context.propsValue['jobTitle']) {
requestBody['organizations'] = [
{
name: context.propsValue['company'] || undefined,
title: context.propsValue['jobTitle'] || undefined,
},
];
}
const request: HttpRequest<Record<string, unknown>> = {
method: HttpMethod.PATCH,
url: `${googleContactsCommon.baseUrl}${resourceName}:updateContact`,
body: requestBody,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
queryParams: qs,
};
return (await httpClient.sendRequest(request)).body;
},
});

View File

@@ -0,0 +1,3 @@
export const googleContactsCommon = {
baseUrl: `https://people.googleapis.com/v1/people`,
};

View File

@@ -0,0 +1,181 @@
import {
AppConnectionValueForAuthProperty,
createTrigger,
OAuth2PropertyValue,
TriggerStrategy,
} from '@activepieces/pieces-framework';
import {
Polling,
DedupeStrategy,
pollingHelper,
} from '@activepieces/pieces-common';
import { googleContactsAuth } from '../../';
import { google } from 'googleapis';
import { OAuth2Client } from 'googleapis-common';
import dayjs from 'dayjs';
const polling: Polling<AppConnectionValueForAuthProperty<typeof googleContactsAuth>, Record<string, never>> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ store, auth }) => {
const authClient = new OAuth2Client();
authClient.setCredentials(auth);
const contactsClient = google.people({ version: 'v1', auth: authClient });
let nextPageToken;
const contactItems: Array<{ data: any; epochMilliSeconds: number }> = [];
do {
const response: any = await contactsClient.people.connections.list({
resourceName: 'people/me',
pageToken: nextPageToken,
pageSize: 100,
sortOrder: 'LAST_MODIFIED_DESCENDING',
personFields: [
'addresses',
'ageRanges',
'biographies',
'birthdays',
'calendarUrls',
'clientData',
'coverPhotos',
'emailAddresses',
'events',
'externalIds',
'genders',
'imClients',
'interests',
'locales',
'locations',
'memberships',
'metadata',
'miscKeywords',
'names',
'nicknames',
'occupations',
'organizations',
'phoneNumbers',
'photos',
'relations',
'sipAddresses',
'skills',
'urls',
'userDefined',
].join(),
});
for (const contact of response.data.connections || []) {
if (contact.metadata?.deleted !== true) {
contactItems.push({
data: contact,
epochMilliSeconds: dayjs(
contact.metadata?.sources?.[0].updateTime
).valueOf(),
});
}
}
nextPageToken = response.data.nextPageToken;
} while (nextPageToken);
return contactItems;
},
};
export const googleContactNewOrUpdatedContact = createTrigger({
auth: googleContactsAuth,
name: 'new_or_updated_contact',
displayName: 'New Or Updated Contact',
description: 'Triggers when there is a new or updated contact',
props: {},
sampleData: {
resourceName: 'people/c4278485694217203807',
etag: '%EiMBAgMFBgcICQoLDA0ODxATFBUWGSEiIyQlJicuNDU3PT4/QBoEAQIFByIMZFVwNlJPNEVKUzg9',
metadata: {
sources: [
{
type: 'CONTACT',
id: '3b603c120c68305f',
etag: '#dUp6RO4EJS8=',
updateTime: '2023-01-30T14:35:18.142565Z',
},
],
objectType: 'PERSON',
},
names: [
{
metadata: {
primary: true,
source: {
type: 'CONTACT',
id: '3b603c120c68305f',
},
},
displayName: 'Shahed Mashni',
familyName: 'Mashni',
givenName: 'Shahed',
displayNameLastFirst: 'Mashni, Shahed',
unstructuredName: 'Shahed Mashni',
},
],
photos: [
{
metadata: {
primary: true,
source: {
type: 'CONTACT',
id: '3b603c120c68305f',
},
},
url: 'https://lh3.googleusercontent.com/cm/AAkddurmZojs4vCcxrpkfSxH9tnqcH-hI82ESDnwv6eq86nZeLStcjYEIe_TCx8r8g5Y=s100',
default: true,
},
],
memberships: [
{
metadata: {
source: {
type: 'CONTACT',
id: '3b603c120c68305f',
},
},
contactGroupMembership: {
contactGroupId: 'myContacts',
contactGroupResourceName: 'contactGroups/myContacts',
},
},
],
},
type: TriggerStrategy.POLLING,
async onEnable(ctx) {
return await pollingHelper.onEnable(polling, {
store: ctx.store,
auth: ctx.auth,
propsValue: {},
});
},
async onDisable(ctx) {
return await pollingHelper.onEnable(polling, {
store: ctx.store,
auth: ctx.auth,
propsValue: {},
});
},
async run(ctx) {
return await pollingHelper.poll(polling, {
store: ctx.store,
auth: ctx.auth,
propsValue: {},
files: ctx.files,
});
},
test: async (ctx) => {
return await pollingHelper.test(polling, {
store: ctx.store,
auth: ctx.auth,
propsValue: {},
files: ctx.files,
});
},
});