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,185 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { zohoDeskAuth } from '../common/auth';
import { zohoDeskApiCall } from '../common';
import { departmentId, organizationId } from '../common/props';
export const createTicketAction = createAction({
auth: zohoDeskAuth,
name: 'create_ticket',
displayName: 'Create Ticket',
description: 'Creates a new ticket.',
props: {
orgId: organizationId({ displayName: 'Organization', required: true }),
departmentId: departmentId({ displayName: 'Department', required: true }),
contactId: Property.ShortText({
displayName: 'Contact ID',
required: false,
description: 'ID of the contact raising the ticket',
}),
subject: Property.ShortText({
displayName: 'Subject',
required: true,
description: 'Subject of the ticket',
}),
description: Property.LongText({
displayName: 'Description',
required: true,
description: 'Description of the issue in the ticket',
}),
email: Property.ShortText({
displayName: 'Email',
required: false,
description: 'Email address of the contact raising the ticket',
}),
phone: Property.ShortText({
displayName: 'Phone',
required: false,
description: 'Phone number of the contact raising the ticket',
}),
status: Property.StaticDropdown({
displayName: 'Status',
required: false,
description: 'Status of the ticket',
options: {
options: [
{ label: 'Open', value: 'Open' },
{ label: 'On Hold', value: 'On Hold' },
{ label: 'Escalated', value: 'Escalated' },
{ label: 'Closed', value: 'Closed' },
],
},
defaultValue: 'Open',
}),
priority: Property.StaticDropdown({
displayName: 'Priority',
required: false,
description: 'Priority of the ticket',
options: {
options: [
{ label: 'Low', value: 'Low' },
{ label: 'Medium', value: 'Medium' },
{ label: 'High', value: 'High' },
{ label: 'Urgent', value: 'Urgent' },
],
},
defaultValue: 'Medium',
}),
category: Property.ShortText({
displayName: 'Category',
required: false,
description: 'Category of the ticket',
}),
subCategory: Property.ShortText({
displayName: 'Sub Category',
required: false,
description: 'Sub-category of the ticket',
}),
dueDate: Property.DateTime({
displayName: 'Due Date',
required: false,
description: 'Due date for the ticket (ISO format)',
}),
channel: Property.StaticDropdown({
displayName: 'Channel',
required: false,
description: 'Channel through which the ticket is created',
options: {
options: [
{ label: 'Email', value: 'Email' },
{ label: 'Phone', value: 'Phone' },
{ label: 'Chat', value: 'Chat' },
{ label: 'Web', value: 'Web' },
{ label: 'Social', value: 'Social' },
],
},
defaultValue: 'Web',
}),
assigneeId: Property.ShortText({
displayName: 'Assignee ID',
required: false,
description: 'ID of the agent to whom the ticket is assigned',
}),
productId: Property.ShortText({
displayName: 'Product ID',
required: false,
description: 'ID of the product to which the ticket belongs',
}),
classification: Property.ShortText({
displayName: 'Classification',
required: false,
description: 'Classification of the ticket',
}),
language: Property.ShortText({
displayName: 'Language',
required: false,
description: 'Language of the ticket',
defaultValue: 'English',
}),
entitySkills: Property.Array({
displayName: 'Entity Skills',
required: false,
description: 'Array of skill IDs associated with the ticket',
}),
customFields: Property.Object({
displayName: 'Custom Fields',
required: false,
description: 'Custom fields in the ticket',
}),
},
async run({ propsValue, auth }) {
const {
orgId,
departmentId,
contactId,
subject,
description,
email,
phone,
status,
priority,
category,
subCategory,
dueDate,
channel,
assigneeId,
productId,
classification,
language,
entitySkills,
customFields,
} = propsValue;
const requestBody: Record<string, any> = {
departmentId,
subject,
description,
};
if (contactId) requestBody['contactId'] = contactId;
if (email) requestBody['email'] = email;
if (phone) requestBody['phone'] = phone;
if (status) requestBody['status'] = status;
if (priority) requestBody['priority'] = priority;
if (category) requestBody['category'] = category;
if (subCategory) requestBody['subCategory'] = subCategory;
if (dueDate) requestBody['dueDate'] = dueDate;
if (channel) requestBody['channel'] = channel;
if (assigneeId) requestBody['assigneeId'] = assigneeId;
if (productId) requestBody['productId'] = productId;
if (classification) requestBody['classification'] = classification;
if (language) requestBody['language'] = language;
if (entitySkills && entitySkills.length > 0) requestBody['entitySkills'] = entitySkills;
if (customFields) requestBody['cf'] = customFields;
const response = await zohoDeskApiCall({
auth,
method: HttpMethod.POST,
resourceUri: '/tickets',
orgId,
body: requestBody,
});
return response;
},
});

