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 {
createTrigger,
TriggerStrategy,
} from '@activepieces/pieces-framework';
import {
DedupeStrategy,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import { CopperActivity, CopperAuth, CopperAuthType } from '../common/constants';
import { CopperApiService } from '../common/requests';
const polling: Polling<
CopperAuthType,
Record<string, never>
> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, lastFetchEpochMS }) => {
const minCreatedUnix =
lastFetchEpochMS != null
? Math.max(0, Math.floor(lastFetchEpochMS / 1000) - 1)
: undefined;
const collected: CopperActivity[] = [];
const pageSize = 100;
let page = 1;
let hasMore = true;
while (hasMore) {
const batch = await CopperApiService.fetchActivities(auth, {
minimum_activity_date: minCreatedUnix,
page_size: pageSize,
page_number: page,
});
if (!batch.length) break;
collected.push(...batch);
if (batch.length < pageSize) hasMore=false;
page += 1;
}
const out = collected.map((a) => ({
epochMilliSeconds: (a.activity_date ?? 0) * 1000,
data: a,
}));
out.sort((a, b) => a.epochMilliSeconds - b.epochMilliSeconds);
return out;
},
};
export const newActivity = createTrigger({
auth: CopperAuth,
name: 'newActivity',
displayName: 'New Activity',
description: 'Triggers when a new activity is logged',
props: {},
sampleData: {},
type: TriggerStrategy.POLLING,
async test(context) {
return await pollingHelper.test(polling, context);
},
async onEnable(context) {
const { store, auth, propsValue } = context;
await pollingHelper.onEnable(polling, { store, auth, propsValue });
},
async onDisable(context) {
const { store, auth, propsValue } = context;
await pollingHelper.onDisable(polling, { store, auth, propsValue });
},
async run(context) {
return await pollingHelper.poll(polling, context);
},
});

View File

@@ -0,0 +1,41 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { CopperAuth } from '../common/constants';
import { CopperApiService } from '../common/requests';
const CACHE_KEY = 'copper_new_lead_trigger_key';
export const newLead = createTrigger({
auth: CopperAuth,
name: 'newLead',
displayName: 'New Lead',
description: 'Triggers when a new lead is created.',
props: {},
sampleData: {},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
const response = await CopperApiService.createWebhook(context.auth, {
target: context.webhookUrl,
type: 'lead',
event: 'new',
});
await context.store.put(CACHE_KEY, {
webhookId: response.id,
});
},
async onDisable(context) {
const cachedWebhookData = (await context.store.get(CACHE_KEY)) as any;
if (cachedWebhookData) {
await CopperApiService.deleteWebhook(
context.auth,
cachedWebhookData.webhookId
).then(async () => {
await context.store.delete(CACHE_KEY);
});
}
},
async run(context) {
return [context.payload.body];
},
});

View File

@@ -0,0 +1,41 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { CopperAuth } from '../common/constants';
import { CopperApiService } from '../common/requests';
const CACHE_KEY = 'copper_new_person_trigger_key';
export const newPerson = createTrigger({
auth: CopperAuth,
name: 'newPerson',
displayName: 'New Person',
description: 'Triggers when a new person/contact is created.',
props: {},
sampleData: {},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
const response = await CopperApiService.createWebhook(context.auth, {
target: context.webhookUrl,
type: 'person',
event: 'new',
});
await context.store.put(CACHE_KEY, {
webhookId: response.id,
});
},
async onDisable(context) {
const cachedWebhookData = (await context.store.get(CACHE_KEY)) as any;
if (cachedWebhookData) {
await CopperApiService.deleteWebhook(
context.auth,
cachedWebhookData.webhookId
).then(async () => {
await context.store.delete(CACHE_KEY);
});
}
},
async run(context) {
return [context.payload.body];
},
});

View File

@@ -0,0 +1,41 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { CopperAuth } from '../common/constants';
import { CopperApiService } from '../common/requests';
const CACHE_KEY = 'copper_new_task_trigger_key';
export const newTask = createTrigger({
auth: CopperAuth,
name: 'newTask',
displayName: 'New Task',
description: 'Triggers when a new task is created.',
props: {},
sampleData: {},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
const response = await CopperApiService.createWebhook(context.auth, {
target: context.webhookUrl,
type: 'task',
event: 'new',
});
await context.store.put(CACHE_KEY, {
webhookId: response.id,
});
},
async onDisable(context) {
const cachedWebhookData = (await context.store.get(CACHE_KEY)) as any;
if (cachedWebhookData) {
await CopperApiService.deleteWebhook(
context.auth,
cachedWebhookData.webhookId
).then(async () => {
await context.store.delete(CACHE_KEY);
});
}
},
async run(context) {
return [context.payload.body];
},
});

View File

