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,60 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { CopperAuth } from '../common/constants';
import {
companyDropdown,
leadDropdown,
opportunityDropdown,
} from '../common/props';
import { CopperApiService } from '../common/requests';
export const convertLead = createAction({
auth: CopperAuth,
name: 'convertLead',
displayName: 'Convert Lead',
description:
'Converts a lead into a person (optionally with company/opportunity).',
props: {
leadId: leadDropdown(['auth']),
companyId: companyDropdown({ refreshers: ['auth'] }),
opportunityId: opportunityDropdown({ refreshers: ['auth'] }),
},
async run(context) {
const { leadId, companyId, opportunityId } = context.propsValue;
const opportunity = opportunityId
? JSON.parse(opportunityId as string)
: null;
const company = companyId ? JSON.parse(companyId as string) : null;
const lead = JSON.parse(leadId as string);
const payload = {
person: {
name: lead.name,
},
company: {
...(company
? {
id: company.id,
}
: {
name: '',
}),
},
...(opportunity
? {
opportunity: {
name: opportunity.name,
pipeline_id: opportunity.pipeline_id,
pipeline_stage_id: opportunity.pipeline_stage_id,
monetary_value: opportunity.monetary_value || undefined,
assignee_id: opportunity.assignee_id,
},
}
: {}),
};
return await CopperApiService.convertLead(context.auth, lead.id, {
details: payload,
});
},
});

View File

@@ -0,0 +1,118 @@
import {
createAction,
InputPropertyMap,
Property,
} from '@activepieces/pieces-framework';
import { CopperAuth, CopperAuthType } from '../common/constants';
import { CopperApiService } from '../common/requests';
import { ActivityTypesDropdown } from '../common/props';
export const createActivity = createAction({
auth: CopperAuth,
name: 'createActivity',
displayName: 'Create Activity',
description: 'Logs an activity related to CRM entities.',
props: {
entity: Property.StaticDropdown({
displayName: 'Parent Entity',
description: 'Select parent entity',
required: true,
options: {
options: [
{ label: 'Person', value: 'person' },
{ label: 'Company', value: 'company' },
{ label: 'Lead', value: 'lead' },
{ label: 'Opportunity', value: 'opportunity' },
{ label: 'Project', value: 'project' },
{ label: 'Task', value: 'task' },
],
},
}),
entityItemId: Property.Dropdown({
auth: CopperAuth,
displayName: 'Parent Entity Resource',
description: 'Select Resource',
required: true,
refreshers: ['auth', 'entity'],
async options(propsValue: Record<string, unknown>) {
const auth = propsValue['auth'] as CopperAuthType | undefined;
const entity = propsValue['entity'] as
| 'person'
| 'company'
| 'lead'
| 'opportunity'
| 'project'
| 'task';
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
if (!entity) {
return {
disabled: true,
placeholder: 'Select a Parent Entity first',
options: [],
};
}
const fetchFnMap = {
person: CopperApiService.fetchPeople,
company: CopperApiService.fetchCompanies,
lead: CopperApiService.fetchLeads,
opportunity: CopperApiService.fetchOpportunities,
task: CopperApiService.fetchTasks,
project: CopperApiService.fetchProjects,
};
const fetchFn = fetchFnMap[entity];
try {
const items = await fetchFn(auth);
return {
options: items.map((item: any) => ({
label: item.name,
value: item.id,
})),
};
} catch (e) {
console.error('Failed to fetch entity items', e);
return {
options: [],
placeholder: 'Unable to load entity items',
};
}
},
}),
details: Property.ShortText({
displayName: 'Details',
description: 'The details of the project',
required: false,
}),
type: ActivityTypesDropdown("user"),
},
async run(context) {
const { type, entity, entityItemId, details } = context.propsValue;
const activityType = JSON.parse(type as string);
const payload = {
parent: {
type: entity,
id: entityItemId,
},
type: {
category: activityType.category,
id: activityType.id,
},
details,
};
return await CopperApiService.createActivity(context.auth, payload);
},
});

View File

@@ -0,0 +1,114 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { CopperAuth, isNonEmptyStr } from '../common/constants';
import { primaryContactsDropdown } from '../common/props';
import { CopperApiService } from '../common/requests';
export const createCompany = createAction({
auth: CopperAuth,
name: 'createCompany',
displayName: 'Create Company',
description: 'Adds a new company.',
props: {
name: Property.ShortText({ displayName: 'Full Name', required: true }),
email_domain: Property.ShortText({
displayName: 'Email Domain',
description: 'E.g. democompany.com',
required: false,
}),
details: Property.ShortText({
displayName: 'Details',
required: false,
}),
phone_numbers: Property.Array({
displayName: 'Phone Numbers',
required: false,
properties: {
number: Property.ShortText({
displayName: 'Number',
required: true,
}),
category: Property.ShortText({
displayName: 'Category (e.g., mobile, work, personal)',
required: true,
}),
},
defaultValue: [],
}),
address_street: Property.ShortText({
displayName: 'Street',
required: false,
}),
address_city: Property.ShortText({ displayName: 'City', required: false }),
address_state: Property.ShortText({
displayName: 'State/Region',
required: false,
}),
address_postal_code: Property.ShortText({
displayName: 'Postal Code',
required: false,
}),
address_country: Property.ShortText({
displayName: 'Country',
required: false,
}),
primaryContactId: primaryContactsDropdown({ refreshers: ['auth'] }),
},
async run(context) {
const {
name,
email_domain,
details,
address_street,
address_city,
address_state,
address_postal_code,
address_country,
phone_numbers,
primaryContactId,
} = context.propsValue as any;
const normPhones = (Array.isArray(phone_numbers) ? phone_numbers : [])
.map((p) => ({
number: String(p?.number ?? '').trim(),
category: isNonEmptyStr(p?.category)
? String(p.category).trim()
: undefined,
}))
.filter((p) => isNonEmptyStr(p.number));
const address = [
address_street,
address_city,
address_state,
address_postal_code,
address_country,
].some(isNonEmptyStr)
? {
street: isNonEmptyStr(address_street)
? address_street.trim()
: undefined,
city: isNonEmptyStr(address_city) ? address_city.trim() : undefined,
state: isNonEmptyStr(address_state)
? address_state.trim()
: undefined,
postal_code: isNonEmptyStr(address_postal_code)
? address_postal_code.trim()
: undefined,
country: isNonEmptyStr(address_country)
? address_country.trim()
: undefined,
}
: undefined;
const body: any = {
name,
email_domain,
details,
...(normPhones.length ? { phone_numbers: normPhones } : {}),
...(address ? { address } : {}),
primary_contact_id: primaryContactId,
};
return await CopperApiService.createCompany(context.auth, body);
},
});

View File

@@ -0,0 +1,112 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { CopperAuth, isNonEmptyStr } from '../common/constants';
import { CopperApiService } from '../common/requests';
export const createLead = createAction({
auth: CopperAuth,
name: 'createLead',
displayName: 'Create Lead',
description: 'Adds a new lead.',
props: {
name: Property.ShortText({ displayName: 'Full Name', required: true }),
email: Property.ShortText({
displayName: 'Email',
required: true,
}),
category: Property.ShortText({
displayName: 'Category of the email address (e.g., work, personal)',
required: true,
}),
phone_numbers: Property.Array({
displayName: 'Phone Numbers',
required: false,
properties: {
number: Property.ShortText({
displayName: 'Number',
required: true,
}),
category: Property.ShortText({
displayName: 'Category (e.g., mobile, work, personal)',
required: true,
}),
},
defaultValue: [],
}),
address_street: Property.ShortText({
displayName: 'Street',
required: false,
}),
address_city: Property.ShortText({ displayName: 'City', required: false }),
address_state: Property.ShortText({
displayName: 'State/Region',
required: false,
}),
address_postal_code: Property.ShortText({
displayName: 'Postal Code',
required: false,
}),
address_country: Property.ShortText({
displayName: 'Country',
required: false,
}),
},
async run(context) {
const {
name,
email,
category,
address_street,
address_city,
address_state,
address_postal_code,
address_country,
phone_numbers,
} = context.propsValue as any;
const normPhones = (Array.isArray(phone_numbers) ? phone_numbers : [])
.map((p) => ({
number: String(p?.number ?? '').trim(),
category: isNonEmptyStr(p?.category)
? String(p.category).trim()
: undefined,
}))
.filter((p) => isNonEmptyStr(p.number));
const address = [
address_street,
address_city,
address_state,
address_postal_code,
address_country,
].some(isNonEmptyStr)
? {
street: isNonEmptyStr(address_street)
? address_street.trim()
: undefined,
city: isNonEmptyStr(address_city) ? address_city.trim() : undefined,
state: isNonEmptyStr(address_state)
? address_state.trim()
: undefined,
postal_code: isNonEmptyStr(address_postal_code)
? address_postal_code.trim()
: undefined,
country: isNonEmptyStr(address_country)
? address_country.trim()
: undefined,
}
: undefined;
const body: any = {
name,
email: {
email,
category
},
...(normPhones.length ? { phone_numbers: normPhones } : {}),
...(address ? { address } : {}),
};
return await CopperApiService.createLead(context.auth, body);
},
});

View File

@@ -0,0 +1,68 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { CopperAuth } from '../common/constants';
import { pipelinesDropdown, primaryContactsDropdown } from '../common/props';
import { CopperApiService } from '../common/requests';
export const createOpportunity = createAction({
auth: CopperAuth,
name: 'createOpportunity',
displayName: 'Create Opportunity',
description: 'Adds a new opportunity.',
props: {
name: Property.ShortText({
displayName: 'Name',
description: 'The name of the opportunity',
required: true,
}),
pipelineId: pipelinesDropdown({ refreshers: ['auth'] }),
pipelineStageId: Property.Dropdown({
auth: CopperAuth,
displayName: 'Pipeline Stage',
description: 'Select a stage',
refreshers: ['auth', 'pipelineId'],
required: false,
async options({ auth, pipelineId }: any) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
if (!pipelineId) {
return {
disabled: true,
placeholder: 'Select a pipeline first',
options: [],
};
}
const pipeline = JSON.parse(pipelineId);
const stages = pipeline.stages;
return {
options: stages.map((stage: any) => ({
label: stage.name,
value: stage.id,
})),
};
},
}),
primaryContactId: primaryContactsDropdown({ refreshers: ['auth'] }),
},
async run(context) {
const { name, primaryContactId, pipelineId, pipelineStageId } =
context.propsValue;
const pipeline = JSON.parse(pipelineId as string);
return await CopperApiService.createOpportunity(context.auth, {
name,
primary_contact_id: primaryContactId,
pipeline_id: pipeline.id,
pipeline_stage_id: pipelineStageId,
});
},
});

View File