View File

@@ -0,0 +1,45 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { zohoDeskApiCall } from '../common';
import { zohoDeskAuth } from '../common/auth';
import { organizationId } from '../common/props';
import { HttpMethod } from '@activepieces/pieces-common';
import { isNil } from '@activepieces/shared';
export const findContactAction = createAction({
auth: zohoDeskAuth,
name: 'find-contact',
displayName: 'Find Contact',
description: 'Finds an existing contact by email.',
props: {
orgId: organizationId({ displayName: 'Organization', required: true }),
email: Property.ShortText({
displayName: 'Email',
required: true,
}),
},
async run(context) {
const response = await zohoDeskApiCall({
auth: context.auth,
method: HttpMethod.GET,
resourceUri: '/contacts/search',
orgId: context.propsValue.orgId,
query: {
email: context.propsValue.email,
},
});
if (isNil(response) || response === '') {
return {
found: false,
result: [],
};
}
const contacts = response as { data: Record<string, any>[] };
return {
found: contacts.data.length > 0,
result: contacts.data,
};
},
});

View File

@@ -0,0 +1,46 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { zohoDeskAuth } from '../common/auth';
import { zohoDeskApiCall } from '../common';
import { organizationId } from '../common/props';
export const listTicketsAction = createAction({
auth: zohoDeskAuth,
name: 'list_tickets',
description: 'List tickets',
displayName: 'List tickets.',
props: {
orgId: organizationId({ displayName: 'Organization', required: true }),
include: Property.StaticMultiSelectDropdown({
displayName: 'include',
required: false,
description: 'Additional information related to the tickets.',
options: {
disabled: false,
options: [
{ label: 'contacts', value: 'contacts' },
{ label: 'products', value: 'products' },
{ label: 'departments', value: 'departments' },
{ label: 'team', value: 'team' },
{ label: 'isRead', value: 'isRead' },
{ label: 'assignee', value: 'assignee' },
],
},
}),
},
async run({ propsValue, auth }) {
const queryParams: Record<string, string> = {};
if (propsValue.include) queryParams['include'] = propsValue.include.join(',');
const response = await zohoDeskApiCall({
auth,
method: HttpMethod.GET,
resourceUri: '/tickets',
orgId: propsValue.orgId,
query: queryParams,
});
return response;
},
});

View File

@@ -0,0 +1,60 @@
import { PieceAuth, Property } from '@activepieces/pieces-framework';
export const zohoDeskAuth = PieceAuth.OAuth2({
props: {
location: Property.StaticDropdown({
displayName: 'Location',
description: 'The location of your Zoho Desk account.',
required: true,
options: {
options: [
{
label: 'zoho.eu (Europe)',
value: 'zoho.eu',
},
{
label: 'zoho.com (United States)',
value: 'zoho.com',
},
{
label: 'zoho.com.au (Australia)',
value: 'zoho.com.au',
},
{
label: 'zoho.jp (Japan)',
value: 'zoho.jp',
},
{
label: 'zoho.in (India)',
value: 'zoho.in',
},
{
label: 'zohocloud.ca (Canada)',
value: 'zohocloud.ca',
},
],
},
}),
},
description: 'Authentication for Zoho Desk',
scope: [
'Desk.tickets.ALL',
'Desk.tasks.ALL',
'Desk.settings.ALL',
'Desk.events.ALL',
'Desk.contacts.READ',
'Desk.contacts.WRITE',
'Desk.contacts.UPDATE',
'Desk.contacts.CREATE',
'Desk.basic.READ',
'Desk.basic.CREATE',
'Desk.search.READ',
'Desk.articles.READ',
'Desk.articles.CREATE',
'Desk.articles.UPDATE',
'Desk.articles.DELETE',
],
authUrl: 'https://accounts.{location}/oauth/v2/auth',
tokenUrl: 'https://accounts.{location}/oauth/v2/token',
required: true,
});

