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,33 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { assertNotNullOrUndefined } from '@activepieces/shared';
import { figmaCommon } from '../common';
import { figmaGetRequest } from '../common/utils';
import { figmaAuth } from '../../';
export const getCommentsAction = createAction({
auth: figmaAuth,
name: 'get_comments',
displayName: 'Get File Comments',
description: 'Get file comments',
props: {
file_key: Property.ShortText({
displayName: 'File Key',
description: 'The Figma file key (copy from Figma file URL)',
required: true,
}),
},
async run(context) {
const token = context.auth.access_token;
const fileKey = context.propsValue.file_key;
assertNotNullOrUndefined(token, 'token');
assertNotNullOrUndefined(fileKey, 'file_key');
const url = `${figmaCommon.baseUrl}/${figmaCommon.comments}`.replace(
':file_key',
fileKey
);
return figmaGetRequest({ token, url });
},
});

View File

@@ -0,0 +1,33 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { assertNotNullOrUndefined } from '@activepieces/shared';
import { figmaCommon } from '../common';
import { figmaGetRequest } from '../common/utils';
import { figmaAuth } from '../../';
export const getFileAction = createAction({
auth: figmaAuth,
name: 'get_file',
displayName: 'Get File',
description: 'Get file',
props: {
file_key: Property.ShortText({
displayName: 'File Key',
description: 'The Figma file key (copy from Figma file URL)',
required: true,
}),
},
async run(context) {
const token = context.auth.access_token;
const fileKey = context.propsValue.file_key;
assertNotNullOrUndefined(token, 'token');
assertNotNullOrUndefined(fileKey, 'file_key');
const url = `${figmaCommon.baseUrl}/${figmaCommon.files}`.replace(
':file_key',
fileKey
);
return figmaGetRequest({ token, url });
},
});

View File

@@ -0,0 +1,39 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { assertNotNullOrUndefined } from '@activepieces/shared';
import { figmaCommon } from '../common';
import { figmaPostRequestWithMessage } from '../common/utils';
import { figmaAuth } from '../../';
export const postCommentAction = createAction({
auth: figmaAuth,
name: 'post_comment',
displayName: 'Post File Comment',
description: 'Post file comment',
props: {
file_key: Property.ShortText({
displayName: 'File Key',
description: 'The Figma file key (copy from Figma file URL)',
required: true,
}),
message: Property.LongText({
displayName: 'Comment',
description: 'Your comment',
required: true,
}),
},
async run(context) {
const token = context.auth.access_token;
const { file_key, message } = context.propsValue;
assertNotNullOrUndefined(token, 'token');
assertNotNullOrUndefined(file_key, 'file_key');
assertNotNullOrUndefined(message, 'comment');
const url = `${figmaCommon.baseUrl}/${figmaCommon.comments}`.replace(
':file_key',
file_key
);
return figmaPostRequestWithMessage({ token, url, message });
},
});

View File

@@ -0,0 +1,7 @@
export const figmaCommon = {
baseUrl: 'https://api.figma.com',
files: 'v1/files/:file_key',
comments: 'v1/files/:file_key/comments',
webhooks: 'v2/webhooks',
webhook: 'v2/webhooks/:webhook_id',
};

View File

@@ -0,0 +1,24 @@
type User = {
handle: string;
img_url: string;
id: string;
};
type Pos = {
x: number;
y: number;
};
export type Comment = {
id: string;
uuid: string;
file_key: string;
parent_id: string;
user: User;
created_at: string;
resolved_at: string;
message: string;
reactions: unknown;
client_meta: Pos;
order_id: number;
};

View File

