From d366da4ac3ba60a4a24c6d8680b5f32414063264 Mon Sep 17 00:00:00 2001 From: cumonvip1 Date: Tue, 5 May 2026 10:50:54 +0200 Subject: [PATCH] feat(deftform): add Deftform piece with 7 actions and 1 polling trigger (#13037) Co-authored-by: David Anyatonwu <51977119+onyedikachi-david@users.noreply.github.com> Co-authored-by: David Anyatonwu --- bun.lock | 12 + .../pieces/community/deftform/.eslintrc.json | 9 + .../pieces/community/deftform/package.json | 16 ++ .../pieces/community/deftform/src/index.ts | 41 +++ .../src/lib/actions/add-form-response.ts | 34 +++ .../deftform/src/lib/actions/get-all-forms.ts | 43 +++ .../src/lib/actions/get-form-fields.ts | 36 +++ .../src/lib/actions/get-form-responses.ts | 38 +++ .../src/lib/actions/get-submission-pdf.ts | 30 ++ .../deftform/src/lib/actions/get-workspace.ts | 20 ++ .../src/lib/actions/update-form-settings.ts | 263 ++++++++++++++++++ .../pieces/community/deftform/src/lib/auth.ts | 33 +++ .../deftform/src/lib/common/index.ts | 100 +++++++ .../src/lib/triggers/new-form-response.ts | 40 +++ .../pieces/community/deftform/tsconfig.json | 15 + .../community/deftform/tsconfig.lib.json | 15 + tsconfig.base.json | 9 +- 17 files changed, 751 insertions(+), 3 deletions(-) create mode 100644 packages/pieces/community/deftform/.eslintrc.json create mode 100644 packages/pieces/community/deftform/package.json create mode 100644 packages/pieces/community/deftform/src/index.ts create mode 100644 packages/pieces/community/deftform/src/lib/actions/add-form-response.ts create mode 100644 packages/pieces/community/deftform/src/lib/actions/get-all-forms.ts create mode 100644 packages/pieces/community/deftform/src/lib/actions/get-form-fields.ts create mode 100644 packages/pieces/community/deftform/src/lib/actions/get-form-responses.ts create mode 100644 packages/pieces/community/deftform/src/lib/actions/get-submission-pdf.ts create mode 100644 packages/pieces/community/deftform/src/lib/actions/get-workspace.ts create mode 100644 packages/pieces/community/deftform/src/lib/actions/update-form-settings.ts create mode 100644 packages/pieces/community/deftform/src/lib/auth.ts create mode 100644 packages/pieces/community/deftform/src/lib/common/index.ts create mode 100644 packages/pieces/community/deftform/src/lib/triggers/new-form-response.ts create mode 100644 packages/pieces/community/deftform/tsconfig.json create mode 100644 packages/pieces/community/deftform/tsconfig.lib.json diff --git a/bun.lock b/bun.lock index 535c1aeeed4..16947ef7d6c 100644 --- a/bun.lock +++ b/bun.lock @@ -2094,6 +2094,16 @@ "zod": "4.3.6", }, }, + "packages/pieces/community/deftform": { + "name": "@activepieces/piece-deftform", + "version": "0.0.1", + "dependencies": { + "@activepieces/pieces-common": "workspace:*", + "@activepieces/pieces-framework": "workspace:*", + "@activepieces/shared": "workspace:*", + "tslib": "2.6.2", + }, + }, "packages/pieces/community/denser-ai": { "name": "@activepieces/piece-denser-ai", "version": "0.1.4", @@ -8728,6 +8738,8 @@ "@activepieces/piece-deepseek": ["@activepieces/piece-deepseek@workspace:packages/pieces/community/deepseek"], + "@activepieces/piece-deftform": ["@activepieces/piece-deftform@workspace:packages/pieces/community/deftform"], + "@activepieces/piece-delay": ["@activepieces/piece-delay@workspace:packages/pieces/core/delay"], "@activepieces/piece-denser-ai": ["@activepieces/piece-denser-ai@workspace:packages/pieces/community/denser-ai"], diff --git a/packages/pieces/community/deftform/.eslintrc.json b/packages/pieces/community/deftform/.eslintrc.json new file mode 100644 index 00000000000..a86bd8287d5 --- /dev/null +++ b/packages/pieces/community/deftform/.eslintrc.json @@ -0,0 +1,9 @@ +{ + "extends": ["../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], "rules": {} }, + { "files": ["*.ts", "*.tsx"], "rules": {} }, + { "files": ["*.js", "*.jsx"], "rules": {} } + ] +} diff --git a/packages/pieces/community/deftform/package.json b/packages/pieces/community/deftform/package.json new file mode 100644 index 00000000000..cd754aeb292 --- /dev/null +++ b/packages/pieces/community/deftform/package.json @@ -0,0 +1,16 @@ +{ + "name": "@activepieces/piece-deftform", + "version": "0.0.1", + "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "scripts": { + "build": "tsc -p tsconfig.lib.json && cp package.json dist/", + "lint": "eslint 'src/**/*.ts'" + }, + "dependencies": { + "@activepieces/pieces-common": "workspace:*", + "@activepieces/pieces-framework": "workspace:*", + "@activepieces/shared": "workspace:*", + "tslib": "2.6.2" + } +} diff --git a/packages/pieces/community/deftform/src/index.ts b/packages/pieces/community/deftform/src/index.ts new file mode 100644 index 00000000000..182de246854 --- /dev/null +++ b/packages/pieces/community/deftform/src/index.ts @@ -0,0 +1,41 @@ +import { createPiece } from '@activepieces/pieces-framework'; +import { createCustomApiCallAction } from '@activepieces/pieces-common'; +import { PieceCategory } from '@activepieces/shared'; +import { deftformAuth } from './lib/auth'; +import { getWorkspaceDetails } from './lib/actions/get-workspace'; +import { getAllForms } from './lib/actions/get-all-forms'; +import { getFormFields } from './lib/actions/get-form-fields'; +import { getFormResponses } from './lib/actions/get-form-responses'; +import { getSubmissionPdf } from './lib/actions/get-submission-pdf'; +import { addFormResponse } from './lib/actions/add-form-response'; +import { updateFormSettings } from './lib/actions/update-form-settings'; +import { newFormResponseTrigger } from './lib/triggers/new-form-response'; + +export { deftformAuth }; + +export const deftform = createPiece({ + displayName: 'Deftform', + description: 'Build powerful forms and automate your workflow with Deftform.', + minimumSupportedRelease: '0.36.1', + logoUrl: 'https://cdn.activepieces.com/pieces/deftform.png', + categories: [PieceCategory.FORMS_AND_SURVEYS], + auth: deftformAuth, + authors: ['cumonvip1'], + actions: [ + getWorkspaceDetails, + getAllForms, + getFormFields, + getFormResponses, + getSubmissionPdf, + addFormResponse, + updateFormSettings, + createCustomApiCallAction({ + baseUrl: () => 'https://deftform.com/api/v1', + auth: deftformAuth, + authMapping: async (auth) => ({ + Authorization: `Bearer ${auth}`, + }), + }), + ], + triggers: [newFormResponseTrigger], +}); diff --git a/packages/pieces/community/deftform/src/lib/actions/add-form-response.ts b/packages/pieces/community/deftform/src/lib/actions/add-form-response.ts new file mode 100644 index 00000000000..1811941aa61 --- /dev/null +++ b/packages/pieces/community/deftform/src/lib/actions/add-form-response.ts @@ -0,0 +1,34 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { deftformAuth } from '../auth'; +import { deftformApiCall, DeftformCommon } from '../common'; + +export const addFormResponse = createAction({ + auth: deftformAuth, + name: 'add_form_response', + displayName: 'Add Form Response', + description: `Submits a new response programmatically to a form. +**Note:** This does **not** trigger email notifications to the form admin, unlike regular public submissions.`, + props: { + formId: DeftformCommon.formDropdown, + responseData: Property.Object({ + displayName: 'Response Data', + description: 'Key-value pairs where each key is the field UUID and the value is the user input.', + required: true, + }), + }, + async run(context) { + const response = await deftformApiCall<{ data?: { id?: string; uuid?: string } }>({ + token: context.auth.secret_text, + method: HttpMethod.POST, + path: `/forms/${context.propsValue.formId}/response`, + body: { data: context.propsValue.responseData }, + }); + + return { + id: response.body.data?.id ?? null, + uuid: response.body.data?.uuid ?? null, + message: 'Response submitted successfully.', + }; + }, +}); diff --git a/packages/pieces/community/deftform/src/lib/actions/get-all-forms.ts b/packages/pieces/community/deftform/src/lib/actions/get-all-forms.ts new file mode 100644 index 00000000000..033e7cd3dd8 --- /dev/null +++ b/packages/pieces/community/deftform/src/lib/actions/get-all-forms.ts @@ -0,0 +1,43 @@ +import { createAction } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { deftformAuth } from '../auth'; +import { deftformApiCall } from '../common'; + +export const getAllForms = createAction({ + auth: deftformAuth, + name: 'get_all_forms', + displayName: 'Get All Forms and Fields', + description: 'Lists all forms in your workspace, including their fields.', + props: {}, + async run(context) { + const response = await deftformApiCall<{ data: unknown[] }>({ + token: context.auth.secret_text, + method: HttpMethod.GET, + path: '/forms', + }); + + return response.body.data.map((form) => { + const f = form as Record; + const fieldsRaw = Array.isArray(f['fields']) + ? (f['fields'] as Record[]) + : []; + return { + id: f['id'] ?? null, + name: f['name'] ?? null, + description: f['description'] ?? null, + is_closed: f['is_closed'] ?? null, + slug: f['slug'] ?? null, + created_at: f['created_at'] ?? null, + updated_at: f['updated_at'] ?? null, + fields_count: fieldsRaw.length, + fields: fieldsRaw.map((field) => ({ + id: field['id'] ?? null, + label: field['label'] ?? field['name'] ?? null, + type: field['type'] ?? null, + })), + responses_limit: f['responses_limit'] ?? null, + admin_email_subject: f['admin_email_subject'] ?? null, + }; + }); + }, +}); diff --git a/packages/pieces/community/deftform/src/lib/actions/get-form-fields.ts b/packages/pieces/community/deftform/src/lib/actions/get-form-fields.ts new file mode 100644 index 00000000000..e813e28565f --- /dev/null +++ b/packages/pieces/community/deftform/src/lib/actions/get-form-fields.ts @@ -0,0 +1,36 @@ +import { createAction } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { deftformAuth } from '../auth'; +import { deftformApiCall, DeftformCommon } from '../common'; + +export const getFormFields = createAction({ + auth: deftformAuth, + name: 'get_form_fields', + displayName: 'Get Form Fields', + description: 'Retrieves only the fields of a specific form. Great for understanding the structure before building automations.', + props: { + formId: DeftformCommon.formDropdown, + }, + async run(context) { + const response = await deftformApiCall<{ data: unknown[] }>({ + token: context.auth.secret_text, + method: HttpMethod.GET, + path: `/forms/${context.propsValue.formId}/fields`, + }); + + return response.body.data.map((field) => { + const f = field as Record; + return { + id: f['id'] ?? null, + name: f['name'] ?? null, + label: f['label'] ?? null, + type: f['type'] ?? null, + required: f['required'] ?? null, + options: Array.isArray(f['options']) ? f['options'] : null, + default_value: f['default_value'] ?? null, + placeholder: f['placeholder'] ?? null, + validation: f['validation'] ?? null, + }; + }); + }, +}); diff --git a/packages/pieces/community/deftform/src/lib/actions/get-form-responses.ts b/packages/pieces/community/deftform/src/lib/actions/get-form-responses.ts new file mode 100644 index 00000000000..4358fdc9eac --- /dev/null +++ b/packages/pieces/community/deftform/src/lib/actions/get-form-responses.ts @@ -0,0 +1,38 @@ +import { createAction } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { deftformAuth } from '../auth'; +import { deftformApiCall, DeftformCommon } from '../common'; + +export const getFormResponses = createAction({ + auth: deftformAuth, + name: 'get_form_responses', + displayName: 'Get Form Responses', + description: 'Retrieves all submissions (responses) for a specific form. Useful for reporting and data exports.', + props: { + formId: DeftformCommon.formDropdown, + }, + async run(context) { + const response = await deftformApiCall<{ data: unknown[] }>({ + token: context.auth.secret_text, + method: HttpMethod.GET, + path: `/responses/${context.propsValue.formId}`, + }); + + return response.body.data.map((item) => { + const i = item as Record; + return { + id: i['id'] ?? null, + uuid: i['uuid'] ?? null, + form_id: i['form_id'] ?? null, + created_at: i['created_at'] ?? null, + updated_at: i['updated_at'] ?? null, + ...Object.fromEntries( + Object.entries((i['fields'] as Record) || {}).map(([k, v]) => [ + `field_${k}`, + typeof v === 'object' ? JSON.stringify(v) : v, + ]), + ), + }; + }); + }, +}); diff --git a/packages/pieces/community/deftform/src/lib/actions/get-submission-pdf.ts b/packages/pieces/community/deftform/src/lib/actions/get-submission-pdf.ts new file mode 100644 index 00000000000..8aa68bc65b9 --- /dev/null +++ b/packages/pieces/community/deftform/src/lib/actions/get-submission-pdf.ts @@ -0,0 +1,30 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { deftformAuth } from '../auth'; +import { deftformApiCall } from '../common'; + +export const getSubmissionPdf = createAction({ + auth: deftformAuth, + name: 'get_submission_pdf', + displayName: 'Get Submission PDF', + description: 'Generates a PDF download link for a specific form submission by its unique response UUID.', + props: { + responseUuid: Property.ShortText({ + displayName: 'Response UUID', + description: 'The unique identifier of the submission. You can find this in the form responses list.', + required: true, + }), + }, + async run(context) { + const response = await deftformApiCall<{ data?: { pdf_url?: string; url?: string } }>({ + token: context.auth.secret_text, + method: HttpMethod.GET, + path: `/response/${context.propsValue.responseUuid}/pdf`, + }); + + return { + pdf_url: response.body.data?.pdf_url ?? response.body.data?.url ?? null, + response_uuid: context.propsValue.responseUuid, + }; + }, +}); diff --git a/packages/pieces/community/deftform/src/lib/actions/get-workspace.ts b/packages/pieces/community/deftform/src/lib/actions/get-workspace.ts new file mode 100644 index 00000000000..febc43f0755 --- /dev/null +++ b/packages/pieces/community/deftform/src/lib/actions/get-workspace.ts @@ -0,0 +1,20 @@ +import { createAction } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { deftformAuth } from '../auth'; +import { deftformApiCall, flattenObject } from '../common'; + +export const getWorkspaceDetails = createAction({ + auth: deftformAuth, + name: 'get_workspace_details', + displayName: 'Get Workspace Details', + description: 'Retrieves all information about your Deftform workspace. Useful as a connection test.', + props: {}, + async run(context) { + const response = await deftformApiCall>({ + token: context.auth.secret_text, + method: HttpMethod.GET, + path: '/workspace', + }); + return flattenObject(response.body); + }, +}); diff --git a/packages/pieces/community/deftform/src/lib/actions/update-form-settings.ts b/packages/pieces/community/deftform/src/lib/actions/update-form-settings.ts new file mode 100644 index 00000000000..91343f06d31 --- /dev/null +++ b/packages/pieces/community/deftform/src/lib/actions/update-form-settings.ts @@ -0,0 +1,263 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { deftformAuth } from '../auth'; +import { deftformApiCall, DeftformCommon } from '../common'; + +export const updateFormSettings = createAction({ + auth: deftformAuth, + name: 'update_form_settings', + displayName: 'Update Form Settings', + description: 'Updates any combination of form settings via a single call — from title and status to integrations, SEO, and security.', + props: { + formId: DeftformCommon.formDropdown, + + generalHeader: Property.MarkDown({ + value: '## General Settings', + }), + name: Property.ShortText({ + displayName: 'Form Name', + description: 'New display name of the form. Leave empty to keep the current value.', + required: false, + }), + description: Property.LongText({ + displayName: 'Form Description', + description: 'New description shown on the public form page.', + required: false, + }), + isClosed: Property.Checkbox({ + displayName: 'Close Form', + description: 'When checked, the form stops accepting new submissions.', + required: false, + }), + responsesLimit: Property.Number({ + displayName: 'Responses Limit', + description: 'Maximum number of submissions allowed. Leave empty for unlimited responses.', + required: false, + }), + + afterSubmissionHeader: Property.MarkDown({ + value: '## After-Submission Behaviour', + }), + afterMessage: Property.LongText({ + displayName: 'After-Submission Message', + description: 'Message shown on screen after the user submits the form.', + required: false, + }), + afterRedirectUrl: Property.ShortText({ + displayName: 'After-Submission Redirect URL', + description: 'URL to redirect the respondent to after submission (instead of showing a message).', + required: false, + }), + afterRedirectDelay: Property.Number({ + displayName: 'Redirect Delay (seconds)', + description: 'Number of seconds to wait before redirecting. Leave empty for instant redirect.', + required: false, + }), + ctaLabel: Property.ShortText({ + displayName: 'CTA Label', + description: 'Text on the primary button shown after submission (e.g. "Download").', + required: false, + }), + ctaLabelContinue: Property.ShortText({ + displayName: 'CTA Label Continue', + description: 'Text on the secondary button shown after submission (e.g. "Next").', + required: false, + }), + closedMessage: Property.LongText({ + displayName: 'Closed Form Message', + description: 'Message shown when a user tries to access a closed form.', + required: false, + }), + + adminEmailHeader: Property.MarkDown({ + value: '## Admin Notification Email', + }), + adminEmailSubject: Property.ShortText({ + displayName: 'Notification Email Subject', + description: 'Subject line of the email sent to admins on each new submission.', + required: false, + }), + adminEmailNote: Property.LongText({ + displayName: 'Notification Email Note', + description: 'Custom note added to the top of the admin notification email.', + required: false, + }), + adminEmailAttachPdf: Property.Checkbox({ + displayName: 'Attach Submission PDF', + description: 'Attach a PDF version of the submission to the admin email.', + required: false, + }), + includeResponseInEmail: Property.Checkbox({ + displayName: 'Include Response in Email', + description: 'Include the full response data in the admin notification body.', + required: false, + }), + afterMessageEmail: Property.LongText({ + displayName: 'Respondent Confirmation Email Message', + description: 'Message sent to the respondent in their confirmation email (if enabled).', + required: false, + }), + afterMessageEmailSubject: Property.ShortText({ + displayName: 'Respondent Confirmation Email Subject', + description: 'Subject line of the confirmation email sent to the respondent.', + required: false, + }), + + seoHeader: Property.MarkDown({ + value: '## SEO Settings', + }), + seoTitle: Property.ShortText({ + displayName: 'SEO Title', + description: 'Custom HTML title tag for the public form page.', + required: false, + }), + seoDescription: Property.ShortText({ + displayName: 'SEO Description', + description: 'Meta description for the public form page.', + required: false, + }), + seoAllowBots: Property.Checkbox({ + displayName: 'Allow Search-Engine Bots', + description: 'When checked, search engines may index the public form page.', + required: false, + }), + + integrationsHeader: Property.MarkDown({ + value: '## Integrations', + }), + discordEnabled: Property.Checkbox({ + displayName: 'Discord Notifications', + description: 'When checked, a notification is sent to the configured Discord channel on each submission.', + required: false, + }), + slackEnabled: Property.Checkbox({ + displayName: 'Slack Notifications', + description: 'When checked, a notification is sent to the configured Slack channel on each submission.', + required: false, + }), + googleSheetsEnabled: Property.Checkbox({ + displayName: 'Google Sheets Integration', + description: 'When checked, submissions are automatically synced to Google Sheets.', + required: false, + }), + hubspotEnabled: Property.Checkbox({ + displayName: 'HubSpot Integration', + description: 'When checked, submissions are pushed to HubSpot as contacts or deals.', + required: false, + }), + + securityHeader: Property.MarkDown({ + value: '## Security', + }), + captcha: Property.StaticDropdown({ + displayName: 'CAPTCHA', + description: 'Protection against spam bots.', + required: false, + options: { + options: [ + { label: 'ALTCHA', value: 'altcha' }, + { label: 'Turnstile (Cloudflare)', value: 'turnstile' }, + { label: 'reCAPTCHA', value: 'recaptcha' }, + { label: 'None', value: 'none' }, + ], + }, + }), + captureLocation: Property.Checkbox({ + displayName: 'Capture Location', + description: 'Record the respondent location (IP-based geolocation).', + required: false, + }), + + layoutHeader: Property.MarkDown({ + value: '## Layout & Display', + }), + showFormTitle: Property.Checkbox({ + displayName: 'Show Form Title', + description: 'Display the form title at the top of the public page.', + required: false, + }), + showMultipageProgress: Property.Checkbox({ + displayName: 'Show Multi-Page Progress', + description: 'Show a progress bar when the form has multiple pages.', + required: false, + }), + aiSummaryEnabled: Property.Checkbox({ + displayName: 'AI Summary', + description: 'Enable the AI-powered summary on the form dashboard.', + required: false, + }), + sendPdfToRespondent: Property.Checkbox({ + displayName: 'Send PDF to Respondent', + description: 'Attach a PDF version of the submission to the respondent confirmation email.', + required: false, + }), + + numberHeader: Property.MarkDown({ + value: '## Submission Numbering', + }), + numberPrefix: Property.ShortText({ + displayName: 'Number Prefix', + description: 'Text added before the submission number (e.g. "INV-").', + required: false, + }), + numberSuffix: Property.ShortText({ + displayName: 'Number Suffix', + description: 'Text added after the submission number (e.g. "-A").', + required: false, + }), + }, + async run(context) { + const body: Record = {}; + + if (context.propsValue.name !== undefined) body['name'] = context.propsValue.name; + if (context.propsValue.description !== undefined) body['description'] = context.propsValue.description; + if (context.propsValue.isClosed !== undefined) body['is_closed'] = context.propsValue.isClosed; + if (context.propsValue.responsesLimit !== undefined) body['responses_limit'] = context.propsValue.responsesLimit; + if (context.propsValue.afterMessage !== undefined) body['after_message'] = context.propsValue.afterMessage; + if (context.propsValue.afterRedirectUrl !== undefined) body['after_redirect_url'] = context.propsValue.afterRedirectUrl; + if (context.propsValue.afterRedirectDelay !== undefined) body['after_redirect_delay'] = context.propsValue.afterRedirectDelay; + if (context.propsValue.ctaLabel !== undefined) body['cta_label'] = context.propsValue.ctaLabel; + if (context.propsValue.ctaLabelContinue !== undefined) body['cta_label_continue'] = context.propsValue.ctaLabelContinue; + if (context.propsValue.closedMessage !== undefined) body['closed_message'] = context.propsValue.closedMessage; + if (context.propsValue.adminEmailSubject !== undefined) body['admin_email_subject'] = context.propsValue.adminEmailSubject; + if (context.propsValue.adminEmailNote !== undefined) body['admin_email_note'] = context.propsValue.adminEmailNote; + if (context.propsValue.adminEmailAttachPdf !== undefined) body['admin_email_attach_pdf'] = context.propsValue.adminEmailAttachPdf; + if (context.propsValue.includeResponseInEmail !== undefined) body['include_response_in_email'] = context.propsValue.includeResponseInEmail; + if (context.propsValue.afterMessageEmail !== undefined) body['after_message_email'] = context.propsValue.afterMessageEmail; + if (context.propsValue.afterMessageEmailSubject !== undefined) body['after_message_email_subject'] = context.propsValue.afterMessageEmailSubject; + if (context.propsValue.seoTitle !== undefined) body['seo_title'] = context.propsValue.seoTitle; + if (context.propsValue.seoDescription !== undefined) body['seo_description'] = context.propsValue.seoDescription; + if (context.propsValue.seoAllowBots !== undefined) body['seo_allow_bots'] = context.propsValue.seoAllowBots; + if (context.propsValue.discordEnabled !== undefined) body['discord_enabled'] = context.propsValue.discordEnabled; + if (context.propsValue.slackEnabled !== undefined) body['slack_enabled'] = context.propsValue.slackEnabled; + if (context.propsValue.googleSheetsEnabled !== undefined) body['google_sheets_enabled'] = context.propsValue.googleSheetsEnabled; + if (context.propsValue.hubspotEnabled !== undefined) body['hubspot_enabled'] = context.propsValue.hubspotEnabled; + if (context.propsValue.captcha !== undefined) body['captcha'] = context.propsValue.captcha; + if (context.propsValue.captureLocation !== undefined) body['capture_location'] = context.propsValue.captureLocation; + if (context.propsValue.showFormTitle !== undefined) body['show_formtitle'] = context.propsValue.showFormTitle; + if (context.propsValue.showMultipageProgress !== undefined) body['show_multipage_progress'] = context.propsValue.showMultipageProgress; + if (context.propsValue.aiSummaryEnabled !== undefined) body['ai_summary_enabled'] = context.propsValue.aiSummaryEnabled; + if (context.propsValue.sendPdfToRespondent !== undefined) body['send_pdf_to_respondent'] = context.propsValue.sendPdfToRespondent; + if (context.propsValue.numberPrefix !== undefined) body['number_prefix'] = context.propsValue.numberPrefix; + if (context.propsValue.numberSuffix !== undefined) body['number_suffix'] = context.propsValue.numberSuffix; + + const response = await deftformApiCall<{ + data?: { + id?: string; + name?: string; + updated_at?: string; + }; + }>({ + token: context.auth.secret_text, + method: HttpMethod.POST, + path: `/forms/${context.propsValue.formId}/settings`, + body, + }); + + return { + id: response.body.data?.id ?? null, + name: response.body.data?.name ?? null, + updated_at: response.body.data?.updated_at ?? null, + }; + }, +}); diff --git a/packages/pieces/community/deftform/src/lib/auth.ts b/packages/pieces/community/deftform/src/lib/auth.ts new file mode 100644 index 00000000000..d8d657be21f --- /dev/null +++ b/packages/pieces/community/deftform/src/lib/auth.ts @@ -0,0 +1,33 @@ +import { PieceAuth } from '@activepieces/pieces-framework'; +import { httpClient, HttpMethod, AuthenticationType } from '@activepieces/pieces-common'; + +export const deftformAuth = PieceAuth.SecretText({ + displayName: 'API Key', + description: `To get your Deftform API key: +1. Log in to your Deftform workspace at https://deftform.com +2. Go to **Settings > API** (https://deftform.com/settings/api) +3. Click **Create API Key** +4. Copy the key and paste it here + +Need help? See https://deftform.com/docs/api`, + required: true, + validate: async ({ auth }) => { + try { + await httpClient.sendRequest({ + method: HttpMethod.GET, + url: 'https://deftform.com/api/v1/workspace', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token: auth, + }, + }); + return { valid: true }; + } catch (e) { + return { valid: false, error: 'Invalid API Key. Make sure you copied the full key from Deftform.' }; + } + }, +}); diff --git a/packages/pieces/community/deftform/src/lib/common/index.ts b/packages/pieces/community/deftform/src/lib/common/index.ts new file mode 100644 index 00000000000..f4ba72d59a4 --- /dev/null +++ b/packages/pieces/community/deftform/src/lib/common/index.ts @@ -0,0 +1,100 @@ +import { + httpClient, + HttpMethod, + AuthenticationType, + HttpMessageBody, + HttpResponse, +} from '@activepieces/pieces-common'; +import { Property } from '@activepieces/pieces-framework'; +import { deftformAuth } from '../auth'; + +const BASE_URL = 'https://deftform.com/api/v1'; + +export async function deftformApiCall({ + token, + method, + path, + body, + queryParams, +}: { + token: string; + method: HttpMethod; + path: string; + body?: unknown; + queryParams?: Record; +}): Promise> { + return await httpClient.sendRequest({ + method, + url: `${BASE_URL}${path}`, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + authentication: { + type: AuthenticationType.BEARER_TOKEN, + token, + }, + queryParams, + body, + }); +} + +export function getApiToken(context: { auth: { secret_text: string } }): string { + return context.auth.secret_text; +} + +export function flattenObject( + obj: Record, + prefix = '', +): Record { + const result: Record = {}; + + for (const [key, value] of Object.entries(obj)) { + const flatKey = prefix ? `${prefix}_${key}` : key; + + if (value === null || value === undefined) { + result[flatKey] = null; + } else if (Array.isArray(value)) { + result[flatKey] = value + .map((v) => (typeof v === 'object' ? JSON.stringify(v) : String(v))) + .join(', '); + } else if (typeof value === 'object') { + Object.assign(result, flattenObject(value as Record, flatKey)); + } else { + result[flatKey] = value; + } + } + + return result; +} + +export const DeftformCommon = { + formDropdown: Property.Dropdown({ + displayName: 'Form', + description: 'Select the form to use', + refreshers: [], + required: true, + auth: deftformAuth, + options: async ({ auth }) => { + if (!auth) { + return { disabled: true, options: [], placeholder: 'Please connect your account first' }; + } + try { + const response = await deftformApiCall<{ data: { id: string; name: string }[] }>({ + token: (auth as { secret_text: string }).secret_text, + method: HttpMethod.GET, + path: '/forms', + }); + return { + disabled: false, + options: response.body.data.map((f) => ({ + label: f.name, + value: f.id, + })), + }; + } catch (error) { + return { disabled: true, options: [], placeholder: 'Failed to load forms. Check your connection.' }; + } + }, + }), +}; diff --git a/packages/pieces/community/deftform/src/lib/triggers/new-form-response.ts b/packages/pieces/community/deftform/src/lib/triggers/new-form-response.ts new file mode 100644 index 00000000000..74c2d347d23 --- /dev/null +++ b/packages/pieces/community/deftform/src/lib/triggers/new-form-response.ts @@ -0,0 +1,40 @@ +import { createTrigger, TriggerStrategy, Property } from '@activepieces/pieces-framework'; +import { deftformAuth } from '../auth'; + +const setupMarkdown = ` +**Setup instructions:** +1. In your Deftform workspace go to **Settings → Notifications / Connections**. +2. Under **Webhook**, click **ADD ENDPOINT** and paste the URL below: +\`\`\` +{{webhookUrl}} +\`\`\` +3. Save the endpoint, then open the form you want to watch, go to its **Notifications / Connections** tab, and enable the webhook you just created. +`; + +export const newFormResponseTrigger = createTrigger({ + auth: deftformAuth, + name: 'new_form_response', + displayName: 'New Form Response', + description: 'Triggers instantly when a new response is submitted to a Deftform form.', + props: { + instructions: Property.MarkDown({ value: setupMarkdown }), + }, + sampleData: { + data: [ + [ + { label: 'Full name', response: 'John Doe', uuid: '6403fc2b-6d52-4231-b63f-db6ea9f651dd', custom_key: 'full_name' }, + { label: 'Email address', response: 'john@example.com', uuid: '6403fc2b-6d52-4231-b63f-db6ea9f651de', custom_key: 'email_address' }, + ], + ], + }, + type: TriggerStrategy.WEBHOOK, + async onEnable(_context) { + // Webhook URL is pasted manually into Deftform workspace settings. + }, + async onDisable(_context) { + // Nothing to unregister. + }, + async run(context) { + return [context.payload.body]; + }, +}); diff --git a/packages/pieces/community/deftform/tsconfig.json b/packages/pieces/community/deftform/tsconfig.json new file mode 100644 index 00000000000..71bc5814f5d --- /dev/null +++ b/packages/pieces/community/deftform/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [{ "path": "./tsconfig.lib.json" }] +} diff --git a/packages/pieces/community/deftform/tsconfig.lib.json b/packages/pieces/community/deftform/tsconfig.lib.json new file mode 100644 index 00000000000..d177359da22 --- /dev/null +++ b/packages/pieces/community/deftform/tsconfig.lib.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "baseUrl": ".", + "paths": {}, + "outDir": "./dist", + "declaration": true, + "declarationMap": true, + "module": "commonjs", + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 66b0f9a55f4..8904faa9ec3 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -361,6 +361,9 @@ "@activepieces/piece-deepseek": [ "packages/pieces/community/deepseek/src/index.ts" ], + "@activepieces/piece-deftform": [ + "packages/pieces/community/deftform/src/index.ts" + ], "@activepieces/piece-delay": ["packages/pieces/core/delay/src/index.ts"], "@activepieces/piece-devin": [ "packages/pieces/community/devin/src/index.ts" @@ -545,6 +548,9 @@ "@activepieces/piece-gotify": [ "packages/pieces/community/gotify/src/index.ts" ], + "@activepieces/piece-greenhouse": [ + "packages/pieces/community/greenhouse/src/index.ts" + ], "@activepieces/piece-granola": [ "packages/pieces/community/granola/src/index.ts" ], @@ -554,9 +560,6 @@ "@activepieces/piece-gravityforms": [ "packages/pieces/community/gravityforms/src/index.ts" ], - "@activepieces/piece-greenhouse": [ - "packages/pieces/community/greenhouse/src/index.ts" - ], "@activepieces/piece-grist": [ "packages/pieces/community/grist/src/index.ts" ],