@@ -0,0 +1,133 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { CopperAuth, isNonEmptyStr } from '../common/constants';
import { CopperApiService } from '../common/requests';
export const createPerson = createAction({
auth: CopperAuth,
name: 'createPerson',
displayName: 'Create Person',
description: 'Adds a new person/contact.',
props: {
name: Property.ShortText({ displayName: 'Full Name', required: true }),
emails: Property.Array({
displayName: 'Emails',
required: true,
properties: {
email: Property.ShortText({
displayName: 'Email',
required: true,
}),
category: Property.ShortText({
displayName: 'Category (e.g., work, personal)',
required: true,
}),
},
defaultValue: [],
}),
phone_numbers: Property.Array({
displayName: 'Phone Numbers',
required: false,
properties: {
number: Property.ShortText({
displayName: 'Number',
required: true,
}),
category: Property.ShortText({
displayName: 'Category (e.g., mobile, work, personal)',
required: true,
}),
},
defaultValue: [],
}),
address_street: Property.ShortText({
displayName: 'Street',
required: false,
}),
address_city: Property.ShortText({ displayName: 'City', required: false }),
address_state: Property.ShortText({
displayName: 'State/Region',
required: false,
}),
address_postal_code: Property.ShortText({
displayName: 'Postal Code',
required: false,
}),
address_country: Property.ShortText({
displayName: 'Country',
required: false,
}),
},
async run(context) {
const {
name,
emails,
address_street,
address_city,
address_state,
address_postal_code,
address_country,
phone_numbers,
} = context.propsValue as any;
const normEmails = (Array.isArray(emails) ? emails : [])
.map((row) => ({
email: String(row?.email ?? '')
.trim()
.toLowerCase(),
category: isNonEmptyStr(row?.category)
? String(row.category).trim()
: undefined,
}))
.filter((e) => isNonEmptyStr(e.email));
if (normEmails.length === 0)
throw new Error('Please provide at least one valid email.');
const seen = new Set<string>();
const dedupedEmails = normEmails.filter(
(e) => !seen.has(e.email) && (seen.add(e.email), true)
);
const normPhones = (Array.isArray(phone_numbers) ? phone_numbers : [])
.map((p) => ({
number: String(p?.number ?? '').trim(),
category: isNonEmptyStr(p?.category)
? String(p.category).trim()
: undefined,
}))
.filter((p) => isNonEmptyStr(p.number));
const address = [
address_street,
address_city,
address_state,
address_postal_code,
address_country,
].some(isNonEmptyStr)
? {
street: isNonEmptyStr(address_street)
? address_street.trim()
: undefined,
city: isNonEmptyStr(address_city) ? address_city.trim() : undefined,
state: isNonEmptyStr(address_state)
? address_state.trim()
: undefined,
postal_code: isNonEmptyStr(address_postal_code)
? address_postal_code.trim()
: undefined,
country: isNonEmptyStr(address_country)
? address_country.trim()
: undefined,
}
: undefined;
const body: any = {
name,
emails: dedupedEmails,
...(normPhones.length ? { phone_numbers: normPhones } : {}),
...(address ? { address } : {}),
};
return await CopperApiService.createPerson(context.auth, body);
},
});

View File

@@ -0,0 +1,30 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { CopperAuth } from '../common/constants';
import { CopperApiService } from '../common/requests';
export const createProject = createAction({
auth: CopperAuth,
name: 'createProject',
displayName: 'Create Project',
description: 'Adds a new project.',
props: {
name: Property.ShortText({
displayName: 'Name',
description: 'The name of the project',
required: true,
}),
details: Property.ShortText({
displayName: 'Details',
description: 'The details of the project',
required: false,
}),
},
async run(context) {
const { name, details } = context.propsValue;
return await CopperApiService.createProject(context.auth, {
name,
details
})
},
});

View File

@@ -0,0 +1,168 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { CopperAuth, CopperAuthType, toUnix } from '../common/constants';
import { CopperApiService } from '../common/requests';
import { ActivityTypesDropdown, usersDropdown } from '../common/props';
export const createTask = createAction({
auth: CopperAuth,
name: 'createTask',
displayName: 'Create Task',
description: 'Adds a new task under a person, lead, or opportunity.',
props: {
name: Property.ShortText({ displayName: 'Task Name', required: true }),
details: Property.ShortText({
displayName: 'Details',
description: 'Details fo this task',
required: false,
}),
custom_activity_type_id: ActivityTypesDropdown('user'),
assigneeId: usersDropdown({ refreshers: ['auth'] }),
entity: Property.StaticDropdown({
displayName: 'Related Record Type',
description:
'Choose the type of Copper record this task should be linked to (e.g. Person, Company, Lead, Opportunity, or Project).',
required: false,
options: {
options: [
{ label: 'Person', value: 'person' },
{ label: 'Company', value: 'company' },
{ label: 'Lead', value: 'lead' },
{ label: 'Opportunity', value: 'opportunity' },
{ label: 'Project', value: 'project' },
],
},
}),
entityItemId: Property.Dropdown({
auth: CopperAuth,
displayName: 'Related Record',
description:
'Select the specific record (from the chosen type above) that this task should be attached to. For example, pick the Person or Opportunity the task relates to.',
required: false,
refreshers: ['auth', 'entity'],
async options(propsValue: Record<string, unknown>) {
const auth = propsValue['auth'] as CopperAuthType | undefined;
const entity = propsValue['entity'] as
| 'person'
| 'company'
| 'lead'
| 'opportunity'
| 'project';
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
if (!entity) {
return {
disabled: true,
placeholder: 'Select a Parent Entity first',
options: [],
};
}
const fetchFnMap = {
person: CopperApiService.fetchPeople,
company: CopperApiService.fetchCompanies,
lead: CopperApiService.fetchLeads,
opportunity: CopperApiService.fetchOpportunities,
project: CopperApiService.fetchProjects,
} as const;
const fetchFn = fetchFnMap[entity];
try {
const items = await fetchFn(auth);
return {
options: items.map((item: any) => ({
label: item.name ?? item.title ?? `#${item.id}`,
value: item.id,
})),
};
} catch (e) {
console.error('Failed to fetch entity items', e);
return {
options: [],
placeholder: 'Unable to load entity items',
};
}
},
}),
due_date: Property.DateTime({
displayName: 'Due Date/Time',
required: false,
description:
'Enter date and time in 24-hour format, e.g. `2025-09-09 11:40` (11:40 AM) or `2025-09-09 13:00` (1:00 PM).',
}),
reminder_date: Property.DateTime({
displayName: 'Reminder Date/Time',
required: false,
description:
'Enter date and time in 24-hour format, e.g. `2025-09-09 11:40` (11:40 AM) or `2025-09-09 13:00` (1:00 PM)',
}),
priority: Property.StaticDropdown({
displayName: 'Priority',
required: false,
options: {
disabled: false,
options: [
{ label: 'None', value: 'None' },
{ label: 'Low', value: 'Low' },
{ label: 'Medium', value: 'Medium' },
{ label: 'High', value: 'High' },
],
},
}),
tags: Property.Array({
displayName: 'Tags',
required: false,
properties: {
tag: Property.ShortText({ displayName: 'Tag', required: true }),
},
defaultValue: [],
}),
},
async run(context) {
const {
name,
details,
custom_activity_type_id,
assigneeId,
priority,
tags,
due_date,
reminder_date,
entity,
entityItemId,
} = context.propsValue;
const custom_activity_type = JSON.parse(custom_activity_type_id as string);
const tagList: string[] = Array.isArray(tags)
? tags.map((t: any) => String(t?.tag ?? '').trim()).filter(Boolean)
: [];
const payload = {
name,
details,
custom_activity_type_id: custom_activity_type.id,
...(assigneeId ? {assignee_id: assigneeId}: {}),
due_date: toUnix(due_date),
reminder_date: toUnix(reminder_date),
priority,
tags: tagList,
...(entity &&
entityItemId && {
related_resource: {
type: entity,
id: entityItemId,
},
}),
};
return await CopperApiService.createTask(context.auth, payload);
},
});

View File

@@ -0,0 +1,285 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { CopperAuth, toUnix } from '../common/constants';
import { CopperApiService } from '../common/requests';
import { MultiContactTypesDropdown, multiUsersDropdown } from '../common/props';
export const searchForACompany = createAction({
auth: CopperAuth,
name: 'searchForACompany',
displayName: 'Search for a Company',
description: 'Lookup a company.',
props: {
name: Property.ShortText({
displayName: 'Name',
description: 'Full name of the Company to search for.',
required: false,
}),
phone_number: Property.ShortText({
displayName: 'Phone Number',
description: 'Phone Number of the Company to search for.',
required: false,
}),
email_domains: Property.ShortText({
displayName: 'Email Domain',
required: false,
description: 'Email Domain of the Company to search for.',
}),
contact_type_ids: MultiContactTypesDropdown({}),
assignee_ids: multiUsersDropdown({ refreshers: ['auth'] }),
city: Property.ShortText({
displayName: 'City',
description: 'The city in which Company must be located.',
required: false,
}),
state: Property.ShortText({
displayName: 'State',
description: 'The state or province in which Company must be located.',
required: false,
}),
postal_code: Property.ShortText({
displayName: 'Postal Code',
description: 'The postal code in which Company must be located.',
required: false,
}),
country: Property.ShortText({
displayName: 'Country',
description:
'The two character country code where Company must be located.',
required: false,
}),
tags: Property.Array({
displayName: 'Tags',
description:
'Filter Company to those that match at least one of the tags specified.',
required: false,
defaultValue: [],
}),
socials: Property.Array({
displayName: 'Socials',
description:
'Filter Company to those that match at least one of the social accounts specified.',
required: false,
defaultValue: [],
}),
followed: Property.StaticDropdown({
displayName: 'Followed',
description: 'Filter by followed state',
required: false,
options: {
options: [
{
label: 'followed',
value: '1',
},
{
label: 'not followed',
value: '2',
},
],
},
}),
age: Property.Number({
displayName: 'Age',
description: 'The maximum age in seconds that Company must be.',
required: false,
}),
page_size: Property.Number({
displayName: 'Page Size',
description: 'Default 50. Max 200.',
required: false,
defaultValue: 50,
}),
page_number: Property.Number({
displayName: 'Page Number',
required: false,
defaultValue: 1,
}),
sort_by: Property.StaticDropdown({
displayName: 'Sort By',
description: 'The field on which to sort the results',
required: false,
options: {
options: [
{
label: 'Name',
value: 'name',
},
{
label: 'Phone',
value: 'phone',
},
{
label: 'Contact',
value: 'contact',
},
{
label: 'Contact First Name',
value: 'contact_first_name',
},
{
label: 'Contact Last Name',
value: 'contact_last_name',
},
{
label: 'Date Modified',
value: 'date_modified',
},
{
label: 'Date Created',
value: 'date_created',
},
{
label: 'Email Domain',
value: 'email_domain',
},
{
label: 'City',
value: 'city',
},
{
label: 'State',
value: 'state',
},
{
label: 'Country',
value: 'country',
},
{
label: 'Zip',
value: 'zip',
},
{
label: 'Assignee',
value: 'assignee',
},
{
label: 'Contact Group',
value: 'contact_group',
},
{
label: 'Last Interaction',
value: 'last_interaction',
},
{
label: 'Interaction Count',
value: 'interaction_count',
},
{
label: 'Primary Website',
value: 'primary_website',
},
{
label: 'Socials',
value: 'socials',
},
],
},
}),
sort_direction: Property.StaticDropdown({
displayName: 'Sort Direction',
description: 'The direction in which to sort the result',
required: false,
options: {
options: [
{
label: 'Ascending',
value: 'asc',
},
{
label: 'Descending',
value: 'desc',
},
],
},
}),
minimum_interaction_count: Property.Number({
displayName: 'Minimum Interaction Count',
required: false,
description: 'The minimum number of interactions Company must have had.',
}),
maximum_interaction_count: Property.Number({
displayName: 'Maximum Interaction Count',
required: false,
description: 'The maximum number of interactions Company must have had.',
}),
minimum_interaction_date: Property.DateTime({
displayName: 'Minimum Interaction Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the earliest date of the last interaction.',
}),
maximum_interaction_date: Property.DateTime({
displayName: 'Maximum Interaction Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the latest date of the last interaction.',
}),
minimum_created_date: Property.DateTime({
displayName: 'Minimum Created Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the earliest date Company are created.',
}),
maximum_created_date: Property.DateTime({
displayName: 'Maximum Created Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the latest date Company are Created.',
}),
},
async run(context) {
const {
name,
phone_number,
email_domains,
contact_type_ids,
assignee_ids,
city,
state,
postal_code,
country,
tags,
socials,
followed,
age,
page_size,
page_number,
sort_by,
sort_direction,
minimum_interaction_count,
maximum_interaction_count,
minimum_interaction_date,
maximum_interaction_date,
minimum_created_date,
maximum_created_date,
} = context.propsValue;
const payload = {
name,
phone_number,
email_domains,
contact_type_ids,
assignee_ids,
city,
state,
postal_code,
country,
tags,
socials,
followed,
age,
page_size,
page_number,
sort_by,
sort_direction,
minimum_interaction_count,
maximum_interaction_count,
minimum_interaction_date: toUnix(minimum_interaction_date),
maximum_interaction_date: toUnix(maximum_interaction_date),
minimum_created_date: toUnix(minimum_created_date),
maximum_created_date: toUnix(maximum_created_date),
};
return await CopperApiService.fetchCompanies(context.auth, payload)
},
});

