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,18 @@
{
"Opnform": "Opnform",
"Create beautiful online forms and surveys with unlimited fields and submissions": "Créez de beaux formulaires et sondages en ligne avec des champs et soumissions illimités",
"Please use your Opnform API Key. [Click here for create API Key](https://opnform.com/home?user-settings=access-tokens)": "Veuillez utiliser votre clé API Opnform. [Cliquez ici pour créer une clé API](https://opnform.com/home?user-settings=access-tokens)",
"Base URL (Default: https://api.opnform.com)": "URL de base (Par défaut : https://api.opnform.com)",
"API Key": "Clé API",
"Invalid API Key": "Clé API invalide",
"New Submission": "Nouvelle soumission",
"Triggers when Opnform receives a new submission": "Se déclenche lorsqu'Opnform reçoit une nouvelle soumission",
"Workspace": "Espace de travail",
"Workspace Name": "Nom de l'espace de travail",
"Connect Opnform account": "Connecter le compte Opnform",
"Select workspace": "Sélectionner un espace de travail",
"Form": "Formulaire",
"Form Name": "Nom du formulaire",
"Select form": "Sélectionner un formulaire"
}

View File

@@ -0,0 +1,29 @@
{
"Create beautiful online forms and surveys with unlimited fields and submissions": "Create beautiful online forms and surveys with unlimited fields and submissions",
"Base URL (Default: https://api.opnform.com)": "Base URL (Default: https://api.opnform.com)",
"API Key": "API Key",
"Please use your Opnform API Key. [Click here for create API Key](https://opnform.com/home?user-settings=access-tokens)": "Please use your Opnform API Key. [Click here for create API Key](https://opnform.com/home?user-settings=access-tokens)",
"Custom API Call": "Custom API Call",
"Make a custom API call to a specific endpoint": "Make a custom API call to a specific endpoint",
"Method": "Method",
"Headers": "Headers",
"Query Parameters": "Query Parameters",
"Body": "Body",
"Response is Binary ?": "Response is Binary ?",
"No Error on Failure": "No Error on Failure",
"Timeout (in seconds)": "Timeout (in seconds)",
"Authorization headers are injected automatically from your connection.": "Authorization headers are injected automatically from your connection.",
"Enable for files like PDFs, images, etc..": "Enable for files like PDFs, images, etc..",
"GET": "GET",
"POST": "POST",
"PATCH": "PATCH",
"PUT": "PUT",
"DELETE": "DELETE",
"HEAD": "HEAD",
"New Submission": "New Submission",
"Triggers when Opnform receives a new submission.": "Triggers when Opnform receives a new submission.",
"Workspace": "Workspace",
"Form": "Form",
"Workspace Name": "Workspace Name",
"Form Name": "Form Name"
}

View File

@@ -0,0 +1,64 @@
import {
createPiece,
PieceAuth,
Property,
} from '@activepieces/pieces-framework';
import { PieceCategory } from '@activepieces/shared';
import { opnformNewSubmission } from './lib/triggers/new-submission';
import { API_URL_DEFAULT, opnformCommon } from './lib/common';
import { createCustomApiCallAction } from '@activepieces/pieces-common';
export const opnformAuth = PieceAuth.CustomAuth({
description:
'Please use your Opnform API Key. [Click here for create API Key](https://opnform.com/home?user-settings=access-tokens)',
required: true,
props: {
baseApiUrl: Property.ShortText({
displayName: `Base URL (Default: ${API_URL_DEFAULT})`,
required: false,
}),
apiKey: PieceAuth.SecretText({
displayName: 'API Key',
required: true,
}),
},
validate: async (
auth
): Promise<{ valid: true } | { valid: false; error: string }> => {
try {
const isValid = await opnformCommon.validateAuth(auth.auth);
if (isValid) {
return { valid: true };
}
return { valid: false, error: 'Invalid API Key' };
} catch (e) {
return { valid: false, error: 'Invalid API Key' };
}
},
});
export const opnform = createPiece({
displayName: 'Opnform',
description:
'Create beautiful online forms and surveys with unlimited fields and submissions',
auth: opnformAuth,
minimumSupportedRelease: '0.36.1',
logoUrl: 'https://cdn.activepieces.com/pieces/opnform.png',
categories: [PieceCategory.FORMS_AND_SURVEYS],
authors: ['JhumanJ', 'chiragchhatrala'],
actions: [
createCustomApiCallAction({
auth: opnformAuth,
baseUrl: (auth) => {
return opnformCommon.getBaseUrl(auth);
},
authMapping: async (auth) => {
return {
Authorization: `Bearer ${auth.props.apiKey}`,
};
},
}),
],
triggers: [opnformNewSubmission],
});

View File