@@ -0,0 +1,139 @@
import {
httpClient,
HttpMethod,
HttpRequest,
AuthenticationType,
} from '@activepieces/pieces-common';
export const figmaGetRequest = async ({
token,
url,
}: FigmaGetRequestParams) => {
const request: HttpRequest = {
method: HttpMethod.GET,
url: url,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token,
},
};
const response = await httpClient.sendRequest(request);
return {
success: true,
request_body: request.body,
response_body: response.body,
};
};
export const figmaPostRequestWithMessage = async ({
token,
url,
message,
}: FigmaPostRequestWithMessageParams) => {
const request: HttpRequest<FigmaPostRequestWithMessageBody> = {
method: HttpMethod.POST,
url: url,
body: {
message: message,
},
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token,
},
};
const response = await httpClient.sendRequest(request);
return {
success: true,
request_body: request.body,
response_body: response.body,
};
};
export const figmaWebhookPostRequest = async ({
token,
url,
eventType,
teamId,
endpoint,
passcode,
}: FigmaWebhookPostRequestParams) => {
const request: HttpRequest<FigmaWebhookPostRequestBody> = {
method: HttpMethod.POST,
url: url,
body: {
event_type: eventType,
team_id: teamId,
endpoint: endpoint,
passcode: passcode,
},
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token,
},
};
const response = await httpClient.sendRequest(request);
return {
success: true,
request_body: request.body,
response_body: response.body,
};
};
export const figmaDeleteRequest = async ({
token,
url,
}: FigmaGetRequestParams) => {
const request: HttpRequest = {
method: HttpMethod.DELETE,
url: url,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token,
},
};
const response = await httpClient.sendRequest(request);
return {
success: true,
request_body: request.body,
response_body: response.body,
};
};
type FigmaGetRequestParams = {
token: string;
url: string;
};
type FigmaPostRequestWithMessageParams = {
token: string;
url: string;
message: string;
};
type FigmaPostRequestWithMessageBody = {
message: string;
};
type FigmaWebhookPostRequestParams = {
token: string;
url: string;
eventType: string;
teamId: string;
endpoint: string;
passcode: string;
};
type FigmaWebhookPostRequestBody = {
event_type: string;
team_id: string;
endpoint: string;
passcode: string;
};

View File

@@ -0,0 +1,96 @@
import {
createTrigger,
Property,
TriggerStrategy,
} from '@activepieces/pieces-framework';
import { assertNotNullOrUndefined } from '@activepieces/shared';
import { nanoid } from 'nanoid';
import { figmaCommon } from '../common';
import { figmaWebhookPostRequest, figmaDeleteRequest } from '../common/utils';
import { figmaAuth } from '../../';
type TriggerData = {
webhookId: string;
};
const TRIGGER_DATA_STORE_KEY = 'figma_new_comment_trigger_data';
export const newCommentTrigger = createTrigger({
auth: figmaAuth,
name: 'new_comment',
displayName: 'New Comment (Figma Professional plan only)',
description: 'Triggers when a new comment is posted',
type: TriggerStrategy.WEBHOOK,
sampleData: [
{
id: '12345',
team_id: '1234567890',
event_type: 'FILE_COMMENT',
client_id: null,
endpoint: 'http://localhost:1234/webhook',
passcode: 'figma-passcode',
status: 'ACTIVE',
description: null,
protocol_version: '2',
},
],
props: {
team_id: Property.ShortText({
displayName: 'Team ID',
description:
'Naviate to team page, copy the Id from the URL after the word team/',
required: true,
}),
},
async onEnable(context): Promise<void> {
const token = context.auth.access_token;
const teamId = context.propsValue['team_id'];
assertNotNullOrUndefined(token, 'token');
assertNotNullOrUndefined(teamId, 'teamId');
const url = `${figmaCommon.baseUrl}/${figmaCommon.webhooks}`;
const eventType = 'FILE_COMMENT';
const passcode = `figma_passcode_${nanoid()}`;
const endpoint = context.webhookUrl;
const { response_body } = await figmaWebhookPostRequest({
token,
url,
eventType,
teamId,
endpoint,
passcode,
});
await context.store?.put<TriggerData>(TRIGGER_DATA_STORE_KEY, {
webhookId: response_body['id'],
});
},
async onDisable(context): Promise<void> {
const token = context.auth.access_token;
assertNotNullOrUndefined(token, 'token');
const triggerData = await context.store?.get<TriggerData>(
TRIGGER_DATA_STORE_KEY
);
if (triggerData !== null && triggerData !== undefined) {
const url = `${figmaCommon.baseUrl}/${figmaCommon.webhook}`.replace(
':webhook_id',
triggerData.webhookId
);
await figmaDeleteRequest({ token, url });
}
},
async run(context) {
const payloadBody = context.payload.body as Record<string, unknown>;
if ('event_type' in payloadBody && payloadBody['event_type'] === 'PING') {
return [];
}
return [payloadBody];
},
});