View File

@@ -0,0 +1,311 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { CopperAuth, toUnix } from '../common/constants';
import {
MultiCustomerSourceDropdown,
MultiLeadStatusDropdown,
multiUsersDropdown,
} from '../common/props';
import { CopperApiService } from '../common/requests';
export const searchForALead = createAction({
auth: CopperAuth,
name: 'searchForALead',
displayName: 'Search for a Lead',
description: 'Lookup a lead using match criteria.',
props: {
name: Property.ShortText({
displayName: 'Name',
description: 'Full name of the Lead to search for.',
required: false,
}),
phone_number: Property.ShortText({
displayName: 'Phone Number',
description: 'Phone Number of the Lead to search for.',
required: false,
}),
emails: Property.ShortText({
displayName: 'Emails',
required: false,
description: 'Emails of the Lead to search for. ',
}),
assignee_ids: multiUsersDropdown({ refreshers: ['auth'] }),
status_ids: MultiLeadStatusDropdown({}),
customer_source_ids: MultiCustomerSourceDropdown({}),
city: Property.ShortText({
displayName: 'City',
description: 'The city in which Lead must be located.',
required: false,
}),
state: Property.ShortText({
displayName: 'State',
description: 'The state or province in which Lead must be located.',
required: false,
}),
postal_code: Property.ShortText({
displayName: 'Postal Code',
description: 'The postal code in which Lead must be located.',
required: false,
}),
country: Property.ShortText({
displayName: 'Country',
description: 'The two character country code where Lead must be located.',
required: false,
}),
tags: Property.Array({
displayName: 'Tags',
description:
'Filter Lead to those that match at least one of the tags specified.',
required: false,
defaultValue: [],
}),
socials: Property.Array({
displayName: 'Socials',
description:
'Filter Lead to those that match at least one of the social accounts specified.',
required: false,
defaultValue: [],
}),
followed: Property.StaticDropdown({
displayName: 'Followed',
description: 'Filter by followed state',
required: false,
options: {
options: [
{
label: 'followed',
value: '1',
},
{
label: 'not followed',
value: '2',
},
],
},
}),
age: Property.Number({
displayName: 'Age',
description: 'The maximum age in seconds that Lead must be.',
required: false,
}),
page_size: Property.Number({
displayName: 'Page Size',
description: 'Default 50. Max 200.',
required: false,
defaultValue: 50,
}),
page_number: Property.Number({
displayName: 'Page Number',
required: false,
defaultValue: 1,
}),
sort_by: Property.StaticDropdown({
displayName: 'Sort By',
description: 'The field on which to sort the results',
required: false,
options: {
options: [
{
label: 'Name',
value: 'name',
},
{
label: 'Company Name',
value: 'company_name',
},
{
label: 'Title',
value: 'title',
},
{
label: 'Value',
value: 'value',
},
{
label: 'Email',
value: 'email',
},
{
label: 'Phone',
value: 'phone',
},
{
label: 'Date Modified',
value: 'date_modified',
},
{
label: 'Date Created',
value: 'date_created',
},
{
label: 'City',
value: 'city',
},
{
label: 'State',
value: 'state',
},
{
label: 'Country',
value: 'country',
},
{
label: 'Zip',
value: 'zip',
},
{
label: 'Inactive Days',
value: 'inactive_days',
},
{
label: 'Socials',
value: 'socials',
},
],
},
}),
sort_direction: Property.StaticDropdown({
displayName: 'Sort Direction',
description: 'The direction in which to sort the result',
required: false,
options: {
options: [
{
label: 'Ascending',
value: 'asc',
},
{
label: 'Descending',
value: 'desc',
},
],
},
}),
include_converted_leads: Property.Checkbox({
displayName: 'Include Converted Leads',
description: 'Specify if response should contain converted leads.',
required: false,
defaultValue: false,
}),
minimum_monetary_value: Property.Number({
displayName: 'Minimum Monetary Value',
required: false,
description: 'The minimum monetary value Leads must have.',
}),
maximum_monetary_value: Property.Number({
displayName: 'Maximum Monetary Value',
required: false,
description: 'The maximum monetary value Leads must have.',
}),
minimum_interaction_count: Property.Number({
displayName: 'Minimum Interaction Count',
required: false,
description: 'The minimum number of interactions Lead must have had.',
}),
maximum_interaction_count: Property.Number({
displayName: 'Maximum Interaction Count',
required: false,
description: 'The maximum number of interactions Lead must have had.',
}),
minimum_interaction_date: Property.DateTime({
displayName: 'Minimum Interaction Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the earliest date of the last interaction.',
}),
maximum_interaction_date: Property.DateTime({
displayName: 'Maximum Interaction Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the latest date of the last interaction.',
}),
minimum_created_date: Property.DateTime({
displayName: 'Minimum Created Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the earliest date Lead are created.',
}),
maximum_created_date: Property.DateTime({
displayName: 'Maximum Created Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the latest date Lead are Created.',
}),
minimum_modified_date: Property.DateTime({
displayName: 'Minimum Modified Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the earliest date Lead are Modified.',
}),
maximum_modified_date: Property.DateTime({
displayName: 'Maximum Modified Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the latest date Lead are Modified.',
}),
},
async run(context) {
const {
name,
phone_number,
emails,
assignee_ids,
status_ids,
customer_source_ids,
city,
state,
postal_code,
country,
tags,
socials,
followed,
age,
page_size,
page_number,
sort_by,
sort_direction,
minimum_interaction_count,
maximum_interaction_count,
minimum_interaction_date,
maximum_interaction_date,
minimum_created_date,
maximum_created_date,
minimum_monetary_value,
maximum_monetary_value,
minimum_modified_date,
maximum_modified_date,
} = context.propsValue;
const payload = {
name,
phone_number,
emails,
assignee_ids,
status_ids,
customer_source_ids,
city,
state,
postal_code,
country,
tags,
socials,
followed,
age,
page_size,
page_number,
sort_by,
sort_direction,
minimum_interaction_count,
maximum_interaction_count,
minimum_interaction_date: toUnix(minimum_interaction_date),
maximum_interaction_date: toUnix(maximum_interaction_date),
minimum_created_date: toUnix(minimum_created_date),
maximum_created_date: toUnix(maximum_created_date),
minimum_monetary_value,
maximum_monetary_value,
minimum_modified_date: toUnix(minimum_modified_date),
maximum_modified_date: toUnix(maximum_modified_date),
};
return await CopperApiService.fetchLeads(context.auth, payload);
},
});

View File

