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,71 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { fragmentAuth } from '../common/auth';
import { fragmentClient } from '../common/client';
import { HttpMethod } from '@activepieces/pieces-common';
export const createTask = createAction({
auth: fragmentAuth,
name: 'create_task',
displayName: 'Create Task',
description: 'Creates a new task in Fragment.',
props: {
title: Property.ShortText({
displayName: 'Title',
description: 'The title of the task',
required: true,
}),
url: Property.ShortText({
displayName: 'URL',
description: 'A URL associated with the task (e.g., link to a ticket or resource)',
required: false,
}),
due_at: Property.DateTime({
displayName: 'Due Date',
description: 'When the task is due',
required: false,
}),
assignee: Property.ShortText({
displayName: 'Assignee',
description: 'Email of the person to assign this task to.',
required: false,
}),
tags: Property.Array({
displayName: 'Tags',
description: 'Tags to categorize the task',
required: false,
}),
custom_fields: Property.Object({
displayName: 'Custom Fields',
description: 'Additional custom fields for the task',
required: false,
}),
},
async run(context) {
const fields: Record<string, any> = {
title: context.propsValue.title,
...(context.propsValue.custom_fields ?? {}),
};
const taskPayload: Record<string, any> = {
fields,
};
if (context.propsValue.url) {
fields['url'] = context.propsValue.url;
}
if (context.propsValue.due_at) {
taskPayload['due_at'] = context.propsValue.due_at;
}
if (context.propsValue.assignee) {
taskPayload['assignee_email'] = context.propsValue.assignee;
}
if (context.propsValue.tags && Array.isArray(context.propsValue.tags)) {
taskPayload['tags'] = context.propsValue.tags;
}
const response = await fragmentClient.makeRequest(HttpMethod.POST, '/tasks', context.auth, taskPayload);
return response;
},
});

View File

@@ -0,0 +1,15 @@
import { createCustomApiCallAction } from '@activepieces/pieces-common';
import { fragmentAuth } from '../common/auth';
export const customApiCall = createCustomApiCallAction({
auth: fragmentAuth,
name: 'custom_api_call',
displayName: 'Custom API Call',
description: 'Make a custom API call to any Fragment endpoint',
baseUrl: () => 'https://api.onfragment.com/api/v1',
authMapping: async (auth) => ({
'Authorization': `Bearer ${auth}`,
'Content-Type': 'application/json',
}),
});

View File

@@ -0,0 +1,28 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { fragmentAuth } from '../common/auth';
import { fragmentClient } from '../common/client';
import { HttpMethod } from '@activepieces/pieces-common';
export const deleteTask = createAction({
auth: fragmentAuth,
name: 'delete_task',
displayName: 'Delete Task',
description: 'Deletes an existing task.',
props: {
task_uid: Property.ShortText({
displayName: 'Task UID',
description: 'The unique identifier of the task to delete',
required: true,
}),
},
async run(context) {
const response = await fragmentClient.makeRequest(
HttpMethod.DELETE,
`/tasks/${context.propsValue.task_uid}`,
context.auth
);
return response;
},
});

View File

@@ -0,0 +1,28 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { fragmentAuth } from '../common/auth';
import { fragmentClient } from '../common/client';
import { HttpMethod } from '@activepieces/pieces-common';
export const getTask = createAction({
auth: fragmentAuth,
name: 'get_task',
displayName: 'Get Task',
description: 'Retrieves details of a specific task.',
props: {
task_uid: Property.ShortText({
displayName: 'Task UID',
description: 'The unique identifier of the task to retrieve',
required: true,
}),
},
async run(context) {
const response = await fragmentClient.makeRequest(
HttpMethod.GET,
`/tasks/${context.propsValue.task_uid}`,
context.auth
);
return response;
},
});

View File