@@ -0,0 +1,64 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { CopperAuth } from '../common/constants';
import { CopperApiService } from '../common/requests';
const CACHE_KEY = 'copper_updated_lead_status_trigger_key';
export const updatedLeadStatus = createTrigger({
auth: CopperAuth,
name: 'updatedLeadStatus',
displayName: 'Updated Lead Status',
description: 'Triggers when the status of a lead changes.',
props: {},
sampleData: {},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
const response = await CopperApiService.createWebhook(context.auth, {
target: context.webhookUrl,
type: 'lead',
event: 'update',
});
await context.store.put(CACHE_KEY, {
webhookId: response.id,
});
},
async onDisable(context) {
const cachedWebhookData = (await context.store.get(CACHE_KEY)) as any;
if (cachedWebhookData) {
await CopperApiService.deleteWebhook(
context.auth,
cachedWebhookData.webhookId
).then(async () => {
await context.store.delete(CACHE_KEY);
});
}
},
async run(context) {
const body = context.payload.body as any;
const ids = Array.isArray(body?.ids) ? body.ids : [];
const updatedAttrs = body?.updated_attributes ?? {};
const statusChanged =
Array.isArray(updatedAttrs.status) &&
updatedAttrs.status.length === 2 &&
updatedAttrs.status[0] !== updatedAttrs.status[1];
if (!statusChanged) {
return [];
}
const events = ids.map((id: number | string) => ({
id,
change_type: 'status_change',
previous_status: updatedAttrs.status?.[0] ?? null,
current_status: updatedAttrs.status?.[1] ?? null,
subscription_id: body.subscription_id,
timestamp: body.timestamp,
}));
return events;
},
})

View File

@@ -0,0 +1,41 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { CopperApiService } from '../common/requests';
import { CopperAuth } from '../common/constants';
const CACHE_KEY = 'copper_updated_lead_trigger_key';
export const updatedLead = createTrigger({
auth: CopperAuth,
name: 'updatedLead',
displayName: 'Updated Lead',
description: 'Triggers when a lead is modified.',
props: {},
sampleData: {},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
const response = await CopperApiService.createWebhook(context.auth, {
target: context.webhookUrl,
type: 'lead',
event: 'update',
});
await context.store.put(CACHE_KEY, {
webhookId: response.id,
});
},
async onDisable(context) {
const cachedWebhookData = (await context.store.get(CACHE_KEY)) as any;
if (cachedWebhookData) {
await CopperApiService.deleteWebhook(
context.auth,
cachedWebhookData.webhookId
).then(async () => {
await context.store.delete(CACHE_KEY);
});
}
},
async run(context) {
return [context.payload.body];
},
});

View File

@@ -0,0 +1,72 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { CopperApiService } from '../common/requests';
import { CopperAuth } from '../common/constants';
const CACHE_KEY = 'copper_updated_opportunity_stage_trigger_key';
export const updatedOpportunityStage = createTrigger({
auth: CopperAuth,
name: 'updatedOpportunityStage',
displayName: 'Updated Opportunity Stage',
description: 'Triggers when an opportunity stage changes',
props: {},
sampleData: {},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
const response = await CopperApiService.createWebhook(context.auth, {
target: context.webhookUrl,
type: 'opportunity',
event: 'update',
});
await context.store.put(CACHE_KEY, {
webhookId: response.id,
});
},
async onDisable(context) {
const cachedWebhookData = (await context.store.get(CACHE_KEY)) as any;
if (cachedWebhookData) {
await CopperApiService.deleteWebhook(
context.auth,
cachedWebhookData.webhookId
).then(async () => {
await context.store.delete(CACHE_KEY);
});
}
},
async run(context) {
const body = context.payload.body as any;
const ids = Array.isArray(body.ids) ? body.ids : [];
const updatedAttrs = body.updated_attributes;
const idChanged =
Array.isArray(updatedAttrs.stage_id) &&
updatedAttrs.stage_id[0] !== updatedAttrs.stage_id[1];
const labelChanged =
Array.isArray(updatedAttrs.stage) &&
updatedAttrs.stage[0] !== updatedAttrs.stage[1];
const isStageMove = idChanged && labelChanged;
if (!isStageMove) {
return [];
}
const events = ids.map((id: any) => ({
id,
change_type: 'stage_change',
previous_stage_id: updatedAttrs.stage_id?.[0] ?? null,
current_stage_id: updatedAttrs.stage_id?.[1] ?? null,
previous_stage_label: updatedAttrs.stage?.[0] ?? null,
current_stage_label: updatedAttrs.stage?.[1] ?? null,
previous_last_stage_at: updatedAttrs.last_stage_at?.[0] ?? null,
current_last_stage_at: updatedAttrs.last_stage_at?.[1] ?? null,
previous_days_in_stage: updatedAttrs.days_in_stage?.[0] ?? null,
current_days_in_stage: updatedAttrs.days_in_stage?.[1] ?? null,
subscription_id: body.subscription_id,
timestamp: body.timestamp,
}));
return events;
},
});