@@ -0,0 +1,269 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { CopperAuth, toUnix } from '../common/constants';
import {
multiCompanyDropdown,
MultiContactTypesDropdown,
multiOpportunityDropdown,
multiUsersDropdown,
} from '../common/props';
import { CopperApiService } from '../common/requests';
export const searchForAPerson = createAction({
auth: CopperAuth,
name: 'searchForAPerson',
displayName: 'Search for a Person',
description: 'Lookup a person using match criteria.',
props: {
name: Property.ShortText({
displayName: 'Name',
description: 'Full name of the People to search for.',
required: false,
}),
phone_number: Property.ShortText({
displayName: 'Phone Number',
description: 'Phone Number of the People to search for.',
required: false,
}),
emails: Property.Array({
displayName: 'Emails',
required: false,
description: 'Emails of the People to search for.',
defaultValue: [],
}),
contact_type_ids: MultiContactTypesDropdown({}),
assignee_ids: multiUsersDropdown({ refreshers: ['auth'] }),
company_ids: multiCompanyDropdown({ refreshers: ['auth'] }),
opportunity_ids: multiOpportunityDropdown({ refreshers: ['auth'] }),
city: Property.ShortText({
displayName: 'City',
description: 'The city in which People must be located.',
required: false,
}),
state: Property.ShortText({
displayName: 'State',
description: 'The state or province in which People must be located.',
required: false,
}),
postal_code: Property.ShortText({
displayName: 'Postal Code',
description: 'The postal code in which People must be located.',
required: false,
}),
country: Property.ShortText({
displayName: 'Country',
description:
'The two character country code where People must be located.',
required: false,
}),
tags: Property.Array({
displayName: 'Tags',
description:
'Filter People to those that match at least one of the tags specified.',
required: false,
defaultValue: [],
}),
socials: Property.Array({
displayName: 'Socials',
description:
'Filter People to those that match at least one of the social accounts specified.',
required: false,
defaultValue: [],
}),
followed: Property.StaticDropdown({
displayName: 'Followed',
description: 'Filter by followed state',
required: false,
options: {
options: [
{
label: 'followed',
value: '1',
},
{
label: 'not followed',
value: '2',
},
],
},
}),
age: Property.Number({
displayName: 'Age',
description: 'The maximum age in seconds that People must be.',
required: false,
}),
page_size: Property.Number({
displayName: 'Page Size',
description: 'Default 50. Max 200.',
required: false,
defaultValue: 50,
}),
page_number: Property.Number({
displayName: 'Page Number',
required: false,
defaultValue: 1,
}),
sort_by: Property.StaticDropdown({
displayName: 'Sort By',
description: 'The field on which to sort the results',
required: false,
options: {
options: [
{
label: 'Name',
value: 'name',
},
{
label: 'Title',
value: 'title',
},
{
label: 'Email',
value: 'email',
},
{
label: 'Phone',
value: 'phone',
},
{
label: 'Date Modified',
value: 'date_modified',
},
{
label: 'Date Created',
value: 'date_created',
},
{
label: 'City',
value: 'city',
},
{
label: 'State',
value: 'state',
},
{
label: 'Country',
value: 'country',
},
{
label: 'Zip',
value: 'zip',
},
{
label: 'Socials',
value: 'socials',
},
],
},
}),
sort_direction: Property.StaticDropdown({
displayName: 'Sort Direction',
description: 'The direction in which to sort the result',
required: false,
options: {
options: [
{
label: 'Ascending',
value: 'asc',
},
{
label: 'Descending',
value: 'desc',
},
],
},
}),
minimum_interaction_count: Property.Number({
displayName: 'Minimum Interaction Count',
required: false,
description: 'The minimum number of interactions People must have had.',
}),
maximum_interaction_count: Property.Number({
displayName: 'Maximum Interaction Count',
required: false,
description: 'The maximum number of interactions People must have had.',
}),
minimum_interaction_date: Property.DateTime({
displayName: 'Minimum Interaction Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the earliest date of the last interaction.',
}),
maximum_interaction_date: Property.DateTime({
displayName: 'Maximum Interaction Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the latest date of the last interaction.',
}),
minimum_created_date: Property.DateTime({
displayName: 'Minimum Created Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the earliest date People are created.',
}),
maximum_created_date: Property.DateTime({
displayName: 'Maximum Created Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the latest date People are Created.',
}),
},
async run(context) {
const {
name,
phone_number,
emails,
contact_type_ids,
assignee_ids,
company_ids,
opportunity_ids,
city,
state,
postal_code,
country,
tags,
socials,
followed,
age,
page_size,
page_number,
sort_by,
sort_direction,
minimum_interaction_count,
maximum_interaction_count,
minimum_interaction_date,
maximum_interaction_date,
minimum_created_date,
maximum_created_date,
} = context.propsValue;
const payload = {
name,
phone_number,
emails,
contact_type_ids,
assignee_ids,
company_ids,
opportunity_ids,
city,
state,
postal_code,
country,
tags,
socials,
followed,
age,
page_size,
page_number,
sort_by,
sort_direction,
minimum_interaction_count,
maximum_interaction_count,
minimum_interaction_date: toUnix(minimum_interaction_date),
maximum_interaction_date: toUnix(maximum_interaction_date),
minimum_created_date: toUnix(minimum_created_date),
maximum_created_date: toUnix(maximum_created_date),
};
return await CopperApiService.fetchPeople(context.auth, payload);
},
});

View File

@@ -0,0 +1,180 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { CopperAuth, toUnix } from '../common/constants';
import { CopperApiService } from '../common/requests';
import { multiUsersDropdown } from '../common/props';
export const searchForAProject = createAction({
auth: CopperAuth,
name: 'searchForAProject',
displayName: 'Search for a Project',
description: 'Lookup a project.',
props: {
name: Property.ShortText({
displayName: 'Name',
description: 'Full name of the Opportunity to search for.',
required: false,
}),
assignee_ids: multiUsersDropdown({ refreshers: ['auth'] }),
statuses: Property.StaticMultiSelectDropdown({
displayName: 'Status',
description: 'Filter by Opportunity status',
required: false,
options: {
options: [
{
label: 'Open',
value: 'Open',
},
{
label: 'Completed',
value: 'Completed',
},
],
},
}),
tags: Property.Array({
displayName: 'Tags',
description:
'Filter People to those that match at least one of the tags specified.',
required: false,
defaultValue: [],
}),
followed: Property.StaticDropdown({
displayName: 'Followed',
description: 'Filter by followed state',
required: false,
options: {
options: [
{
label: 'followed',
value: '1',
},
{
label: 'not followed',
value: '2',
},
],
},
}),
page_size: Property.Number({
displayName: 'Page Size',
description: 'Default 50. Max 200.',
required: false,
defaultValue: 50,
}),
page_number: Property.Number({
displayName: 'Page Number',
required: false,
defaultValue: 1,
}),
sort_by: Property.StaticDropdown({
displayName: 'Sort By',
description: 'The field on which to sort the results',
required: false,
options: {
options: [
{
label: 'Name',
value: 'name',
},
{
label: 'Assigned To',
value: 'assigned_to',
},
{
label: 'Related To',
value: 'related_to',
},
{
label: 'Status',
value: 'status',
},
{
label: 'Date Modified',
value: 'date_modified',
},
{
label: 'Date Created',
value: 'date_created',
},
],
},
}),
sort_direction: Property.StaticDropdown({
displayName: 'Sort Direction',
description: 'The direction in which to sort the result',
required: false,
options: {
options: [
{
label: 'Ascending',
value: 'asc',
},
{
label: 'Descending',
value: 'desc',
},
],
},
}),
minimum_created_date: Property.DateTime({
displayName: 'Minimum Created Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the earliest date Opportunity are created.',
}),
maximum_created_date: Property.DateTime({
displayName: 'Maximum Created Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the latest date Opportunity are Created.',
}),
minimum_modified_date: Property.DateTime({
displayName: 'Minimum Modified Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the earliest date Opportunity are Modified.',
}),
maximum_modified_date: Property.DateTime({
displayName: 'Maximum Modified Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the latest date Opportunity are Modified.',
}),
},
async run(context) {
const {
name,
assignee_ids,
statuses,
tags,
followed,
page_size,
page_number,
sort_by,
sort_direction,
minimum_created_date,
maximum_created_date,
minimum_modified_date,
maximum_modified_date,
} = context.propsValue;
const payload = {
name,
assignee_ids: assignee_ids || [],
statuses,
followed,
page_size,
page_number,
sort_by,
tags,
sort_direction,
minimum_created_date: toUnix(minimum_created_date),
maximum_created_date: toUnix(maximum_created_date),
minimum_modified_date: toUnix(minimum_modified_date),
maximum_modified_date: toUnix(maximum_modified_date),
};
return await CopperApiService.fetchProjects(context.auth, payload);
},
});

View File

@@ -0,0 +1,157 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { CopperAuth, CopperAuthType, toUnix } from '../common/constants';
import { MultiActivityTypesDropdown } from '../common/props';
import { CopperApiService } from '../common/requests';
export const searchForAnActivity = createAction({
auth: CopperAuth,
name: 'searchForAnActivity',
displayName: 'Search for an Activity',
description: 'Find an existing activity by type/criteria.',
props: {
entity: Property.StaticDropdown({
displayName: 'Parent Entity',
description: 'Select parent entity',
required: false,
options: {
options: [
{ label: 'Person', value: 'person' },
{ label: 'Company', value: 'company' },
{ label: 'Lead', value: 'lead' },
{ label: 'Opportunity', value: 'opportunity' },
{ label: 'Project', value: 'project' },
{ label: 'Task', value: 'task' },
],
},
}),
entityItemId: Property.Dropdown({
auth: CopperAuth,
displayName: 'Parent Entity Resource',
description: 'Select Resource',
required: false,
refreshers: ['auth', 'entity'],
async options(propsValue: Record<string, unknown>) {
const auth = propsValue['auth'] as CopperAuthType | undefined;
const entity = propsValue['entity'] as
| 'person'
| 'company'
| 'lead'
| 'opportunity'
| 'project'
| 'task';
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
if (!entity) {
return {
disabled: true,
placeholder: 'Select a Parent Entity first',
options: [],
};
}
const fetchFnMap = {
person: CopperApiService.fetchPeople,
company: CopperApiService.fetchCompanies,
lead: CopperApiService.fetchLeads,
opportunity: CopperApiService.fetchOpportunities,
task: CopperApiService.fetchTasks,
project: CopperApiService.fetchProjects,
};
const fetchFn = fetchFnMap[entity];
try {
const items = await fetchFn(auth);
return {
options: items.map((item: any) => ({
label: item.name,
value: item.id,
})),
};
} catch (e) {
console.error('Failed to fetch entity items', e);
return {
options: [],
placeholder: 'Unable to load entity items',
};
}
},
}),
activity_types: MultiActivityTypesDropdown({}),
page_size: Property.Number({
displayName: 'Page Size',
description: 'Default 50. Max 200.',
required: false,
defaultValue: 50,
}),
page_number: Property.Number({
displayName: 'Page Number',
required: false,
defaultValue: 1,
}),
minimum_activity_date: Property.DateTime({
displayName: 'Minimum Activity Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 11:40. The timestamp of the earliest activity date.',
}),
maximum_activity_date: Property.DateTime({
displayName: 'Maximum Activity Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the latest activity date.',
}),
full_result: Property.Checkbox({
displayName: 'Full Result',
description:
'(Optional) If set, search performance improves but duplicate activity logs may be returned',
required: false,
defaultValue: false,
}),
},
async run(ctx) {
const {
page_size,
page_number,
activity_types,
entity,
entityItemId,
minimum_activity_date,
maximum_activity_date,
full_result,
} = ctx.propsValue;
const parsed_activity_types = (activity_types || []).map(
(activity: any) => {
const parsed_activity = JSON.parse(activity);
return {
id: parsed_activity.id,
category: parsed_activity.category,
};
}
);
const payload = {
...(entity &&
entityItemId && {
parent: { id: entityItemId, type: entity },
}),
activity_types: parsed_activity_types,
page_number,
page_size,
minimum_activity_date: toUnix(minimum_activity_date),
maximum_activity_date: toUnix(maximum_activity_date),
full_result,
};
return await CopperApiService.fetchActivities(ctx.auth, payload);
},
});

View File