@@ -0,0 +1,60 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { fragmentAuth } from '../common/auth';
import { fragmentClient } from '../common/client';
import { HttpMethod, QueryParams } from '@activepieces/pieces-common';
export const listTasks = createAction({
auth: fragmentAuth,
name: 'list_tasks',
displayName: 'List Tasks',
description: 'Retrieves a list of tasks from.',
props: {
status: Property.StaticDropdown({
displayName: 'Status',
description: 'Filter tasks by status',
required: false,
options: {
options: [
{ label: 'TODO', value: 'TODO' },
{ label: 'STARTED', value: 'STARTED' },
{ label: 'DONE', value: 'DONE' },
],
},
}),
assignee: Property.ShortText({
displayName: 'Assignee',
description: 'Filter tasks by assignee email or ID',
required: false,
}),
limit: Property.Number({
displayName: 'Limit',
description: 'Maximum number of tasks to return (default: 50)',
required: false,
defaultValue: 50,
}),
},
async run(context) {
const queryParams: QueryParams = {};
if (context.propsValue.status) {
queryParams['status'] = context.propsValue.status;
}
if (context.propsValue.assignee) {
queryParams['assignee_uid'] = context.propsValue.assignee;
}
if (context.propsValue.limit !== undefined) {
queryParams['limit'] = context.propsValue.limit.toString();
}
const response = await fragmentClient.makeRequest(
HttpMethod.GET,
'/tasks',
context.auth,
undefined,
queryParams
);
return response;
},
});

View File

@@ -0,0 +1,97 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { fragmentAuth } from '../common/auth';
import { fragmentClient } from '../common/client';
import { HttpMethod } from '@activepieces/pieces-common';
import { isEmpty } from '@activepieces/shared';
export const updateTask = createAction({
auth: fragmentAuth,
name: 'update_task',
displayName: 'Update Task',
description: 'Updates an existing task.',
props: {
task_uid: Property.ShortText({
displayName: 'Task UID',
description: 'The unique identifier of the task to update',
required: true,
}),
title: Property.ShortText({
displayName: 'Title',
description: 'The updated title of the task',
required: false,
}),
url: Property.ShortText({
displayName: 'URL',
description: 'The updated URL for the task',
required: false,
}),
due_at: Property.DateTime({
displayName: 'Due Date',
description: 'The updated due date',
required: false,
}),
status: Property.StaticDropdown({
displayName: 'Status',
description: 'The status of the task',
required: false,
options: {
options: [
{ label: 'TODO', value: 'TODO' },
{ label: 'STARTED', value: 'STARTED' },
{ label: 'DONE', value: 'DONE' },
],
},
}),
assignee: Property.ShortText({
displayName: 'Assignee',
description: 'Email of the person to assign this task to',
required: false,
}),
tags: Property.Array({
displayName: 'Tags',
description: 'Updated tags for the task',
required: false,
}),
custom_fields: Property.Object({
displayName: 'Custom Fields',
description: 'Updated custom fields',
required: false,
}),
},
async run(context) {
const fields: Record<string, any> = {
...(context.propsValue.custom_fields ?? {}),
};
const taskPayload: Record<string, any> = {};
if (context.propsValue.title) {
fields['title'] = context.propsValue.title;
}
if (context.propsValue.url) {
fields['url'] = context.propsValue.url;
}
if (context.propsValue.due_at) {
taskPayload['due_at'] = context.propsValue.due_at;
}
if (context.propsValue.status) {
taskPayload['status'] = context.propsValue.status;
}
if (context.propsValue.assignee) {
taskPayload['assignee_email'] = context.propsValue.assignee;
}
if (context.propsValue.tags && Array.isArray(context.propsValue.tags)) {
taskPayload['tags'] = context.propsValue.tags;
}
if(!isEmpty(fields)) taskPayload['fields'] = fields;
const response = await fragmentClient.makeRequest(
HttpMethod.PATCH,
`/tasks/${context.propsValue.task_uid}`,
context.auth,
taskPayload
);
return response;
},
});

View File

@@ -0,0 +1,33 @@
import { PieceAuth } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
export const fragmentAuth = PieceAuth.SecretText({
displayName: 'API Token',
description: `You can obtain your API token by navigating to [Developer Settings](https://app.onfragment.com/settings/account/developers).`,
required: true,
validate: async ({ auth }) => {
try {
// Validate by attempting to list tasks
await httpClient.sendRequest({
method: HttpMethod.GET,
url: 'https://api.onfragment.com/api/v1/tasks',
headers: {
'Authorization': `Bearer ${auth}`,
'Content-Type': 'application/json',
},
queryParams: {
limit: '1',
},
});
return {
valid: true,
};
} catch (error: any) {
return {
valid: false,
error: 'Invalid API token. Please check your API token and try again.',
};
}
},
});

View File