View File

@@ -0,0 +1,59 @@
import {
httpClient,
HttpHeaders,
HttpMessageBody,
HttpMethod,
HttpRequest,
QueryParams,
} from '@activepieces/pieces-common';
import { PiecePropValueSchema } from '@activepieces/pieces-framework';
import { zohoDeskAuth } from './auth';
export type ZohoDeskApiCallParams = {
auth: PiecePropValueSchema<typeof zohoDeskAuth>;
orgId?: string;
method: HttpMethod;
resourceUri: string;
query?: Record<string, string | number | string[] | undefined>;
body?: any;
};
export async function zohoDeskApiCall<T extends HttpMessageBody>({
auth,
orgId,
method,
resourceUri,
query,
body,
}: ZohoDeskApiCallParams): Promise<T> {
const location = auth.props?.['location'] ?? 'zoho.com';
const baseUrl = `https://desk.${location}/api/v1`;
const qs: QueryParams = {};
if (query) {
for (const [key, value] of Object.entries(query)) {
if (value !== null && value !== undefined) {
qs[key] = String(value);
}
}
}
const headers: HttpHeaders = {
Authorization: `Zoho-oauthtoken ${auth.access_token}`,
};
if (orgId) {
headers['orgId'] = orgId;
}
const request: HttpRequest = {
method,
url: baseUrl + resourceUri,
headers,
queryParams: qs,
body,
};
const response = await httpClient.sendRequest<T>(request);
return response.body;
}

View File

@@ -0,0 +1,90 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { PiecePropValueSchema, Property } from '@activepieces/pieces-framework';
import { zohoDeskApiCall } from '.';
import { zohoDeskAuth } from './auth';
interface DropdownParams {
displayName: string;
description?: string;
required: boolean;
}
export const organizationId = (params: DropdownParams) =>
Property.Dropdown({
auth: zohoDeskAuth,
displayName: params.displayName,
description: params.description,
refreshers: [],
required: params.required,
options: async ({ auth }) => {
if (!auth) {
return {
placeholder: 'Please connect your account first.',
options: [],
disabled: true,
};
}
const authValue = auth as PiecePropValueSchema<typeof zohoDeskAuth>;
const response = await zohoDeskApiCall<{ data: { id: string; companyName: string }[] }>({
auth: authValue,
method: HttpMethod.GET,
resourceUri: '/organizations',
});
return {
disabled: false,
options: response.data.map((org) => {
return {
label: org.companyName,
value: org.id,
};
}),
};
},
});
export const departmentId = (params: DropdownParams) =>
Property.Dropdown({
auth: zohoDeskAuth,
displayName: params.displayName,
description: params.description,
refreshers: ['orgId'],
required: params.required,
options: async ({ auth, orgId }) => {
if (!auth) {
return {
placeholder: 'Please connect your account first.',
options: [],
disabled: true,
};
}
if (!orgId) {
return {
placeholder: 'Please select organization first.',
options: [],
disabled: true,
};
}
const authValue = auth as PiecePropValueSchema<typeof zohoDeskAuth>;
const response = await zohoDeskApiCall<{ data: { id: string; name: string }[] }>({
auth: authValue,
method: HttpMethod.GET,
resourceUri: `/departments`,
orgId: orgId as string,
});
return {
disabled: false,
options: response.data.map((department) => {
return {
label: department.name,
value: department.id,
};
}),
};
},
});