@@ -0,0 +1,381 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { CopperAuth, toUnix } from '../common/constants';
import {
multiCompanyDropdown,
MultiCustomerSourceDropdown,
MultiLossReasonsDropdown,
multiPipelinesDropdown,
multiPrimaryContactsDropdown,
multiUsersDropdown,
pipelinesDropdown,
} from '../common/props';
import { CopperApiService } from '../common/requests';
export const searchForAnOpportunity = createAction({
auth: CopperAuth,
name: 'searchForAnOpportunity',
displayName: 'Search for an Opportunity',
description: 'Lookup an opportunity.',
props: {
name: Property.ShortText({
displayName: 'Name',
description: 'Full name of the Opportunity to search for.',
required: false,
}),
assignee_ids: multiUsersDropdown({ refreshers: ['auth'] }),
company_ids: multiCompanyDropdown({ refreshers: ['auth'] }),
status_ids: Property.StaticMultiSelectDropdown({
displayName: 'Status',
description: 'Filter by Opportunity status',
required: false,
options: {
options: [
{
label: 'Open',
value: '0',
},
{
label: 'Won',
value: '1',
},
{
label: 'Lost',
value: '2',
},
{
label: 'Abandoned',
value: '3',
},
],
},
}),
priorities: Property.StaticMultiSelectDropdown({
displayName: 'Priority',
required: false,
options: {
disabled: false,
options: [
{ label: 'None', value: 'None' },
{ label: 'Low', value: 'Low' },
{ label: 'Medium', value: 'Medium' },
{ label: 'High', value: 'High' },
],
},
}),
pipeline_ids: multiPipelinesDropdown({ refreshers: ['auth'] }),
pipeline_stage_ids: Property.MultiSelectDropdown({
auth: CopperAuth,
displayName: 'Pipeline Stage',
description: 'Select a stage',
refreshers: ['auth', 'pipeline_ids'],
required: false,
async options({ auth, pipeline_ids }: any) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
if (!pipeline_ids) {
return {
disabled: true,
placeholder: 'Select a pipeline first',
options: [],
};
}
const stages = pipeline_ids.flatMap((pipeline: any) => {
const stages = JSON.parse(pipeline).stages ?? [];
return stages;
});
return {
options: stages.map((stage: any) => ({
label: stage.name,
value: stage.id,
})),
};
},
}),
primary_contact_ids: multiPrimaryContactsDropdown({ refreshers: ['auth'] }),
customer_source_ids: MultiCustomerSourceDropdown({}),
loss_reason_ids: MultiLossReasonsDropdown({}),
tags: Property.Array({
displayName: 'Tags',
description:
'Filter People to those that match at least one of the tags specified.',
required: false,
defaultValue: [],
}),
followed: Property.StaticDropdown({
displayName: 'Followed',
description: 'Filter by followed state',
required: false,
options: {
options: [
{
label: 'followed',
value: '1',
},
{
label: 'not followed',
value: '2',
},
],
},
}),
page_size: Property.Number({
displayName: 'Page Size',
description: 'Default 50. Max 200.',
required: false,
defaultValue: 50,
}),
page_number: Property.Number({
displayName: 'Page Number',
required: false,
defaultValue: 1,
}),
sort_by: Property.StaticDropdown({
displayName: 'Sort By',
description: 'The field on which to sort the results',
required: false,
options: {
options: [
{
label: 'Assignee',
value: 'assignee',
},
{
label: 'Company Name',
value: 'company_name',
},
{
label: 'Customer Source ID',
value: 'customer_source_id',
},
{
label: 'Date Created',
value: 'date_created',
},
{
label: 'Date Modified',
value: 'date_modified',
},
{
label: 'Inactive Days',
value: 'inactive_days',
},
{
label: 'Interaction Count',
value: 'interaction_count',
},
{
label: 'Last Interaction',
value: 'last_interaction',
},
{
label: 'Monetary Unit',
value: 'monetary_unit',
},
{
label: 'Monetary Value',
value: 'monetary_value',
},
{
label: 'Name',
value: 'name',
},
{
label: 'Primary Contact',
value: 'primary_contact',
},
{
label: 'Priority',
value: 'priority',
},
{
label: 'Stage',
value: 'stage',
},
{
label: 'Status',
value: 'status',
},
],
},
}),
sort_direction: Property.StaticDropdown({
displayName: 'Sort Direction',
description: 'The direction in which to sort the result',
required: false,
options: {
options: [
{
label: 'Ascending',
value: 'asc',
},
{
label: 'Descending',
value: 'desc',
},
],
},
}),
minimum_monetary_value: Property.Number({
displayName: 'Minimum Monetary Value',
required: false,
description: 'The minimum monetary value Opportunities must have.',
}),
maximum_monetary_value: Property.Number({
displayName: 'Maximum Monetary Value',
required: false,
description: 'The maximum monetary value Opportunities must have.',
}),
minimum_interaction_count: Property.Number({
displayName: 'Minimum Interaction Count',
required: false,
description:
'The minimum number of interactions Opportunity must have had.',
}),
maximum_interaction_count: Property.Number({
displayName: 'Maximum Interaction Count',
required: false,
description:
'The maximum number of interactions Opportunity must have had.',
}),
minimum_close_date: Property.DateTime({
displayName: 'Minimum Interaction Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the earliest date close date.',
}),
maximum_close_date: Property.DateTime({
displayName: 'Maximum Interaction Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the latest date close date.',
}),
minimum_interaction_date: Property.DateTime({
displayName: 'Minimum Interaction Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the earliest date of the last interaction.',
}),
maximum_interaction_date: Property.DateTime({
displayName: 'Maximum Interaction Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the latest date of the last interaction.',
}),
minimum_stage_change_date: Property.DateTime({
displayName: 'Minimum Stage Change Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the earliest date of a stage change.',
}),
maximum_stage_change_date: Property.DateTime({
displayName: 'Maximum Stage Change Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the latest date of a stage change.',
}),
minimum_created_date: Property.DateTime({
displayName: 'Minimum Created Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the earliest date Opportunity are created.',
}),
maximum_created_date: Property.DateTime({
displayName: 'Maximum Created Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the latest date Opportunity are Created.',
}),
minimum_modified_date: Property.DateTime({
displayName: 'Minimum Modified Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the earliest date Opportunity are Modified.',
}),
maximum_modified_date: Property.DateTime({
displayName: 'Maximum Modified Date',
required: false,
description:
'24-hour format, e.g. 2025-09-10 13:00. The timestamp of the latest date Opportunity are Modified.',
}),
},
async run(context) {
const {
name,
assignee_ids,
status_ids,
customer_source_ids,
pipeline_ids,
pipeline_stage_ids,
company_ids,
tags,
followed,
page_size,
page_number,
sort_by,
sort_direction,
priorities,
minimum_interaction_count,
maximum_interaction_count,
minimum_interaction_date,
maximum_interaction_date,
minimum_created_date,
maximum_created_date,
minimum_monetary_value,
maximum_monetary_value,
minimum_modified_date,
maximum_modified_date,
minimum_close_date,
minimum_stage_change_date,
maximum_close_date,
maximum_stage_change_date,
primary_contact_ids,
loss_reason_ids,
} = context.propsValue;
const payload = {
name,
assignee_ids,
status_ids,
customer_source_ids,
pipeline_ids: (pipeline_ids ?? []).flatMap((pipeline: any) => {
const id = JSON.parse(pipeline).id ?? [];
return id;
}),
pipeline_stage_ids,
company_ids,
primary_contact_ids,
loss_reason_ids,
tags,
priorities,
followed,
page_size,
page_number,
sort_by,
sort_direction,
minimum_interaction_count,
maximum_interaction_count,
minimum_interaction_date: toUnix(minimum_interaction_date),
maximum_interaction_date: toUnix(maximum_interaction_date),
minimum_created_date: toUnix(minimum_created_date),
maximum_created_date: toUnix(maximum_created_date),
minimum_monetary_value,
maximum_monetary_value,
minimum_modified_date: toUnix(minimum_modified_date),
maximum_modified_date: toUnix(maximum_modified_date),
minimum_close_date: toUnix(minimum_close_date),
minimum_stage_change_date: toUnix(minimum_stage_change_date),
maximum_close_date: toUnix(maximum_close_date),
maximum_stage_change_date: toUnix(maximum_stage_change_date),
};
return await CopperApiService.fetchOpportunities(context.auth, payload);
},
});

View File

@@ -0,0 +1,151 @@
import {
createAction,
InputPropertyMap,
Property,
} from '@activepieces/pieces-framework';
import { CopperAuth, isNonEmptyStr } from '../common/constants';
import { companyDropdown, primaryContactsDropdown } from '../common/props';
import { CopperApiService } from '../common/requests';
export const updateCompany = createAction({
auth: CopperAuth,
name: 'updateCompany',
displayName: 'Update Company',
description: 'Updates a company record.',
props: {
companyId: companyDropdown({ refreshers: ['auth'], required: true }),
fields: Property.DynamicProperties({
auth: CopperAuth,
displayName: '',
description: '',
refreshers: ['auth', 'companyId'],
required: false,
props: async ({ auth, companyId }: any): Promise<InputPropertyMap> => {
if (!auth || !companyId) return {};
const company = JSON.parse(companyId);
return {
name: Property.ShortText({
displayName: 'Full Name',
required: true,
defaultValue: company.name,
}),
email_domain: Property.ShortText({
displayName: 'Email Domain',
description: 'E.g. democompany.com',
required: false,
defaultValue: company.email_domain,
}),
details: Property.ShortText({
displayName: 'Details',
required: false,
defaultValue: company.details,
}),
phone_numbers: Property.Array({
displayName: 'Phone Numbers',
required: false,
properties: {
number: Property.ShortText({
displayName: 'Number',
required: true,
}),
category: Property.ShortText({
displayName: 'Category (e.g., mobile, work, personal)',
required: true,
}),
},
defaultValue: company.phone_numbers,
}),
address_street: Property.ShortText({
displayName: 'Street',
required: false,
defaultValue: company.address?.street,
}),
address_city: Property.ShortText({
displayName: 'City',
required: false,
defaultValue: company.address?.city,
}),
address_state: Property.ShortText({
displayName: 'State/Region',
required: false,
defaultValue: company.address?.state,
}),
address_postal_code: Property.ShortText({
displayName: 'Postal Code',
required: false,
defaultValue: company.address?.postal_code,
}),
address_country: Property.ShortText({
displayName: 'Country',
required: false,
defaultValue: company.address?.country,
}),
};
},
}),
primaryContactId: primaryContactsDropdown({ refreshers: ['auth'] }),
},
async run(context) {
const { fields, companyId, primaryContactId } = context.propsValue;
const company = JSON.parse(companyId as string);
const {
name,
email_domain,
details,
address_street,
address_city,
address_state,
address_postal_code,
address_country,
phone_numbers,
} = fields as any;
const normPhones = (Array.isArray(phone_numbers) ? phone_numbers : [])
.map((p) => ({
number: String(p?.number ?? '').trim(),
category: isNonEmptyStr(p?.category)
? String(p.category).trim()
: undefined,
}))
.filter((p) => isNonEmptyStr(p.number));
const address = [
address_street,
address_city,
address_state,
address_postal_code,
address_country,
].some(isNonEmptyStr)
? {
street: isNonEmptyStr(address_street)
? address_street.trim()
: undefined,
city: isNonEmptyStr(address_city) ? address_city.trim() : undefined,
state: isNonEmptyStr(address_state)
? address_state.trim()
: undefined,
postal_code: isNonEmptyStr(address_postal_code)
? address_postal_code.trim()
: undefined,
country: isNonEmptyStr(address_country)
? address_country.trim()
: undefined,
}
: undefined;
const body: any = {
name,
email_domain,
details,
...(normPhones.length ? { phone_numbers: normPhones } : {}),
...(address ? { address } : {}),
primary_contact_id: primaryContactId,
};
return await CopperApiService.updateCompany(context.auth, company.id, body);
},
});