@@ -0,0 +1,51 @@
import { httpClient, HttpMethod, HttpMessageBody } from '@activepieces/pieces-common';
import { AppConnectionValueForAuthProperty } from '@activepieces/pieces-framework';
import { fragmentAuth } from './auth';
export const BASE_URL = 'https://api.onfragment.com/api/v1';
export interface FragmentTask {
uid?: string;
title: string;
url?: string;
due_at?: string;
priority?: 'low' | 'medium' | 'high' | 'urgent';
status?: 'open' | 'completed' | 'cancelled';
assignee?: string;
tags?: string[];
custom_fields?: Record<string, any>;
created_at?: string;
updated_at?: string;
[key: string]: any;
}
export const fragmentClient = {
async makeRequest<T extends HttpMessageBody = any>(
method: HttpMethod,
path: string,
apiKey: AppConnectionValueForAuthProperty<typeof fragmentAuth>,
body?: any,
queryParams?: Record<string, string>
): Promise<T> {
const url = `${BASE_URL}${path}`;
try {
const response = await httpClient.sendRequest<T>({
method,
url,
headers: {
'Authorization': `Bearer ${apiKey.secret_text}`,
'Content-Type': 'application/json',
},
body,
queryParams,
});
return response.body;
} catch (error: any) {
const errorMessage = error.response?.body?.message || error.message || 'Unknown error occurred';
throw new Error(`Fragment API Error: ${errorMessage}`);
}
},
};

View File

@@ -0,0 +1,3 @@
export * from './auth';
export * from './client';

View File

