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,77 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { assertNotNullOrUndefined } from '@activepieces/shared';
import { todoistRestClient } from '../common/client/rest-client';
import {
todoistProjectIdDropdown,
todoistSectionIdDropdown,
} from '../common/props';
import { TodoistCreateTaskRequest } from '../common/models';
import { todoistAuth } from '../..';
export const todoistCreateTaskAction = createAction({
auth: todoistAuth,
name: 'create_task',
displayName: 'Create Task',
description: 'Create task',
props: {
project_id: todoistProjectIdDropdown(
"Task project ID. If not set, task is put to user's Inbox."
),
content: Property.LongText({
displayName: 'content',
description:
"The task's content. It may contain some markdown-formatted text and hyperlinks",
required: true,
}),
description: Property.LongText({
displayName: 'Description',
description:
'A description for the task. This value may contain some markdown-formatted text and hyperlinks.',
required: false,
}),
labels: Property.Array({
displayName: 'Labels',
required: false,
description:
"The task's labels (a list of names that may represent either personal or shared labels)",
}),
priority: Property.Number({
displayName: 'Priority',
description: 'Task priority from 1 (normal) to 4 (urgent)',
required: false,
}),
due_date: Property.ShortText({
displayName: 'Due date',
description:
"Can be either a specific date in YYYY-MM-DD format relative to user's timezone, a specific date and time in RFC3339 format, or a human defined date (e.g. 'next Monday') using local time",
required: false,
}),
section_id: todoistSectionIdDropdown,
},
async run({ auth, propsValue }) {
const token = auth.access_token;
const {
project_id,
content,
description,
labels,
priority,
due_date,
section_id,
} = propsValue as TodoistCreateTaskRequest;
assertNotNullOrUndefined(token, 'token');
assertNotNullOrUndefined(content, 'content');
return await todoistRestClient.tasks.create({
token,
project_id,
content,
description,
labels,
priority,
due_date,
section_id,
});
},
});

View File

@@ -0,0 +1,36 @@
import { todoistAuth } from '../..';
import { createAction, Property } from '@activepieces/pieces-framework';
import { todoistProjectIdDropdown } from '../common/props';
import { todoistRestClient } from '../common/client/rest-client';
import { assertNotNullOrUndefined } from '@activepieces/shared';
export const todoistFindTaskAction = createAction({
auth: todoistAuth,
name: 'find_task',
displayName: 'Find Task',
description: 'Finds a task by name.',
props: {
name: Property.ShortText({
displayName: 'Name',
description: 'The name of the task to search for.',
required: true,
}),
project_id: todoistProjectIdDropdown(
'Search for tasks within the selected project. If left blank, then all projects are searched.',
),
},
async run(context) {
const token = context.auth.access_token;
const { name, project_id } = context.propsValue;
assertNotNullOrUndefined(token, 'token');
const tasks = await todoistRestClient.tasks.list({ token, project_id });
const matchedTask = tasks.find((task) => task.content == name);
if (!matchedTask) {
throw new Error('Task not found');
} else {
return matchedTask;
}
},
});

View File

@@ -0,0 +1,25 @@
import { assertNotNullOrUndefined } from '@activepieces/shared';
import { todoistAuth } from '../..';
import { createAction, Property } from '@activepieces/pieces-framework';
import { todoistRestClient } from '../common/client/rest-client';
export const todoistMarkTaskCompletedAction = createAction({
auth: todoistAuth,
name: 'mark_task_completed',
displayName: 'Mark Task as Completed',
description: 'Marks a task as being completed.',
props: {
task_id: Property.ShortText({
displayName: 'Task ID',
required: true,
}),
},
async run(context) {
const token = context.auth.access_token;
const { task_id } = context.propsValue;
assertNotNullOrUndefined(token, 'token');
return await todoistRestClient.tasks.close({ token, task_id });
},
});

View File