View File

@@ -0,0 +1,150 @@
import {
createAction,
InputPropertyMap,
Property,
} from '@activepieces/pieces-framework';
import { CopperAuth, isNonEmptyStr } from '../common/constants';
import { leadDropdown, peopleDropdown } from '../common/props';
import { CopperApiService } from '../common/requests';
export const updateLead = createAction({
auth: CopperAuth,
name: 'updateLead',
displayName: 'Update Lead',
description: 'Updates an existing lead.',
props: {
leadId: leadDropdown(['auth']),
fields: Property.DynamicProperties({
displayName: '',
description: '',
refreshers: ['auth', 'leadId'],
required: false,
auth: CopperAuth,
props: async ({ auth, leadId }) => {
if (!auth || !leadId) return {};
const lead = JSON.parse(leadId as string);
const map:InputPropertyMap= {
name: Property.ShortText({
displayName: 'Full Name',
required: true,
defaultValue: lead.name,
}),
email: Property.ShortText({
displayName: 'Email',
required: true,
defaultValue: lead.email.email,
}),
category: Property.ShortText({
displayName: 'Category of the email address (e.g., work, personal)',
required: true,
defaultValue: lead.email.category,
}),
phone_numbers: Property.Array({
displayName: 'Phone Numbers',
required: false,
properties: {
number: Property.ShortText({
displayName: 'Number',
required: true,
}),
category: Property.ShortText({
displayName: 'Category (e.g., mobile, work, personal)',
required: true,
}),
},
defaultValue: lead.phone_numbers,
}),
address_street: Property.ShortText({
displayName: 'Street',
required: false,
defaultValue: lead.address?.street,
}),
address_city: Property.ShortText({
displayName: 'City',
required: false,
defaultValue: lead.address?.city,
}),
address_state: Property.ShortText({
displayName: 'State/Region',
required: false,
defaultValue: lead.address?.state,
}),
address_postal_code: Property.ShortText({
displayName: 'Postal Code',
required: false,
defaultValue: lead.address?.postal_code,
}),
address_country: Property.ShortText({
displayName: 'Country',
required: false,
defaultValue: lead.address?.country,
}),
};
return map;
},
}),
},
async run(context) {
const fields = (context.propsValue as any).fields;
const leadId = (context.propsValue as any).leadId;
const lead = JSON.parse(leadId);
const {
name,
email,
category,
address_street,
address_city,
address_state,
address_postal_code,
address_country,
phone_numbers,
} = fields;
const normPhones = (Array.isArray(phone_numbers) ? phone_numbers : [])
.map((p) => ({
number: String(p?.number ?? '').trim(),
category: isNonEmptyStr(p?.category)
? String(p.category).trim()
: undefined,
}))
.filter((p) => isNonEmptyStr(p.number));
const address = [
address_street,
address_city,
address_state,
address_postal_code,
address_country,
].some(isNonEmptyStr)
? {
street: isNonEmptyStr(address_street)
? address_street.trim()
: undefined,
city: isNonEmptyStr(address_city) ? address_city.trim() : undefined,
state: isNonEmptyStr(address_state)
? address_state.trim()
: undefined,
postal_code: isNonEmptyStr(address_postal_code)
? address_postal_code.trim()
: undefined,
country: isNonEmptyStr(address_country)
? address_country.trim()
: undefined,
}
: undefined;
const body: any = {
name,
email: {
email,
category,
},
...(normPhones.length ? { phone_numbers: normPhones } : {}),
...(address ? { address } : {}),
};
return await CopperApiService.updateLead(context.auth, lead.id, body);
},
});

View File

@@ -0,0 +1,109 @@
import {
createAction,
Property,
InputPropertyMap,
} from '@activepieces/pieces-framework';
import { CopperAuth } from '../common/constants';
import {
opportunityDropdown,
pipelinesDropdown,
primaryContactsDropdown,
} from '../common/props';
import { CopperApiService } from '../common/requests';
export const updateOpportunity = createAction({
auth: CopperAuth,
name: 'updateOpportunity',
displayName: 'Update Opportunity',
description: 'Updates an opportunity using match criteria.',
props: {
opportunityId: opportunityDropdown({
refreshers: ['auth'],
required: true,
}),
updateFields: Property.DynamicProperties({
auth: CopperAuth,
displayName: '',
description: '',
required: false,
refreshers: ['auth', 'opportunityId'],
props: async ({
auth,
opportunityId,
}: any): Promise<InputPropertyMap> => {
if (!auth || !opportunityId) return {};
const opportunity = JSON.parse(opportunityId);
return {
name: Property.ShortText({
displayName: 'Name',
description: 'The name of the opportunity',
required: true,
defaultValue: opportunity.name,
}),
};
},
}),
pipelineId: pipelinesDropdown({ refreshers: ['auth'] }),
pipelineStageId: Property.Dropdown({
auth: CopperAuth,
displayName: 'Pipeline Stage',
description: 'Select a stage',
refreshers: ['auth', 'pipelineId'],
required: false,
async options({ auth, pipelineId }: any) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
if (!pipelineId) {
return {
disabled: true,
placeholder: 'Select a pipeline first',
options: [],
};
}
const pipeline = JSON.parse(pipelineId);
const stages = pipeline.stages;
return {
options: stages.map((stage: any) => ({
label: stage.name,
value: stage.id,
})),
};
},
}),
primaryContactId: primaryContactsDropdown({ refreshers: ['auth'] }),
},
async run(context) {
const {
updateFields,
primaryContactId,
pipelineId,
pipelineStageId,
opportunityId,
} = context.propsValue;
const pipeline = JSON.parse(pipelineId as string);
const opportunity = JSON.parse(opportunityId as string);
return await CopperApiService.updateOpportunity(
context.auth,
opportunity.id,
{
name: (updateFields as any).name,
primary_contact_id: primaryContactId,
pipeline_id: pipeline.id,
pipeline_stage_id: pipelineStageId,
}
);
},
});

View File

@@ -0,0 +1,172 @@
import {
createAction,
InputPropertyMap,
Property,
} from '@activepieces/pieces-framework';
import { CopperAuth, isNonEmptyStr } from '../common/constants';
import { peopleDropdown } from '../common/props';
import { CopperApiService } from '../common/requests';
export const updatePerson = createAction({
auth: CopperAuth,
name: 'updatePerson',
displayName: 'Update Person',
description: 'Updates a person based on matching criteria.',
props: {
personId: peopleDropdown(['auth']),
fields: Property.DynamicProperties({
displayName: '',
description: '',
refreshers: ['auth', 'personId'],
required: false,
auth: CopperAuth,
props: async ({ auth, personId }) => {
if (!auth || !personId) return {};
const person = JSON.parse(personId as string);
const map:InputPropertyMap= {
name: Property.ShortText({
displayName: 'Full Name',
required: true,
defaultValue: person.name,
}),
emails: Property.Array({
displayName: 'Emails',
required: true,
properties: {
email: Property.ShortText({
displayName: 'Email',
required: true,
}),
category: Property.ShortText({
displayName: 'Category (e.g., work, personal)',
required: true,
}),
},
defaultValue: person.emails,
}),
phone_numbers: Property.Array({
displayName: 'Phone Numbers',
required: false,
properties: {
number: Property.ShortText({
displayName: 'Number',
required: true,
}),
category: Property.ShortText({
displayName: 'Category (e.g., mobile, work, personal)',
required: true,
}),
},
defaultValue: person.phone_numbers,
}),
address_street: Property.ShortText({
displayName: 'Street',
required: false,
defaultValue: person.address?.street,
}),
address_city: Property.ShortText({
displayName: 'City',
required: false,
defaultValue: person.address?.city,
}),
address_state: Property.ShortText({
displayName: 'State/Region',
required: false,
defaultValue: person.address?.state,
}),
address_postal_code: Property.ShortText({
displayName: 'Postal Code',
required: false,
defaultValue: person.address?.postal_code,
}),
address_country: Property.ShortText({
displayName: 'Country',
required: false,
defaultValue: person.address?.country,
}),
} ;
return map;
},
}),
},
async run(context) {
const fields = (context.propsValue as any).fields;
const personId = (context.propsValue as any).personId;
const person = JSON.parse(personId);
const {
name,
emails,
address_street,
address_city,
address_state,
address_postal_code,
address_country,
phone_numbers,
} = fields;
const normEmails = (Array.isArray(emails) ? emails : [])
.map((row) => ({
email: String(row?.email ?? '')
.trim()
.toLowerCase(),
category: isNonEmptyStr(row?.category)
? String(row.category).trim()
: undefined,
}))
.filter((e) => isNonEmptyStr(e.email));
if (normEmails.length === 0)
throw new Error('Please provide at least one valid email.');
const seen = new Set<string>();
const dedupedEmails = normEmails.filter(
(e) => !seen.has(e.email) && (seen.add(e.email), true)
);
const normPhones = (Array.isArray(phone_numbers) ? phone_numbers : [])
.map((p) => ({
number: String(p?.number ?? '').trim(),
category: isNonEmptyStr(p?.category)
? String(p.category).trim()
: undefined,
}))
.filter((p) => isNonEmptyStr(p.number));
const address = [
address_street,
address_city,
address_state,
address_postal_code,
address_country,
].some(isNonEmptyStr)
? {
street: isNonEmptyStr(address_street)
? address_street.trim()
: undefined,
city: isNonEmptyStr(address_city) ? address_city.trim() : undefined,
state: isNonEmptyStr(address_state)
? address_state.trim()
: undefined,
postal_code: isNonEmptyStr(address_postal_code)
? address_postal_code.trim()
: undefined,
country: isNonEmptyStr(address_country)
? address_country.trim()
: undefined,
}
: undefined;
const body: any = {
name,
emails: dedupedEmails,
...(normPhones.length ? { phone_numbers: normPhones } : {}),
...(address ? { address } : {}),
};
return await CopperApiService.updatePerson(context.auth, person.id, body);
},
});

View File

