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,56 @@
import { PieceAuth, Property } from "@activepieces/pieces-framework";
import { clickfunnelsApiService } from "./requests";
export const CLICKFUNNELS_BASE_URL = (subdomain: string) => `https://${subdomain}.myclickfunnels.com/api/v2`;
export const API_ENDPOINTS = {
ME: '/me',
TEAMS: '/teams',
WORKSPACES: '/workspaces',
PIPELINES: '/sales/pipelines',
CONTACTS: '/contacts',
COURSES: '/courses',
TAGS: '/tags',
};
export type CLICKFUNNELS_APIKEY_AUTH = {
subdomain: string;
apiKey: string;
};
export const clickfunnelsAuth = PieceAuth.CustomAuth({
description: 'Enter your ClickFunnels subdomain and API key.',
required: true,
props: {
subdomain: Property.ShortText({
displayName: 'Subdomain',
description:
'Your ClickFunnels subdomain (e.g., if your URL is https://mycompany.myclickfunnels.com, enter "mycompany").',
required: true,
}),
apiKey: PieceAuth.SecretText({
displayName: 'API Key',
description:
'Your ClickFunnels API key. You can find this in your ClickFunnels account settings.',
required: true,
}),
},
validate: async ({ auth }) => {
try {
await clickfunnelsApiService.fetchCurrentlyLoggedInUser(auth).catch((err) => {
throw new Error("something went wrong. Please check your username and API key and try again.")
})
return {
valid: true,
};
} catch (error) {
return {
valid: false,
error: `Connection failed: ${
error instanceof Error ? error.message : 'Unknown error'
}`,
};
}
},
});

View File