View File

@@ -0,0 +1,63 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { CopperApiService } from '../common/requests';
import { CopperAuth } from '../common/constants';
const CACHE_KEY = 'copper_updated_opportunity_status_trigger_key';
export const updatedOpportunityStatus = createTrigger({
auth: CopperAuth,
name: 'updatedOpportunityStatus',
displayName: 'Updated Opportunity Status',
description: "Triggers when an opportunity's status changes.",
props: {},
sampleData: {},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
const response = await CopperApiService.createWebhook(context.auth, {
target: context.webhookUrl,
type: 'opportunity',
event: 'update',
});
await context.store.put(CACHE_KEY, {
webhookId: response.id,
});
},
async onDisable(context) {
const cachedWebhookData = (await context.store.get(CACHE_KEY)) as any;
if (cachedWebhookData) {
await CopperApiService.deleteWebhook(
context.auth,
cachedWebhookData.webhookId
).then(async () => {
await context.store.delete(CACHE_KEY);
});
}
},
async run(context) {
const body = context.payload.body as any;
const ids = Array.isArray(body?.ids) ? body.ids : [];
const updatedAttrs = body?.updated_attributes ?? {};
const statusChanged =
Array.isArray(updatedAttrs.status) &&
updatedAttrs.status.length === 2 &&
updatedAttrs.status[0] !== updatedAttrs.status[1];
if (!statusChanged) {
return [];
}
const events = ids.map((id: number | string) => ({
id,
change_type: 'status_change',
previous_status: updatedAttrs.status?.[0] ?? null,
current_status: updatedAttrs.status?.[1] ?? null,
subscription_id: body.subscription_id,
timestamp: body.timestamp,
}));
return events;
},
});

View File

@@ -0,0 +1,41 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { CopperAuth } from '../common/constants';
import { CopperApiService } from '../common/requests';
const CACHE_KEY = 'copper_updated_opportunity_trigger_key';
export const updatedOpportunity = createTrigger({
auth: CopperAuth,
name: 'updatedOpportunity',
displayName: 'Updated Opportunity',
description: 'Triggers when an opportunity changes.',
props: {},
sampleData: {},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
const response = await CopperApiService.createWebhook(context.auth, {
target: context.webhookUrl,
type: 'opportunity',
event: 'update',
});
await context.store.put(CACHE_KEY, {
webhookId: response.id,
});
},
async onDisable(context) {
const cachedWebhookData = (await context.store.get(CACHE_KEY)) as any;
if (cachedWebhookData) {
await CopperApiService.deleteWebhook(
context.auth,
cachedWebhookData.webhookId
).then(async () => {
await context.store.delete(CACHE_KEY);
});
}
},
async run(context) {
return [context.payload.body];
},
});

View File

@@ -0,0 +1,41 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { CopperAuth } from '../common/constants';
import { CopperApiService } from '../common/requests';
const CACHE_KEY = 'copper_updated_project_trigger_key';
export const updatedProject = createTrigger({
auth: CopperAuth,
name: 'updatedProject',
displayName: 'Updated Project',
description: 'Triggers when a project is updated.',
props: {},
sampleData: {},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
const response = await CopperApiService.createWebhook(context.auth, {
target: context.webhookUrl,
type: 'project',
event: 'update',
});
await context.store.put(CACHE_KEY, {
webhookId: response.id,
});
},
async onDisable(context) {
const cachedWebhookData = (await context.store.get(CACHE_KEY)) as any;
if (cachedWebhookData) {
await CopperApiService.deleteWebhook(
context.auth,
cachedWebhookData.webhookId
).then(async () => {
await context.store.delete(CACHE_KEY);
});
}
},
async run(context) {
return [context.payload.body];
},
});

View File

@@ -0,0 +1,41 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { CopperApiService } from '../common/requests';
import { CopperAuth } from '../common/constants';
const CACHE_KEY = 'copper_updated_task_trigger_key';
export const updatedTask = createTrigger({
auth: CopperAuth,
name: 'updatedTask',
displayName: 'Updated Task',
description: 'Triggers when a task is updated.',
props: {},
sampleData: {},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
const response = await CopperApiService.createWebhook(context.auth, {
target: context.webhookUrl,
type: 'task',
event: 'update',
});
await context.store.put(CACHE_KEY, {
webhookId: response.id,
});
},
async onDisable(context) {
const cachedWebhookData = (await context.store.get(CACHE_KEY)) as any;
if (cachedWebhookData) {
await CopperApiService.deleteWebhook(
context.auth,
cachedWebhookData.webhookId
).then(async () => {
await context.store.delete(CACHE_KEY);
});
}
},
async run(context) {
return [context.payload.body];
},
});