@@ -0,0 +1,58 @@
import {
createAction,
InputPropertyMap,
Property,
} from '@activepieces/pieces-framework';
import { CopperAuth } from '../common/constants';
import { projectsDropdown } from '../common/props';
import { CopperApiService } from '../common/requests';
export const updateProject = createAction({
auth: CopperAuth,
name: 'updateProject',
displayName: 'Update Project',
description: 'Updates a project record.',
props: {
projectId: projectsDropdown({ refreshers: ['auth'], required: true }),
updateFields: Property.DynamicProperties({
displayName: '',
description: '',
refreshers: ['auth', 'projectId'],
auth: CopperAuth,
required: false,
props: async ({ auth, projectId }: any): Promise<InputPropertyMap> => {
if (!auth || !projectId) return {};
const project = JSON.parse(projectId);
const map:InputPropertyMap= {
name: Property.ShortText({
displayName: 'Name',
description: 'The name of the project',
required: true,
defaultValue: project.name,
}),
details: Property.ShortText({
displayName: 'Details',
description: 'The details of the project',
required: false,
defaultValue: project.details,
}),
};
return map;
},
}),
},
async run(context) {
const { projectId, updateFields } = context.propsValue;
const { name, details } = updateFields as any;
const project = JSON.parse(projectId as string);
return await CopperApiService.updateProject(context.auth, project.id, {
name,
details,
});
},
});

View File

@@ -0,0 +1,67 @@
import { AppConnectionValueForAuthProperty, PieceAuth, Property } from '@activepieces/pieces-framework';
import { CopperApiService } from './requests';
import { AppConnectionType } from '@activepieces/shared';
export const BASE_URL = 'https://api.copper.com/developer_api';
export const CopperAuth = PieceAuth.CustomAuth({
description: '',
required: true,
props: {
email: Property.ShortText({
displayName: 'Email Address',
description: 'Email Address of the Token Owner',
required: true,
}),
apiKey: Property.ShortText({
displayName: 'API Key',
description: 'Your API Key in settings > integrations',
required: true,
}),
},
validate: async ({ auth }) => {
try {
await CopperApiService.fetchCurrentUser({props: auth, type: AppConnectionType.CUSTOM_AUTH})
return {
valid: true,
};
} catch (err) {
return {
valid: false,
error: "Invalid API Credentials, please check your credentials and try again"
};
}
},
});
export const API_ENDPOINTS = {
USERS: '/users',
WEBHOOKS: '/webhooks',
PEOPLE: '/people',
LEADS: '/leads',
COMPANIES: '/companies',
OPPORTUNITIES: '/opportunities',
TASKS: '/tasks',
PIPELINES: '/pipelines',
PROJECTS: '/projects',
};
export type CopperAuthType = AppConnectionValueForAuthProperty<typeof CopperAuth>;
export const isNonEmptyStr = (v: any) => typeof v === 'string' && v.trim().length > 0;
export const toUnix = (iso?: string | null) =>
iso ? Math.floor(new Date(iso).getTime() / 1000) : undefined;
export type CopperActivity = {
id: number;
name?: string;
details?: string;
assignee_id?: number;
custom_activity_type_id?: number;
parent?: { type: 'person'|'company'|'lead'|'opportunity'|'project'; id: number };
activity_date?: number; // unix seconds
date_created: number; // unix seconds
date_modified?: number; // unix seconds
tags?: string[];
};

View File

@@ -0,0 +1,766 @@
import { Property } from '@activepieces/pieces-framework';
import { CopperApiService } from './requests';
import { CopperAuth } from './constants';
export const peopleDropdown = (refreshers: string[]) =>
Property.Dropdown({
displayName: 'Person',
description: 'select a person',
required: true,
refreshers,
auth: CopperAuth,
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
try {
const people = await CopperApiService.fetchPeople(auth);
return {
options: people.map((person: any) => ({
label: person.name,
value: JSON.stringify(person),
})),
};
} catch (e) {
console.error('Failed to fetch campaigns', e);
return {
options: [],
placeholder: 'Unable to load campaigns',
};
}
},
});
export const leadDropdown = (refreshers: string[]) =>
Property.Dropdown({
auth: CopperAuth,
displayName: 'Lead',
description: 'select a Lead',
required: true,
refreshers,
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
try {
const leads = await CopperApiService.fetchLeads(auth);
return {
options: leads.map((lead: any) => ({
label: lead.name,
value: JSON.stringify(lead),
})),
};
} catch (e) {
console.error('Failed to fetch leads', e);
return {
options: [],
placeholder: 'Unable to load leads',
};
}
},
});
export const companyDropdown = ({
refreshers,
required = false,
}: {
refreshers: string[];
required?: boolean;
}) =>
Property.Dropdown({
auth: CopperAuth,
displayName: 'Company',
description: 'select a Company',
required,
refreshers,
async options({ auth }: any) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
try {
const companies = await CopperApiService.fetchCompanies(auth);
return {
options: companies.map((company: any) => ({
label: company.name,
value: JSON.stringify(company),
})),
};
} catch (e) {
console.error('Failed to fetch companies', e);
return {
options: [],
placeholder: 'Unable to load companies',
};
}
},
});
export const multiCompanyDropdown = ({
refreshers,
required = false,
}: {
refreshers: string[];
required?: boolean;
}) =>
Property.MultiSelectDropdown({
auth: CopperAuth,
displayName: 'Company',
description: 'select Companies',
required,
refreshers,
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
try {
const companies = await CopperApiService.fetchCompanies(auth);
return {
options: companies.map((company: any) => ({
label: company.name,
value: company.id,
})),
};
} catch (e) {
console.error('Failed to fetch companies', e);
return {
options: [],
placeholder: 'Unable to load companies',
};
}
},
});
export const primaryContactsDropdown = ({
refreshers,
required = false,
}: {
refreshers: string[];
required?: boolean;
}) =>
Property.Dropdown({
auth: CopperAuth,
displayName: 'Primary Contact',
description: 'select a primary contact',
required,
refreshers,
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
try {
const primaryContacts = await CopperApiService.fetchPeople(auth);
return {
options: primaryContacts.map((contact: any) => ({
label: contact.name,
value: contact.id,
})),
};
} catch (e) {
console.error('Failed to fetch opportunities', e);
return {
options: [],
placeholder: 'Unable to load opportunities',
};
}
},
});
export const multiPrimaryContactsDropdown = ({
refreshers,
required = false,
}: {
refreshers: string[];
required?: boolean;
}) =>
Property.MultiSelectDropdown({
auth: CopperAuth,
displayName: 'Primary Contacts',
description: 'select primary contacts',
required,
refreshers,
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
try {
const primaryContacts = await CopperApiService.fetchPeople(auth);
return {
options: primaryContacts.map((contact: any) => ({
label: contact.name,
value: contact.id,
})),
};
} catch (e) {
console.error('Failed to fetch opportunities', e);
return {
options: [],
placeholder: 'Unable to load opportunities',
};
}
},
});
export const usersDropdown = ({
refreshers,
required = false,
}: {
refreshers: string[];
required?: boolean;
}) =>
Property.Dropdown({
auth: CopperAuth,
displayName: 'Assignee',
description: 'select a user to assign to',
required,
refreshers,
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
try {
const users = await CopperApiService.fetchUsers(auth);
return {
options: users.map((user: any) => ({
label: user.name,
value: user.id,
})),
};
} catch (e) {
console.error('Failed to fetch opportunities', e);
return {
options: [],
placeholder: 'Unable to load opportunities',
};
}
},
});
export const multiUsersDropdown = ({
refreshers,
required = false,
}: {
refreshers: string[];
required?: boolean;
}) =>
Property.MultiSelectDropdown({
auth: CopperAuth,
displayName: 'Assignee',
description: 'select assignees',
required,
refreshers,
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
try {
const users = await CopperApiService.fetchUsers(auth);
return {
options: users.map((user: any) => ({
label: user.name,
value: user.id,
})),
};
} catch (e) {
console.error('Failed to fetch opportunities', e);
return {
options: [],
placeholder: 'Unable to load opportunities',
};
}
},
});
export const opportunityDropdown = ({
refreshers,
required = false,
}: {
refreshers: string[];
required?: boolean;
}) =>
Property.Dropdown({
auth: CopperAuth,
displayName: 'Opportunity',
description: 'select an Opportunity',
required,
refreshers,
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
try {
const opportunities = await CopperApiService.fetchOpportunities(auth);
return {
options: opportunities.map((opportunity: any) => ({
label: opportunity.name,
value: JSON.stringify(opportunity),
})),
};
} catch (e) {
console.error('Failed to fetch opportunities', e);
return {
options: [],
placeholder: 'Unable to load opportunities',
};
}
},
});
export const multiOpportunityDropdown = ({
refreshers,
required = false,
}: {
refreshers: string[];
required?: boolean;
}) =>
Property.MultiSelectDropdown({
auth: CopperAuth,
displayName: 'Opportunity',
description: 'select Opportunities',
required,
refreshers,
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
try {
const opportunities = await CopperApiService.fetchOpportunities(auth);
return {
options: opportunities.map((opportunity: any) => ({
label: opportunity.name,
value: opportunity.id,
})),
};
} catch (e) {
console.error('Failed to fetch opportunities', e);
return {
options: [],
placeholder: 'Unable to load opportunities',
};
}
},
});
export const pipelinesDropdown = ({
refreshers,
required = false,
}: {
refreshers: string[];
required?: boolean;
}) =>
Property.Dropdown({
auth: CopperAuth,
displayName: 'Pipeline',
description: 'select a Pipeline',
required,
refreshers,
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
try {
const pipelines = await CopperApiService.fetchPipelines(auth);
return {
options: pipelines.map((pipeline: any) => ({
label: pipeline.name,
value: JSON.stringify(pipeline),
})),
};
} catch (e) {
console.error('Failed to fetch pipelines', e);
return {
options: [],
placeholder: 'Unable to load pipelines',
};
}
},
});
export const multiPipelinesDropdown = ({
refreshers,
required = false,
}: {
refreshers: string[];
required?: boolean;
}) =>
Property.MultiSelectDropdown({
auth: CopperAuth,
displayName: 'Pipeline',
description: 'select a Pipeline',
required,
refreshers,
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
try {
const pipelines = await CopperApiService.fetchPipelines(auth);
return {
options: pipelines.map((pipeline: any) => ({
label: pipeline.name,
value: JSON.stringify(pipeline),
})),
};
} catch (e) {
console.error('Failed to fetch pipelines', e);
return {
options: [],
placeholder: 'Unable to load pipelines',
};
}
},
});
export const projectsDropdown = ({
refreshers,
required = false,
}: {
refreshers: string[];
required?: boolean;
}) =>
Property.Dropdown({
auth: CopperAuth,
displayName: 'Project',
description: 'select a Project',
required,
refreshers,
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
try {
const projects = await CopperApiService.fetchProjects(auth);
return {
options: projects.map((project: any) => ({
label: project.name,
value: JSON.stringify(project),
})),
};
} catch (e) {
console.error('Failed to fetch projects', e);
return {
options: [],
placeholder: 'Unable to load projects',
};
}
},
});
export const ActivityTypesDropdown = (entity?: 'user' | 'system') =>
Property.Dropdown({
auth: CopperAuth,
displayName: 'Activity Type',
description: 'Select activity Type',
required: true,
refreshers: ['auth'],
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
try {
const response = await CopperApiService.fetchActivityTypes(auth);
const items =
entity && response[entity]
? response[entity]
: [...response.user, ...response.system];
return {
options: items.map((item: any) => ({
label: item.name,
value: JSON.stringify(item),
})),
};
} catch (e) {
console.error('Failed to fetch activity types', e);
return {
options: [],
placeholder: 'Unable to load activity types',
};
}
},
});
export const MultiActivityTypesDropdown = ({
entity,
required = false
}: {
entity?: 'user' | 'system';
required?: boolean;
}) =>
Property.MultiSelectDropdown({
auth: CopperAuth,
displayName: 'Activity Type',
description: 'Select activity Type',
required,
refreshers: ['auth'],
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
try {
const response = await CopperApiService.fetchActivityTypes(auth);
const items =
entity && response[entity]
? response[entity]
: [...response.user, ...response.system];
return {
options: items.map((item: any) => ({
label: item.name,
value: JSON.stringify(item),
})),
};
} catch (e) {
console.error('Failed to fetch activity types', e);
return {
options: [],
placeholder: 'Unable to load activity types',
};
}
},
});
export const MultiContactTypesDropdown = ({
required = false
}: {
required?: boolean;
}) =>
Property.MultiSelectDropdown({
auth: CopperAuth,
displayName: 'Contact Type',
description: 'Select contact Type',
required,
refreshers: ['auth'],
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
try {
const response = await CopperApiService.fetchContactTypes(auth);
return {
options: response.map((item: any) => ({
label: item.name,
value: item.id,
})),
};
} catch (e) {
console.error('Failed to fetch contact types', e);
return {
options: [],
placeholder: 'Unable to load contact types',
};
}
},
});
export const MultiLeadStatusDropdown = ({
required = false
}: {
required?: boolean;
}) =>
Property.MultiSelectDropdown({
auth: CopperAuth,
displayName: 'Lead Status',
description: 'Select lead status',
required,
refreshers: ['auth'],
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
try {
const response = await CopperApiService.fetchLeadStatuses(auth);
return {
options: response.map((item: any) => ({
label: item.name,
value: item.id,
})),
};
} catch (e) {
console.error('Failed to fetch lead statuses', e);
return {
options: [],
placeholder: 'Unable to load lead statuses',
};
}
},
});
export const MultiCustomerSourceDropdown = ({
required = false
}: {
required?: boolean;
}) =>
Property.MultiSelectDropdown({
auth: CopperAuth,
displayName: 'Customer Source',
description: 'Select customer source.',
required,
refreshers: ['auth'],
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
try {
const response = await CopperApiService.fetchCustomerSources(auth);
return {
options: response.map((item: any) => ({
label: item.name,
value: item.id,
})),
};
} catch (e) {
console.error('Failed to fetch customer sources', e);
return {
options: [],
placeholder: 'Unable to load customer sources',
};
}
},
});
export const MultiLossReasonsDropdown = ({
required = false
}: {
required?: boolean;
}) =>
Property.MultiSelectDropdown({
auth: CopperAuth,
displayName: 'Loss Reason',
description: 'Select loss reason.',
required,
refreshers: ['auth'],
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your Copper account first',
options: [],
};
}
try {
const response = await CopperApiService.fetchLossReasons(auth);
return {
options: response.map((item: any) => ({
label: item.name,
value: item.id,
})),
};
} catch (e) {
console.error('Failed to fetch loss reasons', e);
return {
options: [],
placeholder: 'Unable to load loss reasons',
};
}
},
});