@@ -0,0 +1,474 @@
import { Property } from '@activepieces/pieces-framework';
import { clickfunnelsApiService } from './requests';
import { clickfunnelsAuth } from './constants';
export const teamsDropdown = (refreshers: string[]) =>
Property.Dropdown({
displayName: 'Team',
description: 'Select the team',
required: true,
refreshers,
auth: clickfunnelsAuth,
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your ClickFunnels account first',
};
}
try {
const response = await clickfunnelsApiService.fetchTeams(auth.props);
return {
options: response.map((team: any) => ({
label: team.name,
value: team.id,
})),
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder:
'Failed to load teams. Please check your authentication.',
};
}
},
});
export const coursesDropdown = (refreshers: string[]) =>
Property.Dropdown({
auth: clickfunnelsAuth,
displayName: 'Course',
description: 'Select a course',
required: true,
refreshers,
options: async ({ auth, workspaceId }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your ClickFunnels account first',
};
}
if (!workspaceId) {
return {
disabled: true,
options: [],
placeholder: 'Please select a workspace first',
};
}
try {
const courses = await clickfunnelsApiService.fetchCourses(
auth.props,
workspaceId as string
);
return {
options: courses.map((course: any) => ({
label: course.title,
value: course.id,
})),
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder:
'Failed to load teams. Please check your authentication.',
};
}
},
});
export const teamMembershipsDropdown = (
refreshers: string[],
required = true
) =>
Property.Dropdown({
auth: clickfunnelsAuth,
displayName: 'Assignee',
description: 'Select an assignee on your team',
required,
refreshers,
options: async ({ auth, teamId }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your ClickFunnels account first',
};
}
if (!teamId) {
return {
disabled: true,
options: [],
placeholder: 'Please select a team first',
};
}
try {
const response = await clickfunnelsApiService.fetchTeam(auth.props, teamId as string);
return {
options: [
{ label: 'Leave Unassigned', value: null },
...response.memberships,
].map((membership) => {
if (membership.label) {
return {
label: membership.label,
value: membership.value,
};
}
return {
label: `${membership.user.first_name} ${membership.user.last_name}`,
value: membership.id,
};
}),
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder:
'Failed to load team members. Please check your authentication.',
};
}
},
});
export const workspacesDropdown = (refreshers: string[]) =>
Property.Dropdown({
auth: clickfunnelsAuth,
displayName: 'Workspace',
description: 'Select the workspace',
required: true,
refreshers,
options: async ({ auth, teamId }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your ClickFunnels account first',
};
}
if (!teamId) {
return {
disabled: true,
options: [],
placeholder: 'Please select a team first',
};
}
try {
const workspaces = await clickfunnelsApiService.fetchWorkspaces(
auth.props,
teamId as string
);
return {
options: workspaces.map((workspace: any) => ({
label: workspace.name,
value: workspace.id,
})),
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder:
'Failed to load workspaces. Please check your authentication.',
};
}
},
});
export const pipelinesDropdown = (refreshers: string[]) =>
Property.Dropdown({
auth: clickfunnelsAuth,
displayName: 'Pipeline',
description: 'Select a pipeline',
required: true,
refreshers,
options: async ({ auth, workspaceId }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your ClickFunnels account first',
};
}
if (!workspaceId) {
return {
disabled: true,
options: [],
placeholder: 'Please select a workspace first',
};
}
try {
const pipelines = await clickfunnelsApiService.fetchPipelines(
auth.props,
workspaceId as string
);
return {
options: pipelines.map((pipeline: any) => ({
label: pipeline.name,
value: pipeline.id,
})),
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder:
'Failed to load pipelines. Please check your authentication.',
};
}
},
});
export const pipelineStagesDropdown = (refreshers: string[]) =>
Property.Dropdown({
auth: clickfunnelsAuth,
displayName: 'Pipeline Stage',
description: 'Select a pipeline stage.',
required: true,
refreshers,
options: async ({ auth, pipelineId }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your ClickFunnels account first',
};
}
if (!pipelineId) {
return {
disabled: true,
options: [],
placeholder: 'Please select a pipeline first',
};
}
try {
const pipelineStages = await clickfunnelsApiService.fetchPipelineStages(
auth.props,
pipelineId as string
);
return {
options: pipelineStages.map((pipelineStage: any) => ({
label: pipelineStage.name,
value: pipelineStage.id,
})),
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder:
'Failed to load pipeline stages. Please check your authentication.',
};
}
},
});
export const contactsDropdown = (refreshers: string[]) =>
Property.Dropdown({
auth: clickfunnelsAuth,
displayName: 'Contact',
description: 'Select a contact',
required: true,
refreshers,
options: async ({ auth, workspaceId }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your ClickFunnels account first',
};
}
if (!workspaceId) {
return {
disabled: true,
options: [],
placeholder: 'Please select a workspace first',
};
}
try {
const contacts = await clickfunnelsApiService.fetchContacts(
auth.props,
workspaceId as string
);
return {
options: contacts.map((contact: any) => ({
label: (() => {
const firstName = contact.first_name || '';
const lastName = contact.last_name || '';
const fullName = `${firstName} ${lastName}`.trim();
return (
fullName ||
contact.email_address ||
contact.phone_number ||
'Unknown Contact'
);
})(),
value: contact.id,
})),
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder:
'Failed to load contacts. Please check your authentication.',
};
}
},
});
export const tagsDropdown = (refreshers: string[]) =>
Property.Dropdown({
auth: clickfunnelsAuth,
displayName: 'Tag',
description: 'Select a tag to apply',
required: true,
refreshers,
options: async ({ auth, workspaceId }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your ClickFunnels account first',
};
}
if (!workspaceId) {
return {
disabled: true,
options: [],
placeholder: 'Please select a workspace first',
};
}
try {
const tags = await clickfunnelsApiService.fetchTags(auth.props, workspaceId as string);
return {
options: tags.map((tag: any) => ({
label: tag.name,
value: tag.id,
})),
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder: 'Failed to load tags. Please check your authentication.',
};
}
},
});
export const multiTagsDropdown = (refreshers: string[]) =>
Property.MultiSelectDropdown({
auth: clickfunnelsAuth,
displayName: 'Tag',
description: 'Select tags to apply',
required: false,
refreshers,
options: async ({ auth, workspaceId }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your ClickFunnels account first',
};
}
if (!workspaceId) {
return {
disabled: true,
options: [],
placeholder: 'Please select a workspace first',
};
}
try {
const tags = await clickfunnelsApiService.fetchTags(auth.props, workspaceId as string);
return {
options: tags.map((tag: any) => ({
label: tag.name,
value: tag.id,
})),
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder: 'Failed to load tags. Please check your authentication.',
};
}
},
});
export const appliedTagsDropdown = (refreshers: string[]) =>
Property.Dropdown({
auth: clickfunnelsAuth,
displayName: 'Tag',
description: 'Select a tag',
required: true,
refreshers,
options: async ({ auth, contactId }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your ClickFunnels account first',
};
}
if (!contactId) {
return {
disabled: true,
options: [],
placeholder: 'Please select a contact first',
};
}
try {
const tags = await clickfunnelsApiService.fetchAppliedTags(
auth.props,
contactId as string
);
return {
options: tags.map((tag: any) => ({
label: tag.tag.name,
value: tag.id,
})),
};
} catch (error) {
return {
disabled: true,
options: [],
placeholder: 'Failed to load tags. Please check your authentication.',
};
}
},
});