@@ -0,0 +1,243 @@
import {
Property,
DropdownOption,
} from '@activepieces/pieces-framework';
import {
HttpRequest,
HttpMethod,
AuthenticationType,
httpClient,
} from '@activepieces/pieces-common';
import { opnformAuth } from '../../index';
export const API_URL_DEFAULT = 'https://api.opnform.com';
type WorkspaceListResponse = {
id: string;
name: string;
}[];
type FormListResponse = {
meta: {
current_page: number;
from: number;
last_page: number;
per_page: number;
to: number;
total: number;
};
data: {
id: string;
title: string;
slug?: string;
}[];
};
export const workspaceIdProp = Property.Dropdown<string,true,typeof opnformAuth>({
displayName: 'Workspace',
description: 'Workspace Name',
required: true,
refreshers: [],
auth: opnformAuth,
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect Opnform account',
options: [],
};
}
const accessToken = (auth as any).apiKey;
const options: DropdownOption<string>[] = [];
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${opnformCommon.getBaseUrl(auth)}/open/workspaces`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
}
};
const response = await httpClient.sendRequest<WorkspaceListResponse>(request);
for (const workspace of response.body) {
options.push({ label: workspace.name, value: workspace.id });
}
return {
disabled: false,
placeholder: 'Select workspace',
options,
};
},
});
export const formIdProp = Property.Dropdown<string,true,typeof opnformAuth>({
displayName: 'Form',
description: 'Form Name',
required: true,
refreshers: ['workspaceId'],
auth: opnformAuth,
async options({ auth, workspaceId }) {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect Opnform account',
options: [],
};
}
if (!workspaceId) {
return {
disabled: true,
placeholder: 'Select workspace',
options: [],
};
}
const accessToken = (auth as any).apiKey;
const options: DropdownOption<string>[] = [];
let hasMore = true;
let page = 1;
do {
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${opnformCommon.getBaseUrl(auth)}/open/workspaces/${workspaceId}/forms`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
queryParams: {
page: page.toString(),
},
};
const response = await httpClient.sendRequest<FormListResponse>(request);
for (const form of response.body.data) {
options.push({ label: form.title, value: form.id });
}
hasMore =
response.body.meta != undefined &&
response.body.meta.current_page < response.body.meta.last_page;
page++;
} while (hasMore);
return {
disabled: false,
placeholder: 'Select form',
options,
};
},
});
export const opnformCommon = {
getBaseUrl: (auth: any) => {
return auth.baseApiUrl || API_URL_DEFAULT;
},
validateAuth: async (auth: any) => {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${opnformCommon.getBaseUrl(auth)}/open/workspaces`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: (auth as any).apiKey,
},
});
return response.status === 200;
},
checkExistsIntegration: async (
auth: any,
formId: string,
flowUrl: string,
) => {
// Fetch all integrations for this form
const allIntegrations = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${opnformCommon.getBaseUrl(auth)}/open/forms/${formId}/integrations`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: (auth as any).apiKey,
},
});
const integration = allIntegrations.body.find((integration: any) =>
integration.integration_id === 'activepieces' && integration.data?.provider_url === flowUrl
);
return integration || null;
},
createIntegration: async (
auth: any,
formId: string,
webhookUrl: string,
flowUrl: string,
) => {
const request: HttpRequest = {
method: HttpMethod.POST,
url: `${opnformCommon.getBaseUrl(auth)}/open/forms/${formId}/integrations`,
headers: {
'Content-Type': 'application/json',
},
body: {
'integration_id': 'activepieces',
'status': 'active',
'data': {
'webhook_url': webhookUrl,
'provider_url': flowUrl
}
},
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: (auth as any).apiKey,
},
queryParams: {},
};
const response = await httpClient.sendRequest(request);
return (response as any)?.form_integration?.id as number || null;
},
createOrUpdateIntegration: async (
auth: any,
formId: string,
webhookUrl: string,
flowUrl: string,
) => {
// Check if the integration already exists
const existingIntegration = await opnformCommon.checkExistsIntegration(auth, formId, flowUrl);
if (existingIntegration) {
// If webhook URL matches, return existing ID
if (existingIntegration.data?.webhook_url === webhookUrl) {
return existingIntegration.id;
}
// If webhook URL differs (test -> production), delete and recreate
await opnformCommon.deleteIntegration(auth, formId, existingIntegration.id);
}
// Create new integration (or recreate after deletion)
return await opnformCommon.createIntegration(auth, formId, webhookUrl, flowUrl);
},
deleteIntegration: async (
auth: any,
formId: string,
integrationId: number,
) => {
const request: HttpRequest = {
method: HttpMethod.DELETE,
url: `${opnformCommon.getBaseUrl(auth)}/open/forms/${formId}/integrations/${integrationId}`,
headers: {
'Content-Type': 'application/json',
},
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: (auth as any).apiKey,
},
};
return await httpClient.sendRequest(request);
},
};

View File

@@ -0,0 +1,75 @@
import { opnformCommon, workspaceIdProp, formIdProp } from '../common';
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { opnformAuth } from '../..';
export const opnformNewSubmission = createTrigger({
auth: opnformAuth,
name: 'new_submission',
displayName: 'New Submission',
description: 'Triggers when Opnform receives a new submission.',
props: {
workspaceId: workspaceIdProp,
formId: formIdProp,
},
type: TriggerStrategy.WEBHOOK,
sampleData: {
form_title: "My Form",
form_slug: "my-form-vuep24",
data: {
"6cc0dcf4-0ca8-43e4-b31a-b3f3413f859a": {
value: "This is test",
name: "Name"
},
"6e171bce-3eab-47a4-a289-20a0cb8ec693": {
value: "abc@example.com",
name: "Email"
}
}
},
async onEnable(context) {
const formId = context.propsValue['formId'];
const webhookUrl = context.webhookUrl;
if (!formId) {
throw new Error('Form is required');
}
const flowUrl = `${new URL(context.server.publicUrl).origin}/projects/${context.project.id}/flows/${context.flows.current.id}`;
const integrationId = await opnformCommon.createOrUpdateIntegration(
context.auth,
formId,
webhookUrl,
flowUrl
);
if (integrationId) {
await context.store?.put<WebhookInformation>('_new_submission_trigger', {
integrationId: integrationId as number,
});
} else {
throw new Error('Failed to create integration');
}
},
async onDisable(context) {
const response = await context.store?.get<WebhookInformation>(
'_new_submission_trigger'
);
if (response !== null && response !== undefined && response.integrationId) {
const formId = context.propsValue['formId'];
if (!formId) {
throw new Error('Form is required');
}
await opnformCommon.deleteIntegration(
context.auth,
formId,
response.integrationId
);
}
},
async run(context) {
return [context.payload.body];
},
});
interface WebhookInformation {
integrationId: number;
}