View File

@@ -0,0 +1,269 @@
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
import { API_ENDPOINTS, BASE_URL, CopperAuthType } from './constants';
async function fireHttpRequest({
method,
path,
auth,
body,
}: {
method: HttpMethod;
path: string;
auth: CopperAuthType;
body?: any;
}) {
return await httpClient
.sendRequest({
method,
url: `${BASE_URL}${path}`,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-PW-AccessToken': auth.props.apiKey,
'X-PW-Application': 'developer_api',
'X-PW-UserEmail': auth.props.email,
},
body,
})
.then((res) => res.body)
.catch((err) => {
throw err;
});
}
export const CopperApiService = {
async fetchCurrentUser(auth: CopperAuthType) {
return await fireHttpRequest({
method: HttpMethod.GET,
path: `/v1${API_ENDPOINTS.USERS}/me`,
auth,
});
},
async fetchLeads(auth: CopperAuthType, payload?: any) {
return await fireHttpRequest({
method: HttpMethod.POST,
path: `/v1${API_ENDPOINTS.LEADS}/search`,
auth,
body: payload
});
},
async createTask(auth: CopperAuthType, payload: any) {
return await fireHttpRequest({
method: HttpMethod.POST,
path: `/v1${API_ENDPOINTS.TASKS}`,
auth,
body: payload,
});
},
async createProject(auth: CopperAuthType, payload: any) {
return await fireHttpRequest({
method: HttpMethod.POST,
path: `/v1${API_ENDPOINTS.PROJECTS}`,
auth,
body: payload,
});
},
async updateProject(auth: CopperAuthType, projectId: string, payload: any) {
return await fireHttpRequest({
method: HttpMethod.PUT,
path: `/v1${API_ENDPOINTS.PROJECTS}/${projectId}`,
auth,
body: payload,
});
},
async fetchProjects(auth: CopperAuthType, payload?: any) {
return await fireHttpRequest({
method: HttpMethod.POST,
path: `/v1${API_ENDPOINTS.PROJECTS}/search`,
auth,
body: payload
});
},
async createCompany(auth: CopperAuthType, payload: any) {
return await fireHttpRequest({
method: HttpMethod.POST,
path: `/v1${API_ENDPOINTS.COMPANIES}`,
auth,
body: payload,
});
},
async updateCompany(auth: CopperAuthType, companyId: string, payload: any) {
return await fireHttpRequest({
method: HttpMethod.PUT,
path: `/v1${API_ENDPOINTS.COMPANIES}/${companyId}`,
auth,
body: payload,
});
},
async fetchCompanies(auth: CopperAuthType, payload?: any) {
return await fireHttpRequest({
method: HttpMethod.POST,
path: `/v1${API_ENDPOINTS.COMPANIES}/search`,
auth,
body: payload
});
},
async fetchActivityTypes(auth: CopperAuthType) {
return await fireHttpRequest({
method: HttpMethod.GET,
path: `/v1/activity_types`,
auth,
});
},
async fetchContactTypes(auth: CopperAuthType) {
return await fireHttpRequest({
method: HttpMethod.GET,
path: `/v1/contact_types`,
auth,
});
},
async fetchLeadStatuses(auth: CopperAuthType) {
return await fireHttpRequest({
method: HttpMethod.GET,
path: `/v1/lead_statuses`,
auth,
});
},
async fetchCustomerSources(auth: CopperAuthType) {
return await fireHttpRequest({
method: HttpMethod.GET,
path: `/v1/customer_sources`,
auth,
});
},
async fetchLossReasons(auth: CopperAuthType) {
return await fireHttpRequest({
method: HttpMethod.GET,
path: `/v1/loss_reasons`,
auth,
});
},
async createActivity(auth: CopperAuthType, payload: any) {
return await fireHttpRequest({
method: HttpMethod.POST,
path: `/v1/activities`,
auth,
body: payload,
});
},
async fetchActivities(auth: CopperAuthType, payload: any) {
return await fireHttpRequest({
method: HttpMethod.POST,
path: `/v1/activities/search`,
auth,
body: payload,
});
},
async createOpportunity(auth: CopperAuthType, payload: any) {
return await fireHttpRequest({
method: HttpMethod.POST,
path: `/v1${API_ENDPOINTS.OPPORTUNITIES}`,
auth,
body: payload,
});
},
async updateOpportunity(
auth: CopperAuthType,
opportunityId: string,
payload: any
) {
return await fireHttpRequest({
method: HttpMethod.PUT,
path: `/v1${API_ENDPOINTS.OPPORTUNITIES}/${opportunityId}`,
auth,
body: payload,
});
},
async fetchOpportunities(auth: CopperAuthType, payload?: any) {
return await fireHttpRequest({
method: HttpMethod.POST,
path: `/v1${API_ENDPOINTS.OPPORTUNITIES}/search`,
auth,
body: payload
});
},
async fetchTasks(auth: CopperAuthType) {
return await fireHttpRequest({
method: HttpMethod.POST,
path: `/v1${API_ENDPOINTS.TASKS}/search`,
auth,
});
},
async fetchPipelines(auth: CopperAuthType) {
return await fireHttpRequest({
method: HttpMethod.GET,
path: `/v1${API_ENDPOINTS.PIPELINES}`,
auth,
});
},
async createLead(auth: CopperAuthType, payload: any) {
return await fireHttpRequest({
method: HttpMethod.POST,
path: `/v1${API_ENDPOINTS.LEADS}`,
auth,
body: payload,
});
},
async updateLead(auth: CopperAuthType, leadId: string, payload: any) {
return await fireHttpRequest({
method: HttpMethod.PUT,
path: `/v1${API_ENDPOINTS.LEADS}/${leadId}`,
auth,
body: payload,
});
},
async convertLead(auth: CopperAuthType, leadId: string, payload: any) {
return await fireHttpRequest({
method: HttpMethod.POST,
path: `/v1${API_ENDPOINTS.LEADS}/${leadId}/convert`,
auth,
body: payload,
});
},
async createPerson(auth: CopperAuthType, payload: any) {
return await fireHttpRequest({
method: HttpMethod.POST,
path: `/v1${API_ENDPOINTS.PEOPLE}`,
auth,
body: payload,
});
},
async updatePerson(auth: CopperAuthType, personId: string, payload: any) {
return await fireHttpRequest({
method: HttpMethod.PUT,
path: `/v1${API_ENDPOINTS.PEOPLE}/${personId}`,
auth,
body: payload,
});
},
async fetchPeople(auth: CopperAuthType, payload?: any) {
return await fireHttpRequest({
method: HttpMethod.POST,
path: `/v1${API_ENDPOINTS.PEOPLE}/search`,
auth,
body: payload
});
},
async fetchUsers(auth: CopperAuthType) {
return await fireHttpRequest({
method: HttpMethod.POST,
path: `/v1${API_ENDPOINTS.USERS}/search`,
auth,
});
},
async createWebhook(auth: CopperAuthType, payload: any) {
return await fireHttpRequest({
method: HttpMethod.POST,
path: `/v1${API_ENDPOINTS.WEBHOOKS}`,
auth,
body: payload,
});
},
async deleteWebhook(auth: CopperAuthType, webhookId: string) {
return await fireHttpRequest({
method: HttpMethod.DELETE,
path: `/v1${API_ENDPOINTS.WEBHOOKS}/${webhookId}`,
auth,
});
},
};

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];
},
});