@@ -0,0 +1,63 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { assertNotNullOrUndefined } from '@activepieces/shared';
import { todoistRestClient } from '../common/client/rest-client';
import { todoistAuth } from '../..';
export const todoistUpdateTaskAction = createAction({
auth: todoistAuth,
name: 'update_task',
displayName: 'Update Task',
description: 'Updates an existing task.',
props: {
task_id: Property.ShortText({
displayName: 'Task ID',
required: true,
}),
content: Property.LongText({
displayName: 'content',
description:
"The task's content. It may contain some markdown-formatted text and hyperlinks",
required: false,
}),
description: Property.LongText({
displayName: 'Description',
description:
'A description for the task. This value may contain some markdown-formatted text and hyperlinks.',
required: false,
}),
labels: Property.Array({
displayName: 'Labels',
required: false,
description:
"The task's labels (a list of names that may represent either personal or shared labels)",
}),
priority: Property.Number({
displayName: 'Priority',
description: 'Task priority from 1 (normal) to 4 (urgent)',
required: false,
}),
due_date: Property.ShortText({
displayName: 'Due date',
description:
"Can be either a specific date in YYYY-MM-DD format relative to user's timezone, a specific date and time in RFC3339 format, or a human defined date (e.g. 'next Monday') using local time",
required: false,
}),
},
async run({ auth, propsValue }) {
const token = auth.access_token;
const { task_id, content, description, priority, due_date } = propsValue;
const labels = propsValue.labels as string[];
assertNotNullOrUndefined(token, 'token');
return await todoistRestClient.tasks.update({
token,
task_id,
content,
description,
labels,
priority,
due_date,
});
},
});

View File

@@ -0,0 +1,189 @@
import {
HttpRequest,
HttpMethod,
AuthenticationType,
httpClient,
} from '@activepieces/pieces-common';
import { isNotUndefined, pickBy } from '@activepieces/shared';
import {
TodoistCreateTaskRequest,
TodoistProject,
TodoistSection,
TodoistTask,
TodoistUpdateTaskRequest,
} from '../models';
const API = 'https://api.todoist.com/rest/v2';
export const todoistRestClient = {
projects: {
async list({ token }: ProjectsListParams): Promise<TodoistProject[]> {
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${API}/projects`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token,
},
};
const response = await httpClient.sendRequest<TodoistProject[]>(request);
return response.body;
},
},
sections: {
async list(params: SectionsListPrams): Promise<TodoistSection[]> {
const qs: Record<string, any> = {};
if (params.project_id) qs['project_id'] = params.project_id;
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${API}/sections`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: params.token,
},
queryParams: qs,
};
const response = await httpClient.sendRequest<TodoistSection[]>(request);
return response.body;
},
},
tasks: {
async create({
token,
project_id,
content,
description,
labels,
priority,
due_date,
section_id,
}: TasksCreateParams): Promise<TodoistTask> {
const body: TodoistCreateTaskRequest = {
content,
project_id,
description,
labels,
priority,
section_id,
...dueDateParams(due_date),
};
const request: HttpRequest<TodoistCreateTaskRequest> = {
method: HttpMethod.POST,
url: `${API}/tasks`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token,
},
body,
};
const response = await httpClient.sendRequest<TodoistTask>(request);
return response.body;
},
async update(params: TasksUpdateParams): Promise<TodoistTask> {
const body: TodoistUpdateTaskRequest = {
content: params.content,
description: params.description,
labels: params.labels?.length === 0 ? undefined : params.labels,
priority: params.priority,
...dueDateParams(params.due_date),
};
const request: HttpRequest<TodoistUpdateTaskRequest> = {
method: HttpMethod.POST,
url: `${API}/tasks/${params.task_id}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: params.token,
},
body,
};
const response = await httpClient.sendRequest<TodoistTask>(request);
return response.body;
},
async list({
token,
project_id,
filter,
}: TasksListParams): Promise<TodoistTask[]> {
const queryParams = {
filter,
project_id,
};
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${API}/tasks`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token,
},
queryParams: pickBy(queryParams, isNotUndefined),
};
const response = await httpClient.sendRequest<TodoistTask[]>(request);
return response.body;
},
async close({ token, task_id }: { token: string; task_id: string }) {
const request: HttpRequest = {
method: HttpMethod.POST,
url: `${API}/tasks/${task_id}/close`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token,
},
};
const response = await httpClient.sendRequest(request);
return response.body;
},
},
};
type ProjectsListParams = {
token: string;
};
type SectionsListPrams = {
token: string;
project_id?: string;
};
type TasksCreateParams = {
token: string;
} & TodoistCreateTaskRequest;
type TasksUpdateParams = {
token: string;
task_id: string;
} & TodoistUpdateTaskRequest;
type TasksListParams = {
token: string;
project_id?: string | undefined;
filter?: string | undefined;
};
const dueDateParams = (dueDate?: string) => {
if (dueDate) {
const parsedDate = Date.parse(dueDate);
if (isNaN(parsedDate)) {
return { due_string: dueDate };
} else if (/\d{4}-\d{2}-\d{2}/.test(dueDate)) {
return { due_date: dueDate };
} else {
return { due_datetime: new Date(parsedDate).toISOString() };
}
}
return {};
};