@@ -0,0 +1,132 @@
import { AppConnectionValueForAuthProperty, createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { fragmentAuth } from '../common/auth';
import { fragmentClient, FragmentTask } from '../common/client';
import { HttpMethod, QueryParams } from '@activepieces/pieces-common';
import { DedupeStrategy, Polling, pollingHelper } from '@activepieces/pieces-common';
import dayjs from 'dayjs';
const polling: Polling<AppConnectionValueForAuthProperty<typeof fragmentAuth>, Record<string, never>> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, lastFetchEpochMS }) => {
const isTest = lastFetchEpochMS > 0;
const qs: QueryParams = {
order_by: 'created_at',
limit: isTest ? '10' : '100',
};
if (!isTest) {
qs['created_at_after'] = dayjs(lastFetchEpochMS).toISOString();
}
const response = await fragmentClient.makeRequest<{ items: FragmentTask[] }>(
HttpMethod.GET,
'/tasks',
auth,
undefined,
qs,
);
const items = response.items.map((task) => ({
epochMilliSeconds: dayjs(task.created_at).valueOf(),
data: task,
}));
return items;
},
};
export const newTaskTrigger = createTrigger({
auth: fragmentAuth,
name: 'new_task',
displayName: 'New Task',
description: 'Triggers when a new task is created.',
props: {},
type: TriggerStrategy.POLLING,
async onEnable(context) {
await pollingHelper.onEnable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
async onDisable(context) {
await pollingHelper.onDisable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
async test(context) {
return await pollingHelper.test(polling, context);
},
async run(context) {
return await pollingHelper.poll(polling, context);
},
sampleData: {
uid: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
archived: true,
status: 'TODO',
legacy_data: {},
skills: ['3c90c3cc-0d44-4b50-8888-8dd25736052a'],
queue_uid: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
assignee_uid: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
participants: {},
case_uid: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
parent_uid: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
due_at: '2023-11-07T05:31:56Z',
snooze_expires_at: '2023-11-07T05:31:56Z',
fields: {},
metadata_form_uid: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
form_data: {},
form_type: '<string>',
assigned_at: '2023-11-07T05:31:56Z',
started_at: '2023-11-07T05:31:56Z',
done_at: '2023-11-07T05:31:56Z',
created_at: '2023-11-07T05:31:56Z',
updated_at: '2023-11-07T05:31:56Z',
assignee_updated_by: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
queue_updated_by: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
is_assigned_player: true,
internal_created_at: '2023-11-07T05:31:56Z',
internal_updated_at: '2023-11-07T05:31:56Z',
external_created_at: '2023-11-07T05:31:56Z',
external_updated_at: '2023-11-07T05:31:56Z',
external_status: 'TODO',
external_status_updated_at: '2023-11-07T05:31:56Z',
external_assignee_uid: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
queue_time: 123,
queue_time_business: 123,
work_time: 123,
work_time_business: 123,
incremental_work_time: 123,
incremental_work_time_business: 123,
resolution_time: 123,
resolution_time_business: 123,
num_children: 123,
num_children_done: 123,
sla_breach_at: '2023-11-07T05:31:56Z',
sla_breach_business_at: '2023-11-07T05:31:56Z',
wait_time: 123,
wait_time_business: 123,
review_status: '<string>',
task_type: '<string>',
played_at: '2023-11-07T05:31:56Z',
assignee: {
uid: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
archived: true,
role: 'super_admin',
email: 'jsmith@example.com',
first_name: '<string>',
last_name: '<string>',
skills: ['3c90c3cc-0d44-4b50-8888-8dd25736052a'],
legacy_data: {},
created_at: '2023-11-07T05:31:56Z',
updated_at: '2023-11-07T05:31:56Z',
is_registered: true,
},
parent: {},
children: [{}],
},
});

View File

@@ -0,0 +1,132 @@
import { AppConnectionValueForAuthProperty, createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { fragmentAuth } from '../common/auth';
import { fragmentClient, FragmentTask } from '../common/client';
import { HttpMethod, QueryParams } from '@activepieces/pieces-common';
import { DedupeStrategy, Polling, pollingHelper } from '@activepieces/pieces-common';
import dayjs from 'dayjs';
const polling: Polling<AppConnectionValueForAuthProperty<typeof fragmentAuth>, Record<string, never>> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, lastFetchEpochMS }) => {
const isTest = lastFetchEpochMS > 0;
const qs: QueryParams = {
order_by: 'updated_at',
limit: isTest ? '10' : '100',
};
if (!isTest) {
qs['updated_at_after'] = dayjs(lastFetchEpochMS).toISOString();
}
const response = await fragmentClient.makeRequest<{ items: FragmentTask[] }>(
HttpMethod.GET,
'/tasks',
auth,
undefined,
qs,
);
const items = response.items.map((task) => ({
epochMilliSeconds: dayjs(task.updated_at).valueOf(),
data: task,
}));
return items;
},
};
export const taskUpdatedTrigger = createTrigger({
auth: fragmentAuth,
name: 'task_updated',
displayName: 'Task Updated',
description: 'Triggers when a task is updated.',
props: {},
type: TriggerStrategy.POLLING,
async onEnable(context) {
await pollingHelper.onEnable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
async onDisable(context) {
await pollingHelper.onDisable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
async test(context) {
return await pollingHelper.test(polling, context);
},
async run(context) {
return await pollingHelper.poll(polling, context);
},
sampleData: {
uid: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
archived: true,
status: 'TODO',
legacy_data: {},
skills: ['3c90c3cc-0d44-4b50-8888-8dd25736052a'],
queue_uid: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
assignee_uid: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
participants: {},
case_uid: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
parent_uid: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
due_at: '2023-11-07T05:31:56Z',
snooze_expires_at: '2023-11-07T05:31:56Z',
fields: {},
metadata_form_uid: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
form_data: {},
form_type: '<string>',
assigned_at: '2023-11-07T05:31:56Z',
started_at: '2023-11-07T05:31:56Z',
done_at: '2023-11-07T05:31:56Z',
created_at: '2023-11-07T05:31:56Z',
updated_at: '2023-11-07T05:31:56Z',
assignee_updated_by: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
queue_updated_by: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
is_assigned_player: true,
internal_created_at: '2023-11-07T05:31:56Z',
internal_updated_at: '2023-11-07T05:31:56Z',
external_created_at: '2023-11-07T05:31:56Z',
external_updated_at: '2023-11-07T05:31:56Z',
external_status: 'TODO',
external_status_updated_at: '2023-11-07T05:31:56Z',
external_assignee_uid: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
queue_time: 123,
queue_time_business: 123,
work_time: 123,
work_time_business: 123,
incremental_work_time: 123,
incremental_work_time_business: 123,
resolution_time: 123,
resolution_time_business: 123,
num_children: 123,
num_children_done: 123,
sla_breach_at: '2023-11-07T05:31:56Z',
sla_breach_business_at: '2023-11-07T05:31:56Z',
wait_time: 123,
wait_time_business: 123,
review_status: '<string>',
task_type: '<string>',
played_at: '2023-11-07T05:31:56Z',
assignee: {
uid: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
archived: true,
role: 'super_admin',
email: 'jsmith@example.com',
first_name: '<string>',
last_name: '<string>',
skills: ['3c90c3cc-0d44-4b50-8888-8dd25736052a'],
legacy_data: {},
created_at: '2023-11-07T05:31:56Z',
updated_at: '2023-11-07T05:31:56Z',
is_registered: true,
},
parent: {},
children: [{}],
},
});