View File

@@ -0,0 +1,245 @@
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
import {
API_ENDPOINTS,
CLICKFUNNELS_BASE_URL,
CLICKFUNNELS_APIKEY_AUTH,
} from './constants';
async function fireHttpRequest<T>({
method,
auth,
path,
body,
}: {
auth: CLICKFUNNELS_APIKEY_AUTH;
method: HttpMethod;
path: string;
body?: unknown;
}) {
const BASE_URL = CLICKFUNNELS_BASE_URL(auth.subdomain);
return await httpClient
.sendRequest({
method,
url: `${BASE_URL}${path}`,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${auth.apiKey}`,
},
body,
})
.then((res) => res.body);
}
export const clickfunnelsApiService = {
fetchCurrentlyLoggedInUser: async (auth: CLICKFUNNELS_APIKEY_AUTH) => {
const response = await fireHttpRequest({
method: HttpMethod.GET,
auth,
path: API_ENDPOINTS.ME,
});
return response;
},
fetchTeams: async (auth: CLICKFUNNELS_APIKEY_AUTH) => {
const response = await fireHttpRequest({
method: HttpMethod.GET,
auth,
path: API_ENDPOINTS.TEAMS,
});
return response;
},
fetchTeam: async (auth: CLICKFUNNELS_APIKEY_AUTH, teamId: string) => {
const response = await fireHttpRequest({
method: HttpMethod.GET,
auth,
path: `${API_ENDPOINTS.TEAMS}/${teamId}`,
});
return response;
},
fetchWorkspaces: async (auth: CLICKFUNNELS_APIKEY_AUTH, teamId: string) => {
const response = await fireHttpRequest({
method: HttpMethod.GET,
auth,
path: `${API_ENDPOINTS.TEAMS}/${teamId}${API_ENDPOINTS.WORKSPACES}`,
});
return response;
},
fetchContacts: async (
auth: CLICKFUNNELS_APIKEY_AUTH,
workspaceId: string
) => {
const response = await fireHttpRequest({
method: HttpMethod.GET,
auth,
path: `${API_ENDPOINTS.WORKSPACES}/${workspaceId}${API_ENDPOINTS.CONTACTS}`,
});
return response;
},
fetchContactByEmailSearch: async (
auth: CLICKFUNNELS_APIKEY_AUTH,
workspaceId: string,
filterQuery: string
) => {
const isEmail = filterQuery.includes('@');
const filterParam = isEmail
? `filter[email_address]=${encodeURIComponent(filterQuery)}`
: `filter[id]=${encodeURIComponent(filterQuery)}`;
const response = await fireHttpRequest({
method: HttpMethod.GET,
auth,
path: `${API_ENDPOINTS.WORKSPACES}/${workspaceId}${API_ENDPOINTS.CONTACTS}?${filterParam}`,
});
return response;
},
upsertContact: async (
auth: CLICKFUNNELS_APIKEY_AUTH,
workspaceId: string,
payload: any
) => {
const response = await fireHttpRequest({
method: HttpMethod.POST,
auth,
path: `${API_ENDPOINTS.WORKSPACES}/${workspaceId}${API_ENDPOINTS.CONTACTS}/upsert`,
body: payload,
});
return response;
},
fetchTags: async (auth: CLICKFUNNELS_APIKEY_AUTH, workspaceId: string) => {
const response = await fireHttpRequest({
method: HttpMethod.GET,
auth,
path: `${API_ENDPOINTS.WORKSPACES}/${workspaceId}${API_ENDPOINTS.CONTACTS}${API_ENDPOINTS.TAGS}`,
});
return response;
},
fetchCourses: async (auth: CLICKFUNNELS_APIKEY_AUTH, workspaceId: string) => {
const response = await fireHttpRequest({
method: HttpMethod.GET,
auth,
path: `${API_ENDPOINTS.WORKSPACES}/${workspaceId}${API_ENDPOINTS.COURSES}`,
});
return response;
},
createCourseEnrollment: async (
auth: CLICKFUNNELS_APIKEY_AUTH,
courseId: string,
payload: any
) => {
const response = await fireHttpRequest({
method: HttpMethod.POST,
auth,
path: `${API_ENDPOINTS.COURSES}/${courseId}/enrollments`,
body: payload
});
return response;
},
fetchAppliedTags: async (
auth: CLICKFUNNELS_APIKEY_AUTH,
contactId: string
) => {
const response = await fireHttpRequest({
method: HttpMethod.GET,
auth,
path: `${API_ENDPOINTS.CONTACTS}/${contactId}/applied_tags`,
});
return response;
},
removeAppliedTags: async (auth: CLICKFUNNELS_APIKEY_AUTH, tagId: string) => {
const response = await fireHttpRequest({
method: HttpMethod.DELETE,
auth,
path: `${API_ENDPOINTS.CONTACTS}/applied_tags/${tagId}`,
});
return response;
},
fetchPipelines: async (
auth: CLICKFUNNELS_APIKEY_AUTH,
workspaceId: string
) => {
const response = await fireHttpRequest({
method: HttpMethod.GET,
auth,
path: `${API_ENDPOINTS.WORKSPACES}/${workspaceId}${API_ENDPOINTS.PIPELINES}`,
});
return response;
},
fetchPipelineStages: async (
auth: CLICKFUNNELS_APIKEY_AUTH,
pipelineId: string
) => {
const response = await fireHttpRequest({
method: HttpMethod.GET,
auth,
path: `${API_ENDPOINTS.PIPELINES}/${pipelineId}/stages`,
});
return response;
},
createOpportunity: async (
auth: CLICKFUNNELS_APIKEY_AUTH,
workspaceId: string,
payload: any
) => {
const response = await fireHttpRequest({
method: HttpMethod.POST,
auth,
path: `${API_ENDPOINTS.WORKSPACES}/${workspaceId}/sales/opportunities`,
body: payload,
});
return response;
},
createAppliedTag: async (
auth: CLICKFUNNELS_APIKEY_AUTH,
contactId: string,
payload: any
) => {
const response = await fireHttpRequest({
method: HttpMethod.POST,
auth,
path: `${API_ENDPOINTS.CONTACTS}/${contactId}/applied_tags`,
body: payload,
});
return response;
},
createWebhook: async (
auth: CLICKFUNNELS_APIKEY_AUTH,
workspaceId: string,
payload: any
) => {
const response = await fireHttpRequest({
method: HttpMethod.POST,
auth,
path: `${API_ENDPOINTS.WORKSPACES}/${workspaceId}/webhooks/outgoing/endpoints`,
body: payload,
});
return response;
},
deleteWebhook: async (auth: CLICKFUNNELS_APIKEY_AUTH, webhookId: string) => {
const response = await fireHttpRequest({
method: HttpMethod.DELETE,
auth,
path: `/webhooks/outgoing/endpoints/${webhookId}`,
});
return response;
},
};