View File

@@ -0,0 +1,49 @@
import {
AuthenticationType,
httpClient,
HttpMethod,
HttpRequest,
} from '@activepieces/pieces-common';
import { isNotUndefined, pickBy } from '@activepieces/shared';
import { TodoistCompletedListResponse } from '../models';
const API = 'https://api.todoist.com/sync/v9';
export const todoistSyncClient = {
completed: {
async list({
token,
since,
project_id,
until,
}: CompletedListParams): Promise<TodoistCompletedListResponse> {
const queryParams = {
limit: '200',
since,
until,
project_id,
};
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${API}/completed/get_all`,
queryParams: pickBy(queryParams, isNotUndefined),
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token,
},
};
const response =
await httpClient.sendRequest<TodoistCompletedListResponse>(request);
return response.body;
},
},
};
type CompletedListParams = {
token: string;
since: string;
until: string;
project_id: string | undefined;
};

View File

@@ -0,0 +1,76 @@
export type TodoistProject = {
id: string;
name: string;
};
export type TodoistSection = {
id: string;
name: string;
project_id: string;
order: number;
};
export type TodoistCreateTaskRequest = {
content: string;
project_id?: string | undefined;
description?: string | undefined;
labels?: Array<string> | undefined;
priority?: number | undefined;
due_date?: string | undefined;
due_string?: string | undefined;
due_datetime?: string | undefined;
section_id?: string | undefined;
};
export type TodoistUpdateTaskRequest = {
content?: string;
description?: string;
labels?: Array<string>;
priority?: number;
due_date?: string | undefined;
due_string?: string | undefined;
due_datetime?: string | undefined;
};
type TodoistTaskDue = {
string: string;
date: string;
is_recurring: boolean;
datetime?: string | undefined;
timezone?: string | undefined;
};
export type TodoistTask = {
id: string;
projectId: string | null;
sectionId: string | null;
content: string;
description?: string | undefined;
is_completed: boolean;
labels: string[];
parent_id: string | null;
order: number;
priority: number;
due: TodoistTaskDue | null;
url: string;
comment_count: number;
created_at: string;
creator_id: string;
assignee_id: string | null;
assigner_id: string | null;
};
export type TodoistCompletedTask = {
id: string;
task_id: string;
user_id: string;
project_id: string;
section_id: string;
content: string;
completed_at: string;
note_count: number;
};
export type TodoistCompletedListResponse = {
items: TodoistCompletedTask[];
};

View File

@@ -0,0 +1,80 @@
import { OAuth2PropertyValue, Property } from '@activepieces/pieces-framework';
import { todoistRestClient } from './client/rest-client';
import { todoistAuth } from '../..';
const buildEmptyList = ({ placeholder }: { placeholder: string }) => {
return {
disabled: true,
options: [],
placeholder,
};
};
export const todoistProjectIdDropdown = (description: string) =>
Property.Dropdown<string,false,typeof todoistAuth>({
auth: todoistAuth,
displayName: 'Project',
refreshers: [],
description,
required: false,
options: async ({ auth }) => {
if (!auth) {
return buildEmptyList({
placeholder: 'Please select an authentication',
});
}
const token = (auth as OAuth2PropertyValue).access_token;
const projects = await todoistRestClient.projects.list({ token });
if (projects.length === 0) {
return buildEmptyList({
placeholder: 'No projects found! Please create a project.',
});
}
const options = projects.map((p) => ({
label: p.name,
value: p.id,
}));
return {
disabled: false,
options,
};
},
});
export const todoistSectionIdDropdown = Property.Dropdown({
auth: todoistAuth,
displayName: 'Section',
refreshers: ['project_id'],
required: false,
options: async ({ auth, project_id }) => {
if (!auth) {
return buildEmptyList({
placeholder: 'Please select an authentication',
});
}
const token = (auth as OAuth2PropertyValue).access_token;
const projectId = project_id as string | undefined;
const sections = await todoistRestClient.sections.list({ token, project_id: projectId });
if (sections.length === 0) {
return buildEmptyList({
placeholder: 'No sections found! Please create a section.',
});
}
const options = sections.map((p) => ({
label: p.name,
value: p.id,
}));
return {
disabled: false,
options,
};
},
});

View File

@@ -0,0 +1,119 @@
import {
AppConnectionValueForAuthProperty,
createTrigger,
PiecePropValueSchema,
TriggerStrategy,
} from '@activepieces/pieces-framework';
import dayjs from 'dayjs';
import { TodoistCompletedListResponse, TodoistCompletedTask } from '../common/models';
import { todoistProjectIdDropdown } from '../common/props';
import { todoistAuth } from '../..';
import {
AuthenticationType,
DedupeStrategy,
httpClient,
HttpMethod,
Polling,
pollingHelper,
QueryParams,
} from '@activepieces/pieces-common';
const ISO_FORMAT = 'YYYY-MM-DDTHH:mm:ss';
const polling: Polling<AppConnectionValueForAuthProperty<typeof todoistAuth>, { project_id?: string }> = {
strategy: DedupeStrategy.TIMEBASED,
async items({ auth, propsValue, lastFetchEpochMS }) {
const lastUpdatedTime =
lastFetchEpochMS === 0
? dayjs().subtract(5, 'minutes').format(ISO_FORMAT)
: dayjs(lastFetchEpochMS).format(ISO_FORMAT);
const tasks: TodoistCompletedTask[] = [];
let hasMore = true;
let offset = 0;
const limit = 200;
do {
const qs: QueryParams = {
limit: limit.toString(),
offset: offset.toString(),
since: lastUpdatedTime,
};
if (propsValue.project_id) {
qs.project_id = propsValue.project_id;
}
const response = await httpClient.sendRequest<TodoistCompletedListResponse>({
method: HttpMethod.GET,
url: 'https://api.todoist.com/sync/v9/completed/get_all',
queryParams: qs,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth.access_token,
},
});
if (response.body.items.length > 0) {
tasks.push(...response.body.items);
offset += limit;
} else {
hasMore = false;
}
} while (hasMore);
return tasks.map((task) => {
return {
epochMilliSeconds: dayjs(task.completed_at).valueOf(),
data: task,
};
});
},
};
export const todoistTaskCompletedTrigger = createTrigger({
auth: todoistAuth,
name: 'task_completed',
displayName: 'Task Completed',
description: 'Triggers when a new task is completed',
type: TriggerStrategy.POLLING,
props: {
project_id: todoistProjectIdDropdown(
'Leave it blank if you want to get completed tasks from all your projects.',
),
},
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: {
content: 'Buy Milk',
meta_data: null,
user_id: '2671355',
task_id: '2995104339',
note_count: 0,
project_id: '2203306141',
section_id: '7025',
completed_at: '2015-02-17T15:40:41.000000Z',
id: '1899066186',
},
});