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:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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],
|
||||
});
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user