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,29 @@
|
||||
import { createPiece } from "@activepieces/pieces-framework";
|
||||
import { PieceCategory } from "@activepieces/shared";
|
||||
import { featheryAuth } from "./lib/common/auth";
|
||||
import { createFormAction } from "./lib/actions/create-form";
|
||||
import { updateFormAction } from "./lib/actions/update-form";
|
||||
import { deleteFormAction } from "./lib/actions/delete-form";
|
||||
import { listFormSubmissionsAction } from "./lib/actions/list-form-submissions";
|
||||
import { exportSubmissionPdfAction } from "./lib/actions/export-submission-pdf";
|
||||
import { newSubmissionTrigger } from "./lib/triggers/new-submission";
|
||||
import { formCompletedTrigger } from "./lib/triggers/form-completed";
|
||||
import { fileSubmittedTrigger } from "./lib/triggers/file-submitted";
|
||||
|
||||
export const feathery = createPiece({
|
||||
displayName: "Feathery",
|
||||
description: "Build powerful forms, workflows, and document automation.",
|
||||
auth: featheryAuth,
|
||||
minimumSupportedRelease: '0.36.1',
|
||||
logoUrl: "https://cdn.activepieces.com/pieces/feathery.png",
|
||||
categories: [PieceCategory.FORMS_AND_SURVEYS],
|
||||
authors: ["onyedikachi-david"],
|
||||
actions: [
|
||||
createFormAction,
|
||||
updateFormAction,
|
||||
deleteFormAction,
|
||||
listFormSubmissionsAction,
|
||||
exportSubmissionPdfAction,
|
||||
],
|
||||
triggers: [newSubmissionTrigger, formCompletedTrigger, fileSubmittedTrigger],
|
||||
});
|
||||
@@ -0,0 +1,633 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { featheryAuth } from '../common/auth';
|
||||
import { featheryCommon } from '../common/client';
|
||||
|
||||
export const createFormAction = createAction({
|
||||
auth: featheryAuth,
|
||||
name: 'create_form',
|
||||
displayName: 'Create Form',
|
||||
description: 'Create a form based on an existing template form.',
|
||||
props: {
|
||||
form_name: Property.ShortText({
|
||||
displayName: 'Form Name',
|
||||
description: 'The name of the new form (must be unique).',
|
||||
required: true,
|
||||
}),
|
||||
template_form_id: Property.Dropdown({
|
||||
displayName: 'Template Form',
|
||||
description: 'Select the template form to copy from.',
|
||||
required: true,
|
||||
refreshers: [],
|
||||
auth: featheryAuth,
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Connect your account first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
const forms = await featheryCommon.apiCall<
|
||||
Array<{ id: string; name: string; active: boolean }>
|
||||
>({
|
||||
method: HttpMethod.GET,
|
||||
url: '/form/',
|
||||
apiKey: auth.secret_text,
|
||||
});
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
options: forms.map((form) => ({
|
||||
label: form.name,
|
||||
value: form.id,
|
||||
})),
|
||||
};
|
||||
},
|
||||
}),
|
||||
enabled: Property.Checkbox({
|
||||
displayName: 'Enabled',
|
||||
description: 'Whether the created form should be enabled. If not set, inherits from template.',
|
||||
required: false,
|
||||
}),
|
||||
steps: Property.Array({
|
||||
displayName: 'Steps',
|
||||
description: 'Define the steps for your form.',
|
||||
required: true,
|
||||
properties: {
|
||||
step_id: Property.ShortText({
|
||||
displayName: 'Step ID',
|
||||
description: 'Unique ID for this step.',
|
||||
required: true,
|
||||
}),
|
||||
template_step_id: Property.ShortText({
|
||||
displayName: 'Template Step ID',
|
||||
description: 'ID of the step to copy from the template. Leave empty to auto-create.',
|
||||
required: false,
|
||||
}),
|
||||
origin: Property.Checkbox({
|
||||
displayName: 'Is First Step',
|
||||
description: 'Set to true if this is the first step of the form.',
|
||||
required: false,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
step_images: Property.Array({
|
||||
displayName: 'Step Images',
|
||||
description: 'Edit image elements on steps.',
|
||||
required: false,
|
||||
properties: {
|
||||
step_id: Property.ShortText({
|
||||
displayName: 'Step ID',
|
||||
description: 'The step containing this image.',
|
||||
required: true,
|
||||
}),
|
||||
id: Property.ShortText({
|
||||
displayName: 'Image Element ID',
|
||||
description: 'ID of the image element to edit.',
|
||||
required: true,
|
||||
}),
|
||||
source_url: Property.ShortText({
|
||||
displayName: 'Image URL',
|
||||
description: 'New image URL.',
|
||||
required: false,
|
||||
}),
|
||||
asset: Property.ShortText({
|
||||
displayName: 'Asset Name',
|
||||
description: 'Name of the image asset from the template theme.',
|
||||
required: false,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
step_videos: Property.Array({
|
||||
displayName: 'Step Videos',
|
||||
description: 'Edit video elements on steps.',
|
||||
required: false,
|
||||
properties: {
|
||||
step_id: Property.ShortText({
|
||||
displayName: 'Step ID',
|
||||
description: 'The step containing this video.',
|
||||
required: true,
|
||||
}),
|
||||
id: Property.ShortText({
|
||||
displayName: 'Video Element ID',
|
||||
description: 'ID of the video element to edit.',
|
||||
required: true,
|
||||
}),
|
||||
source_url: Property.ShortText({
|
||||
displayName: 'Video URL',
|
||||
description: 'New video URL.',
|
||||
required: false,
|
||||
}),
|
||||
asset: Property.ShortText({
|
||||
displayName: 'Asset Name',
|
||||
description: 'Name of the video asset from the template theme.',
|
||||
required: false,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
step_texts: Property.Array({
|
||||
displayName: 'Step Texts',
|
||||
description: 'Edit text elements on steps.',
|
||||
required: false,
|
||||
properties: {
|
||||
step_id: Property.ShortText({
|
||||
displayName: 'Step ID',
|
||||
description: 'The step containing this text element.',
|
||||
required: true,
|
||||
}),
|
||||
id: Property.ShortText({
|
||||
displayName: 'Text Element ID',
|
||||
description: 'ID of the text element to edit.',
|
||||
required: true,
|
||||
}),
|
||||
text: Property.LongText({
|
||||
displayName: 'Text',
|
||||
description: 'New text to display.',
|
||||
required: false,
|
||||
}),
|
||||
asset: Property.ShortText({
|
||||
displayName: 'Asset Name',
|
||||
description: 'Name of the text asset from the template theme.',
|
||||
required: false,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
step_buttons: Property.Array({
|
||||
displayName: 'Step Buttons',
|
||||
description: 'Edit button elements on steps.',
|
||||
required: false,
|
||||
properties: {
|
||||
step_id: Property.ShortText({
|
||||
displayName: 'Step ID',
|
||||
description: 'The step containing this button.',
|
||||
required: true,
|
||||
}),
|
||||
id: Property.ShortText({
|
||||
displayName: 'Button Element ID',
|
||||
description: 'ID of the button to edit.',
|
||||
required: true,
|
||||
}),
|
||||
text: Property.ShortText({
|
||||
displayName: 'Button Text',
|
||||
description: 'Text to display on the button.',
|
||||
required: false,
|
||||
}),
|
||||
asset: Property.ShortText({
|
||||
displayName: 'Asset Name',
|
||||
description: 'Name of the button asset from the template theme.',
|
||||
required: false,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
step_progress_bars: Property.Array({
|
||||
displayName: 'Step Progress Bars',
|
||||
description: 'Edit progress bar elements on steps.',
|
||||
required: false,
|
||||
properties: {
|
||||
step_id: Property.ShortText({
|
||||
displayName: 'Step ID',
|
||||
description: 'The step containing this progress bar.',
|
||||
required: true,
|
||||
}),
|
||||
id: Property.ShortText({
|
||||
displayName: 'Progress Bar ID',
|
||||
description: 'ID of the progress bar to edit.',
|
||||
required: true,
|
||||
}),
|
||||
progress: Property.Number({
|
||||
displayName: 'Progress',
|
||||
description: 'Progress percentage (0-100).',
|
||||
required: false,
|
||||
}),
|
||||
asset: Property.ShortText({
|
||||
displayName: 'Asset Name',
|
||||
description: 'Name of the progress bar asset from the template theme.',
|
||||
required: false,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
step_fields: Property.Array({
|
||||
displayName: 'Step Fields',
|
||||
description: 'Edit field elements on steps.',
|
||||
required: false,
|
||||
properties: {
|
||||
step_id: Property.ShortText({
|
||||
displayName: 'Step ID',
|
||||
description: 'The step containing this field.',
|
||||
required: true,
|
||||
}),
|
||||
id: Property.ShortText({
|
||||
displayName: 'Field Element ID',
|
||||
description: 'ID of the field to copy.',
|
||||
required: true,
|
||||
}),
|
||||
field_id: Property.ShortText({
|
||||
displayName: 'New Field ID',
|
||||
description: 'ID to set for the field. Links to existing field data if ID exists.',
|
||||
required: false,
|
||||
}),
|
||||
type: Property.StaticDropdown({
|
||||
displayName: 'Field Type',
|
||||
description: 'Type of the field.',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Text Field', value: 'text_field' },
|
||||
{ label: 'Text Area', value: 'text_area' },
|
||||
{ label: 'Integer Field', value: 'integer_field' },
|
||||
{ label: 'Email', value: 'email' },
|
||||
{ label: 'Phone Number', value: 'phone_number' },
|
||||
{ label: 'Dropdown', value: 'dropdown' },
|
||||
{ label: 'Select', value: 'select' },
|
||||
{ label: 'Multiselect', value: 'multiselect' },
|
||||
{ label: 'Checkbox', value: 'checkbox' },
|
||||
{ label: 'Date', value: 'date' },
|
||||
{ label: 'File Upload', value: 'file_upload' },
|
||||
{ label: 'Button Group', value: 'button_group' },
|
||||
{ label: 'URL', value: 'url' },
|
||||
{ label: 'SSN', value: 'ssn' },
|
||||
{ label: 'Pin Input', value: 'pin_input' },
|
||||
{ label: 'Hex Color', value: 'hex_color' },
|
||||
{ label: 'Login', value: 'login' },
|
||||
{ label: 'Google Maps Line 1', value: 'gmap_line_1' },
|
||||
{ label: 'Google Maps Line 2', value: 'gmap_line_2' },
|
||||
{ label: 'Google Maps City', value: 'gmap_city' },
|
||||
{ label: 'Google Maps State', value: 'gmap_state' },
|
||||
{ label: 'Google Maps Zip', value: 'gmap_zip' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
description: Property.ShortText({
|
||||
displayName: 'Label',
|
||||
description: 'Label/description of the field.',
|
||||
required: false,
|
||||
}),
|
||||
required: Property.Checkbox({
|
||||
displayName: 'Required',
|
||||
description: 'Whether the field is required.',
|
||||
required: false,
|
||||
}),
|
||||
placeholder: Property.ShortText({
|
||||
displayName: 'Placeholder',
|
||||
description: 'Placeholder text for the field.',
|
||||
required: false,
|
||||
}),
|
||||
tooltipText: Property.ShortText({
|
||||
displayName: 'Tooltip Text',
|
||||
description: 'Tooltip text for the field.',
|
||||
required: false,
|
||||
}),
|
||||
max_length: Property.Number({
|
||||
displayName: 'Max Length',
|
||||
description: 'Maximum length of the field value.',
|
||||
required: false,
|
||||
}),
|
||||
min_length: Property.Number({
|
||||
displayName: 'Min Length',
|
||||
description: 'Minimum length of the field value.',
|
||||
required: false,
|
||||
}),
|
||||
submit_trigger: Property.StaticDropdown({
|
||||
displayName: 'Submit Trigger',
|
||||
description: 'Does filling out this field trigger step submission?',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'None', value: 'none' },
|
||||
{ label: 'Auto', value: 'auto' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
asset: Property.ShortText({
|
||||
displayName: 'Asset Name',
|
||||
description: 'Name of the field asset from the template theme.',
|
||||
required: false,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
navigation_rules: Property.Array({
|
||||
displayName: 'Navigation Rules',
|
||||
description: 'Define how users navigate between steps.',
|
||||
required: false,
|
||||
properties: {
|
||||
previous_step_id: Property.ShortText({
|
||||
displayName: 'From Step ID',
|
||||
description: 'The step the user is coming from.',
|
||||
required: true,
|
||||
}),
|
||||
next_step_id: Property.ShortText({
|
||||
displayName: 'To Step ID',
|
||||
description: 'The step the user is going to.',
|
||||
required: true,
|
||||
}),
|
||||
trigger: Property.StaticDropdown({
|
||||
displayName: 'Trigger',
|
||||
description: 'How navigation is triggered.',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Click', value: 'click' },
|
||||
{ label: 'Change', value: 'change' },
|
||||
{ label: 'Load', value: 'load' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
element_type: Property.StaticDropdown({
|
||||
displayName: 'Element Type',
|
||||
description: 'Type of element that triggers navigation.',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Button', value: 'button' },
|
||||
{ label: 'Field', value: 'field' },
|
||||
{ label: 'Text', value: 'text' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
element_id: Property.ShortText({
|
||||
displayName: 'Element ID',
|
||||
description: 'ID of the element that triggers navigation.',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
navigation_conditions: Property.Array({
|
||||
displayName: 'Navigation Conditions',
|
||||
description: 'Conditions for navigation rules. Reference the navigation rule by its index (0-based).',
|
||||
required: false,
|
||||
properties: {
|
||||
rule_index: Property.Number({
|
||||
displayName: 'Rule Index',
|
||||
description: 'Index of the navigation rule (0 for first rule, 1 for second, etc.).',
|
||||
required: true,
|
||||
}),
|
||||
comparison: Property.StaticDropdown({
|
||||
displayName: 'Comparison',
|
||||
description: 'Type of comparison.',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Equal', value: 'equal' },
|
||||
{ label: 'Not Equal', value: 'not_equal' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
field_key: Property.ShortText({
|
||||
displayName: 'Field Key',
|
||||
description: 'ID of the field to compare.',
|
||||
required: true,
|
||||
}),
|
||||
value: Property.ShortText({
|
||||
displayName: 'Value',
|
||||
description: 'Value to compare against.',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
logic_rules: Property.Array({
|
||||
displayName: 'Logic Rules',
|
||||
description: 'Advanced logic rules for the form.',
|
||||
required: false,
|
||||
properties: {
|
||||
name: Property.ShortText({
|
||||
displayName: 'Rule Name',
|
||||
description: 'Name of the logic rule.',
|
||||
required: true,
|
||||
}),
|
||||
code: Property.LongText({
|
||||
displayName: 'JavaScript Code',
|
||||
description: 'JavaScript code to run.',
|
||||
required: true,
|
||||
}),
|
||||
trigger_event: Property.StaticDropdown({
|
||||
displayName: 'Trigger Event',
|
||||
description: 'Event that triggers the rule.',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Change', value: 'change' },
|
||||
{ label: 'Load', value: 'load' },
|
||||
{ label: 'Form Complete', value: 'form_complete' },
|
||||
{ label: 'Submit', value: 'submit' },
|
||||
{ label: 'Error', value: 'error' },
|
||||
{ label: 'View', value: 'view' },
|
||||
{ label: 'Action', value: 'action' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
description: Property.ShortText({
|
||||
displayName: 'Description',
|
||||
description: 'Description of the logic rule.',
|
||||
required: false,
|
||||
}),
|
||||
index: Property.Number({
|
||||
displayName: 'Execution Order',
|
||||
description: 'Order in which this rule executes.',
|
||||
required: false,
|
||||
}),
|
||||
steps_csv: Property.ShortText({
|
||||
displayName: 'Steps (comma-separated)',
|
||||
description: 'Step IDs that trigger the rule (for submit/load events). Separate with commas.',
|
||||
required: false,
|
||||
}),
|
||||
elements_csv: Property.ShortText({
|
||||
displayName: 'Elements (comma-separated)',
|
||||
description: 'Element IDs that trigger the rule (for change/error/view/action events). Separate with commas.',
|
||||
required: false,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const {
|
||||
form_name,
|
||||
template_form_id,
|
||||
enabled,
|
||||
steps,
|
||||
step_images,
|
||||
step_videos,
|
||||
step_texts,
|
||||
step_buttons,
|
||||
step_progress_bars,
|
||||
step_fields,
|
||||
navigation_rules,
|
||||
navigation_conditions,
|
||||
logic_rules,
|
||||
} = context.propsValue;
|
||||
|
||||
const stepsMap = new Map<string, any>();
|
||||
|
||||
for (const step of steps as any[]) {
|
||||
stepsMap.set(step.step_id, {
|
||||
step_id: step.step_id,
|
||||
...(step.template_step_id && { template_step_id: step.template_step_id }),
|
||||
...(step.origin && { origin: step.origin }),
|
||||
images: [],
|
||||
videos: [],
|
||||
texts: [],
|
||||
buttons: [],
|
||||
progress_bars: [],
|
||||
fields: [],
|
||||
});
|
||||
}
|
||||
|
||||
if (step_images && Array.isArray(step_images)) {
|
||||
for (const img of step_images as any[]) {
|
||||
const step = stepsMap.get(img.step_id);
|
||||
if (step) {
|
||||
step.images.push({
|
||||
id: img.id,
|
||||
...(img.source_url && { source_url: img.source_url }),
|
||||
...(img.asset && { asset: img.asset }),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (step_videos && Array.isArray(step_videos)) {
|
||||
for (const video of step_videos as any[]) {
|
||||
const step = stepsMap.get(video.step_id);
|
||||
if (step) {
|
||||
step.videos.push({
|
||||
id: video.id,
|
||||
...(video.source_url && { source_url: video.source_url }),
|
||||
...(video.asset && { asset: video.asset }),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (step_texts && Array.isArray(step_texts)) {
|
||||
for (const txt of step_texts as any[]) {
|
||||
const step = stepsMap.get(txt.step_id);
|
||||
if (step) {
|
||||
step.texts.push({
|
||||
id: txt.id,
|
||||
...(txt.text && { text: txt.text }),
|
||||
...(txt.asset && { asset: txt.asset }),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (step_buttons && Array.isArray(step_buttons)) {
|
||||
for (const btn of step_buttons as any[]) {
|
||||
const step = stepsMap.get(btn.step_id);
|
||||
if (step) {
|
||||
step.buttons.push({
|
||||
id: btn.id,
|
||||
...(btn.text && { text: btn.text }),
|
||||
...(btn.asset && { asset: btn.asset }),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (step_progress_bars && Array.isArray(step_progress_bars)) {
|
||||
for (const pb of step_progress_bars as any[]) {
|
||||
const step = stepsMap.get(pb.step_id);
|
||||
if (step) {
|
||||
step.progress_bars.push({
|
||||
id: pb.id,
|
||||
...(pb.progress !== undefined && { progress: pb.progress }),
|
||||
...(pb.asset && { asset: pb.asset }),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (step_fields && Array.isArray(step_fields)) {
|
||||
for (const field of step_fields as any[]) {
|
||||
const step = stepsMap.get(field.step_id);
|
||||
if (step) {
|
||||
step.fields.push({
|
||||
id: field.id,
|
||||
...(field.field_id && { field_id: field.field_id }),
|
||||
...(field.type && { type: field.type }),
|
||||
...(field.description && { description: field.description }),
|
||||
...(field.required !== undefined && { required: field.required }),
|
||||
...(field.placeholder && { placeholder: field.placeholder }),
|
||||
...(field.tooltipText && { tooltipText: field.tooltipText }),
|
||||
...(field.max_length !== undefined && { max_length: field.max_length }),
|
||||
...(field.min_length !== undefined && { min_length: field.min_length }),
|
||||
...(field.submit_trigger && { submit_trigger: field.submit_trigger }),
|
||||
...(field.asset && { asset: field.asset }),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const navRulesArray: any[] = [];
|
||||
if (navigation_rules && Array.isArray(navigation_rules)) {
|
||||
for (const rule of navigation_rules as any[]) {
|
||||
navRulesArray.push({
|
||||
previous_step_id: rule.previous_step_id,
|
||||
next_step_id: rule.next_step_id,
|
||||
trigger: rule.trigger,
|
||||
element_type: rule.element_type,
|
||||
element_id: rule.element_id,
|
||||
rules: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (navigation_conditions && Array.isArray(navigation_conditions)) {
|
||||
for (const cond of navigation_conditions as any[]) {
|
||||
const ruleIndex = cond.rule_index;
|
||||
if (ruleIndex >= 0 && ruleIndex < navRulesArray.length) {
|
||||
navRulesArray[ruleIndex].rules.push({
|
||||
comparison: cond.comparison,
|
||||
field_key: cond.field_key,
|
||||
value: cond.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const logicRulesArray: any[] = [];
|
||||
if (logic_rules && Array.isArray(logic_rules)) {
|
||||
for (const rule of logic_rules as any[]) {
|
||||
logicRulesArray.push({
|
||||
name: rule.name,
|
||||
code: rule.code,
|
||||
trigger_event: rule.trigger_event,
|
||||
...(rule.description && { description: rule.description }),
|
||||
...(rule.index !== undefined && { index: rule.index }),
|
||||
...(rule.steps_csv && { steps: rule.steps_csv.split(',').map((s: string) => s.trim()) }),
|
||||
...(rule.elements_csv && { elements: rule.elements_csv.split(',').map((s: string) => s.trim()) }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const body: any = {
|
||||
form_name,
|
||||
template_form_id,
|
||||
steps: Array.from(stepsMap.values()),
|
||||
navigation_rules: navRulesArray,
|
||||
};
|
||||
|
||||
if (enabled !== undefined) {
|
||||
body.enabled = enabled;
|
||||
}
|
||||
|
||||
if (logicRulesArray.length > 0) {
|
||||
body.logic_rules = logicRulesArray;
|
||||
}
|
||||
|
||||
const response = await featheryCommon.apiCall<{
|
||||
id: string;
|
||||
name: string;
|
||||
internal_id?: string;
|
||||
}>({
|
||||
method: HttpMethod.POST,
|
||||
url: '/form/',
|
||||
apiKey: context.auth.secret_text,
|
||||
body,
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { featheryAuth } from '../common/auth';
|
||||
import { featheryCommon } from '../common/client';
|
||||
|
||||
export const deleteFormAction = createAction({
|
||||
auth: featheryAuth,
|
||||
name: 'delete_form',
|
||||
displayName: 'Delete Form',
|
||||
description: 'Delete a specific form.',
|
||||
props: {
|
||||
form_id: Property.Dropdown({
|
||||
displayName: 'Form',
|
||||
description: 'Select the form to delete.',
|
||||
required: true,
|
||||
refreshers: [],
|
||||
auth: featheryAuth,
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Connect your account first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
const forms = await featheryCommon.apiCall<
|
||||
Array<{ id: string; name: string; active: boolean }>
|
||||
>({
|
||||
method: HttpMethod.GET,
|
||||
url: '/form/',
|
||||
apiKey: auth.secret_text,
|
||||
});
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
options: forms.map((form) => ({
|
||||
label: form.name,
|
||||
value: form.id,
|
||||
})),
|
||||
};
|
||||
},
|
||||
}),
|
||||
confirm_delete: Property.Checkbox({
|
||||
displayName: 'Confirm Delete',
|
||||
description: 'Check to confirm deletion. This action cannot be undone.',
|
||||
required: true,
|
||||
defaultValue: false,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { form_id, confirm_delete } = context.propsValue;
|
||||
|
||||
if (!confirm_delete) {
|
||||
throw new Error('You must confirm the deletion by checking the confirm box.');
|
||||
}
|
||||
|
||||
await featheryCommon.apiCall({
|
||||
method: HttpMethod.DELETE,
|
||||
url: `/form/${form_id}/`,
|
||||
apiKey: context.auth.secret_text,
|
||||
body: { confirm_delete: true },
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Form ${form_id} deleted successfully.`,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { featheryAuth } from '../common/auth';
|
||||
import { featheryCommon } from '../common/client';
|
||||
|
||||
export const exportSubmissionPdfAction = createAction({
|
||||
auth: featheryAuth,
|
||||
name: 'export_submission_pdf',
|
||||
displayName: 'Export Form Submission PDF',
|
||||
description: 'Create a PDF export for a specific form submission.',
|
||||
props: {
|
||||
form_id: Property.Dropdown({
|
||||
displayName: 'Form',
|
||||
description: 'Select the form to export submission from.',
|
||||
required: true,
|
||||
refreshers: [],
|
||||
auth: featheryAuth,
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Connect your account first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
const forms = await featheryCommon.apiCall<
|
||||
Array<{ id: string; name: string; active: boolean }>
|
||||
>({
|
||||
method: HttpMethod.GET,
|
||||
url: '/form/',
|
||||
apiKey: auth.secret_text,
|
||||
});
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
options: forms.map((form) => ({
|
||||
label: form.name,
|
||||
value: form.id,
|
||||
})),
|
||||
};
|
||||
},
|
||||
}),
|
||||
user_id: Property.Dropdown({
|
||||
displayName: 'User',
|
||||
description: 'Select the user whose submission to export.',
|
||||
required: true,
|
||||
refreshers: [],
|
||||
auth: featheryAuth,
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Connect your account first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
const users = await featheryCommon.apiCall<
|
||||
Array<{ id: string; created_at: string; updated_at: string }>
|
||||
>({
|
||||
method: HttpMethod.GET,
|
||||
url: '/user/',
|
||||
apiKey: auth.secret_text,
|
||||
});
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
options: users.map((user) => ({
|
||||
label: user.id,
|
||||
value: user.id,
|
||||
})),
|
||||
};
|
||||
},
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { form_id, user_id } = context.propsValue;
|
||||
|
||||
const response = await featheryCommon.apiCall<{
|
||||
pdf_url: string;
|
||||
}>({
|
||||
method: HttpMethod.POST,
|
||||
url: '/form/submission/pdf/',
|
||||
apiKey: context.auth.secret_text,
|
||||
body: {
|
||||
form_id,
|
||||
user_id,
|
||||
},
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { featheryAuth } from '../common/auth';
|
||||
import { featheryCommon } from '../common/client';
|
||||
|
||||
export const listFormSubmissionsAction = createAction({
|
||||
auth: featheryAuth,
|
||||
name: 'list_form_submissions',
|
||||
displayName: 'List Form Submissions',
|
||||
description: 'List submission data for a particular form.',
|
||||
props: {
|
||||
form_id: Property.Dropdown({
|
||||
displayName: 'Form',
|
||||
description: 'Select the form to get submissions for.',
|
||||
required: true,
|
||||
refreshers: [],
|
||||
auth: featheryAuth,
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Connect your account first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
const forms = await featheryCommon.apiCall<
|
||||
Array<{ id: string; name: string; active: boolean }>
|
||||
>({
|
||||
method: HttpMethod.GET,
|
||||
url: '/form/',
|
||||
apiKey: auth.secret_text,
|
||||
});
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
options: forms.map((form) => ({
|
||||
label: form.name,
|
||||
value: form.id,
|
||||
})),
|
||||
};
|
||||
},
|
||||
}),
|
||||
start_time: Property.DateTime({
|
||||
displayName: 'Start Time',
|
||||
description: 'Limit submissions to after this update time.',
|
||||
required: false,
|
||||
}),
|
||||
end_time: Property.DateTime({
|
||||
displayName: 'End Time',
|
||||
description: 'Limit submissions to before this update time.',
|
||||
required: false,
|
||||
}),
|
||||
created_after: Property.DateTime({
|
||||
displayName: 'Created After',
|
||||
description: 'Limit submissions to after this creation time.',
|
||||
required: false,
|
||||
}),
|
||||
created_before: Property.DateTime({
|
||||
displayName: 'Created Before',
|
||||
description: 'Limit submissions to before this creation time.',
|
||||
required: false,
|
||||
}),
|
||||
count: Property.Number({
|
||||
displayName: 'Count',
|
||||
description: 'Limit the number of returned submissions.',
|
||||
required: false,
|
||||
}),
|
||||
completed: Property.StaticDropdown({
|
||||
displayName: 'Completion Status',
|
||||
description: 'Filter by completion status.',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'All', value: '' },
|
||||
{ label: 'Completed Only', value: 'true' },
|
||||
{ label: 'Incomplete Only', value: 'false' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
fields: Property.ShortText({
|
||||
displayName: 'Fields',
|
||||
description: 'Comma-separated list of field IDs to return.',
|
||||
required: false,
|
||||
}),
|
||||
no_field_values: Property.Checkbox({
|
||||
displayName: 'Exclude Field Values',
|
||||
description: 'Don\'t return field data. More performant for large datasets.',
|
||||
required: false,
|
||||
}),
|
||||
sort: Property.StaticDropdown({
|
||||
displayName: 'Sort',
|
||||
description: 'How to sort the returned field values.',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Alphabetically by Field ID', value: '' },
|
||||
{ label: 'By Form Layout', value: 'layout' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
page_size: Property.Number({
|
||||
displayName: 'Page Size',
|
||||
description: 'Number of results per page (default 500, max 1000).',
|
||||
required: false,
|
||||
}),
|
||||
use_cache: Property.Checkbox({
|
||||
displayName: 'Use Cache',
|
||||
description: 'Use cached data for faster response (may be a few minutes old).',
|
||||
required: false,
|
||||
}),
|
||||
field_search: Property.Array({
|
||||
displayName: 'Field Search',
|
||||
description: 'Search for submissions with specific field values.',
|
||||
required: false,
|
||||
properties: {
|
||||
field_id: Property.ShortText({
|
||||
displayName: 'Field ID',
|
||||
description: 'The field ID to search.',
|
||||
required: true,
|
||||
}),
|
||||
value: Property.ShortText({
|
||||
displayName: 'Value',
|
||||
description: 'The value to match.',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
fuzzy_search_threshold: Property.Number({
|
||||
displayName: 'Fuzzy Search Threshold',
|
||||
description: 'Score threshold between 0 and 1 for fuzzy search.',
|
||||
required: false,
|
||||
}),
|
||||
fuzzy_search_parameters: Property.Array({
|
||||
displayName: 'Fuzzy Search Parameters',
|
||||
description: 'Fields and terms for fuzzy search. Weights must sum to 1.',
|
||||
required: false,
|
||||
properties: {
|
||||
field_id: Property.ShortText({
|
||||
displayName: 'Field ID',
|
||||
description: 'The field to compare.',
|
||||
required: true,
|
||||
}),
|
||||
term: Property.ShortText({
|
||||
displayName: 'Search Term',
|
||||
description: 'The term to search for.',
|
||||
required: true,
|
||||
}),
|
||||
weight: Property.Number({
|
||||
displayName: 'Weight',
|
||||
description: 'Importance of this field (0-1). All weights must sum to 1.',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const {
|
||||
form_id,
|
||||
start_time,
|
||||
end_time,
|
||||
created_after,
|
||||
created_before,
|
||||
count,
|
||||
completed,
|
||||
fields,
|
||||
no_field_values,
|
||||
sort,
|
||||
page_size,
|
||||
use_cache,
|
||||
field_search,
|
||||
fuzzy_search_threshold,
|
||||
fuzzy_search_parameters,
|
||||
} = context.propsValue;
|
||||
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.append('form_id', form_id as string);
|
||||
|
||||
if (start_time) {
|
||||
queryParams.append('start_time', start_time);
|
||||
}
|
||||
if (end_time) {
|
||||
queryParams.append('end_time', end_time);
|
||||
}
|
||||
if (created_after) {
|
||||
queryParams.append('created_after', created_after);
|
||||
}
|
||||
if (created_before) {
|
||||
queryParams.append('created_before', created_before);
|
||||
}
|
||||
if (count !== undefined) {
|
||||
queryParams.append('count', count.toString());
|
||||
}
|
||||
if (completed && completed !== '') {
|
||||
queryParams.append('completed', completed);
|
||||
}
|
||||
if (fields) {
|
||||
queryParams.append('fields', fields);
|
||||
}
|
||||
if (no_field_values) {
|
||||
queryParams.append('no_field_values', 'true');
|
||||
}
|
||||
if (sort && sort !== '') {
|
||||
queryParams.append('sort', sort);
|
||||
}
|
||||
if (page_size !== undefined) {
|
||||
queryParams.append('page_size', page_size.toString());
|
||||
}
|
||||
if (use_cache) {
|
||||
queryParams.append('use_cache', 'true');
|
||||
}
|
||||
|
||||
if (field_search && Array.isArray(field_search) && field_search.length > 0) {
|
||||
const fieldSearchArray = (field_search as Array<{ field_id: string; value: string }>).map(
|
||||
(fs) => ({ field_id: fs.field_id, value: fs.value })
|
||||
);
|
||||
queryParams.append('field_search', JSON.stringify(fieldSearchArray));
|
||||
}
|
||||
|
||||
if (
|
||||
fuzzy_search_threshold !== undefined &&
|
||||
fuzzy_search_parameters &&
|
||||
Array.isArray(fuzzy_search_parameters) &&
|
||||
fuzzy_search_parameters.length > 0
|
||||
) {
|
||||
const fuzzySearch = {
|
||||
threshold: fuzzy_search_threshold,
|
||||
parameters: (
|
||||
fuzzy_search_parameters as Array<{ field_id: string; term: string; weight: number }>
|
||||
).map((p) => ({
|
||||
field_id: p.field_id,
|
||||
term: p.term,
|
||||
weight: p.weight,
|
||||
})),
|
||||
};
|
||||
queryParams.append('fuzzy_search', JSON.stringify(fuzzySearch));
|
||||
}
|
||||
|
||||
const response = await featheryCommon.apiCall<{
|
||||
count: number;
|
||||
next: string | null;
|
||||
previous: string | null;
|
||||
total_pages: number;
|
||||
current_page: number;
|
||||
results: Array<{
|
||||
values: Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
value: unknown;
|
||||
hidden: boolean;
|
||||
display_text: string;
|
||||
internal_id: string;
|
||||
}>;
|
||||
user_id: string;
|
||||
submission_start: string;
|
||||
last_submitted: string;
|
||||
}>;
|
||||
}>({
|
||||
method: HttpMethod.GET,
|
||||
url: `/form/submission/?${queryParams.toString()}`,
|
||||
apiKey: context.auth.secret_text,
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { featheryAuth } from '../common/auth';
|
||||
import { featheryCommon } from '../common/client';
|
||||
|
||||
export const updateFormAction = createAction({
|
||||
auth: featheryAuth,
|
||||
name: 'update_form',
|
||||
displayName: 'Update Form',
|
||||
description: 'Update a form\'s properties including status and translations.',
|
||||
props: {
|
||||
form_id: Property.Dropdown({
|
||||
displayName: 'Form',
|
||||
description: 'Select the form to update.',
|
||||
required: true,
|
||||
refreshers: [],
|
||||
auth: featheryAuth,
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Connect your account first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
const forms = await featheryCommon.apiCall<
|
||||
Array<{ id: string; name: string; active: boolean }>
|
||||
>({
|
||||
method: HttpMethod.GET,
|
||||
url: '/form/',
|
||||
apiKey: auth.secret_text,
|
||||
});
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
options: forms.map((form) => ({
|
||||
label: form.name,
|
||||
value: form.id,
|
||||
})),
|
||||
};
|
||||
},
|
||||
}),
|
||||
form_name: Property.ShortText({
|
||||
displayName: 'Form Name',
|
||||
description: 'New name for the form.',
|
||||
required: false,
|
||||
}),
|
||||
enabled: Property.Checkbox({
|
||||
displayName: 'Enabled',
|
||||
description: 'Whether the form should be enabled or disabled.',
|
||||
required: false,
|
||||
}),
|
||||
translations: Property.Array({
|
||||
displayName: 'Translations',
|
||||
description: 'Add translations for form text. Note: This will override existing translations.',
|
||||
required: false,
|
||||
properties: {
|
||||
default_text: Property.ShortText({
|
||||
displayName: 'Default Text',
|
||||
description: 'The original text to translate.',
|
||||
required: true,
|
||||
}),
|
||||
language_code: Property.ShortText({
|
||||
displayName: 'Language Code',
|
||||
description: 'Language code (e.g., "es" for Spanish, "fr" for French).',
|
||||
required: true,
|
||||
}),
|
||||
translation: Property.ShortText({
|
||||
displayName: 'Translation',
|
||||
description: 'The translated text.',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { form_id, form_name, enabled, translations } = context.propsValue;
|
||||
|
||||
const body: Record<string, unknown> = {};
|
||||
|
||||
if (form_name) {
|
||||
body['form_name'] = form_name;
|
||||
}
|
||||
|
||||
if (enabled !== undefined) {
|
||||
body['enabled'] = enabled;
|
||||
}
|
||||
|
||||
if (translations && Array.isArray(translations) && translations.length > 0) {
|
||||
const translationsObj: Record<string, Record<string, string>> = {};
|
||||
|
||||
for (const t of translations as Array<{
|
||||
default_text: string;
|
||||
language_code: string;
|
||||
translation: string;
|
||||
}>) {
|
||||
if (!translationsObj[t.default_text]) {
|
||||
translationsObj[t.default_text] = {};
|
||||
}
|
||||
translationsObj[t.default_text][t.language_code] = t.translation;
|
||||
}
|
||||
|
||||
body['translations'] = translationsObj;
|
||||
}
|
||||
|
||||
const response = await featheryCommon.apiCall<{
|
||||
enabled: boolean;
|
||||
form_name: string;
|
||||
}>({
|
||||
method: HttpMethod.PATCH,
|
||||
url: `/form/${form_id}/`,
|
||||
apiKey: context.auth.secret_text,
|
||||
body,
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { PieceAuth } from '@activepieces/pieces-framework';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { featheryCommon } from './client';
|
||||
|
||||
export const featheryAuth = PieceAuth.SecretText({
|
||||
displayName: 'API Key',
|
||||
description: 'Your Feathery admin API key. You can get an API key by creating an account.',
|
||||
required: true,
|
||||
validate: async ({ auth }) => {
|
||||
try {
|
||||
await featheryCommon.apiCall({
|
||||
method: HttpMethod.GET,
|
||||
url: '/account/',
|
||||
apiKey: auth,
|
||||
});
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
message: 'API key validated successfully. Connected to Feathery.'
|
||||
};
|
||||
} catch (error: any) {
|
||||
if (error.message.includes('401') || error.message.includes('403')) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Invalid API key. Please check your API key and try again.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
error: `Authentication failed: ${error.message}. Please verify your API key is correct.`,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
|
||||
export const featheryCommon = {
|
||||
baseUrl: 'https://api.feathery.io/api',
|
||||
|
||||
async apiCall<T>({
|
||||
method,
|
||||
url,
|
||||
body,
|
||||
apiKey,
|
||||
headers,
|
||||
}: {
|
||||
method: HttpMethod;
|
||||
url: string;
|
||||
body?: any;
|
||||
apiKey: string;
|
||||
headers?: Record<string, string>;
|
||||
}): Promise<T> {
|
||||
const response = await httpClient.sendRequest<T>({
|
||||
method,
|
||||
url: `${this.baseUrl}${url}`,
|
||||
headers: {
|
||||
'Authorization': `Token ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw new Error(`Feathery API error: ${response.status} - ${JSON.stringify(response.body)}`);
|
||||
}
|
||||
|
||||
return response.body;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import {
|
||||
createTrigger,
|
||||
Property,
|
||||
TriggerStrategy,
|
||||
AppConnectionValueForAuthProperty,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import {
|
||||
DedupeStrategy,
|
||||
Polling,
|
||||
pollingHelper,
|
||||
HttpMethod,
|
||||
} from '@activepieces/pieces-common';
|
||||
import dayjs from 'dayjs';
|
||||
import { featheryAuth } from '../common/auth';
|
||||
import { featheryCommon } from '../common/client';
|
||||
|
||||
const polling: Polling<
|
||||
AppConnectionValueForAuthProperty<typeof featheryAuth>,
|
||||
{ lookup_type: string; document_id?: string; user_id?: string }
|
||||
> = {
|
||||
strategy: DedupeStrategy.TIMEBASED,
|
||||
items: async ({ auth, propsValue }) => {
|
||||
const { lookup_type, document_id, user_id } = propsValue;
|
||||
|
||||
let url: string;
|
||||
if (lookup_type === 'document') {
|
||||
if (!document_id) {
|
||||
throw new Error('Please enter a Document ID to monitor for file submissions.');
|
||||
}
|
||||
url = `/document/envelope/?type=document&id=${encodeURIComponent(document_id)}`;
|
||||
} else if (lookup_type === 'user') {
|
||||
if (!user_id) {
|
||||
throw new Error('Please select a User to monitor for file submissions.');
|
||||
}
|
||||
url = `/document/envelope/?type=user&id=${encodeURIComponent(user_id)}`;
|
||||
} else {
|
||||
throw new Error('Please select a lookup type (Document Template or User).');
|
||||
}
|
||||
|
||||
try {
|
||||
const envelopes = await featheryCommon.apiCall<
|
||||
Array<{
|
||||
id: string;
|
||||
document: string;
|
||||
user: string;
|
||||
signer: string;
|
||||
sender: string;
|
||||
file: string;
|
||||
type: string;
|
||||
viewed: boolean;
|
||||
signed: boolean;
|
||||
tags: string[];
|
||||
created_at: string;
|
||||
}>
|
||||
>({
|
||||
method: HttpMethod.GET,
|
||||
url,
|
||||
apiKey: auth.secret_text,
|
||||
});
|
||||
|
||||
// Handle case where API returns error object instead of array
|
||||
if (!Array.isArray(envelopes)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return envelopes.map((envelope) => ({
|
||||
epochMilliSeconds: dayjs(envelope.created_at).valueOf(),
|
||||
data: envelope,
|
||||
}));
|
||||
} catch {
|
||||
// Return empty if no documents/users found
|
||||
return [];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const fileSubmittedTrigger = createTrigger({
|
||||
auth: featheryAuth,
|
||||
name: 'file_submitted',
|
||||
displayName: 'File Submitted',
|
||||
description: 'Triggers when a file is submitted in your form, or when a document is signed/generated.',
|
||||
props: {
|
||||
lookup_type: Property.StaticDropdown({
|
||||
displayName: 'Lookup By',
|
||||
description: 'Choose how to find document envelopes.',
|
||||
required: true,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Document Template', value: 'document' },
|
||||
{ label: 'User', value: 'user' },
|
||||
],
|
||||
},
|
||||
defaultValue: 'document',
|
||||
}),
|
||||
document_id: Property.ShortText({
|
||||
displayName: 'Document ID',
|
||||
description: 'The ID of the document template to monitor.',
|
||||
required: false,
|
||||
}),
|
||||
user_id: Property.Dropdown({
|
||||
displayName: 'User',
|
||||
description: 'Select the user to monitor for file submissions.',
|
||||
required: false,
|
||||
refreshers: [],
|
||||
auth: featheryAuth,
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Connect your account first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
const users = await featheryCommon.apiCall<
|
||||
Array<{ id: string; created_at: string; updated_at: string }>
|
||||
>({
|
||||
method: HttpMethod.GET,
|
||||
url: '/user/',
|
||||
apiKey: auth.secret_text,
|
||||
});
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
options: users.map((user) => ({
|
||||
label: user.id,
|
||||
value: user.id,
|
||||
})),
|
||||
};
|
||||
},
|
||||
}),
|
||||
},
|
||||
type: TriggerStrategy.POLLING,
|
||||
sampleData: {
|
||||
id: 'envelope-123',
|
||||
document: 'doc-456',
|
||||
user: 'user@example.com',
|
||||
signer: 'signer@example.com',
|
||||
sender: 'sender@example.com',
|
||||
file: 'https://link-to-filled-file.com',
|
||||
type: 'pdf',
|
||||
viewed: true,
|
||||
signed: true,
|
||||
tags: ['document-tag'],
|
||||
created_at: '2024-06-03T00:00:00Z',
|
||||
},
|
||||
async onEnable(context) {
|
||||
await pollingHelper.onEnable(polling, context);
|
||||
},
|
||||
async onDisable(context) {
|
||||
await pollingHelper.onDisable(polling, context);
|
||||
},
|
||||
async run(context) {
|
||||
return await pollingHelper.poll(polling, context);
|
||||
},
|
||||
async test(context) {
|
||||
return await pollingHelper.test(polling, context);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
import {
|
||||
createTrigger,
|
||||
Property,
|
||||
TriggerStrategy,
|
||||
AppConnectionValueForAuthProperty,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import {
|
||||
DedupeStrategy,
|
||||
Polling,
|
||||
pollingHelper,
|
||||
HttpMethod,
|
||||
} from '@activepieces/pieces-common';
|
||||
import dayjs from 'dayjs';
|
||||
import { featheryAuth } from '../common/auth';
|
||||
import { featheryCommon } from '../common/client';
|
||||
|
||||
const polling: Polling<AppConnectionValueForAuthProperty<typeof featheryAuth>, { form_id: string }> = {
|
||||
strategy: DedupeStrategy.TIMEBASED,
|
||||
items: async ({ auth, propsValue, lastFetchEpochMS }) => {
|
||||
const { form_id } = propsValue;
|
||||
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.append('form_id', form_id);
|
||||
queryParams.append('completed', 'true');
|
||||
|
||||
if (lastFetchEpochMS) {
|
||||
queryParams.append('start_time', new Date(lastFetchEpochMS).toISOString());
|
||||
}
|
||||
|
||||
const response = await featheryCommon.apiCall<{
|
||||
count: number;
|
||||
results: Array<{
|
||||
values: Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
value: unknown;
|
||||
hidden: boolean;
|
||||
display_text: string;
|
||||
internal_id: string;
|
||||
}>;
|
||||
user_id: string;
|
||||
submission_start: string;
|
||||
last_submitted: string;
|
||||
}>;
|
||||
}>({
|
||||
method: HttpMethod.GET,
|
||||
url: `/form/submission/?${queryParams.toString()}`,
|
||||
apiKey: auth.secret_text,
|
||||
});
|
||||
|
||||
return response.results.map((submission) => ({
|
||||
epochMilliSeconds: dayjs(submission.last_submitted).valueOf(),
|
||||
data: submission,
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
export const formCompletedTrigger = createTrigger({
|
||||
auth: featheryAuth,
|
||||
name: 'form_completed',
|
||||
displayName: 'Form Completed',
|
||||
description: 'Triggers when a form is completed by an end user.',
|
||||
props: {
|
||||
form_id: Property.Dropdown({
|
||||
displayName: 'Form',
|
||||
description: 'Select the form to monitor for completions.',
|
||||
required: true,
|
||||
refreshers: [],
|
||||
auth: featheryAuth,
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Connect your account first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
const forms = await featheryCommon.apiCall<
|
||||
Array<{ id: string; name: string; active: boolean }>
|
||||
>({
|
||||
method: HttpMethod.GET,
|
||||
url: '/form/',
|
||||
apiKey: auth.secret_text,
|
||||
});
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
options: forms.map((form) => ({
|
||||
label: form.name,
|
||||
value: form.id,
|
||||
})),
|
||||
};
|
||||
},
|
||||
}),
|
||||
},
|
||||
type: TriggerStrategy.POLLING,
|
||||
sampleData: {
|
||||
values: [
|
||||
{
|
||||
id: 'email_field',
|
||||
type: 'email',
|
||||
created_at: '2024-10-28T07:56:09.391398Z',
|
||||
updated_at: '2024-10-28T16:39:32.577794Z',
|
||||
value: 'user@example.com',
|
||||
hidden: false,
|
||||
display_text: '',
|
||||
internal_id: 'ef5ed054-73de-4463-ba61-82c36aca5afc',
|
||||
},
|
||||
],
|
||||
user_id: '131e7132-dg6d-4a8c-9d70-cgd493c2a368',
|
||||
submission_start: '2024-10-30T02:07:32Z',
|
||||
last_submitted: '2024-10-30T02:07:32Z',
|
||||
},
|
||||
async onEnable(context) {
|
||||
await pollingHelper.onEnable(polling, context);
|
||||
},
|
||||
async onDisable(context) {
|
||||
await pollingHelper.onDisable(polling, context);
|
||||
},
|
||||
async run(context) {
|
||||
return await pollingHelper.poll(polling, context);
|
||||
},
|
||||
async test(context) {
|
||||
return await pollingHelper.test(polling, context);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
import {
|
||||
createTrigger,
|
||||
Property,
|
||||
TriggerStrategy,
|
||||
AppConnectionValueForAuthProperty,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import {
|
||||
DedupeStrategy,
|
||||
Polling,
|
||||
pollingHelper,
|
||||
HttpMethod,
|
||||
} from '@activepieces/pieces-common';
|
||||
import dayjs from 'dayjs';
|
||||
import { featheryAuth } from '../common/auth';
|
||||
import { featheryCommon } from '../common/client';
|
||||
|
||||
const polling: Polling<AppConnectionValueForAuthProperty<typeof featheryAuth>, { form_id: string }> = {
|
||||
strategy: DedupeStrategy.TIMEBASED,
|
||||
items: async ({ auth, propsValue, lastFetchEpochMS }) => {
|
||||
const { form_id } = propsValue;
|
||||
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.append('form_id', form_id);
|
||||
|
||||
// Filter by submissions after last fetch
|
||||
if (lastFetchEpochMS) {
|
||||
queryParams.append('start_time', new Date(lastFetchEpochMS).toISOString());
|
||||
}
|
||||
|
||||
const response = await featheryCommon.apiCall<{
|
||||
count: number;
|
||||
results: Array<{
|
||||
values: Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
value: unknown;
|
||||
hidden: boolean;
|
||||
display_text: string;
|
||||
internal_id: string;
|
||||
}>;
|
||||
user_id: string;
|
||||
submission_start: string;
|
||||
last_submitted: string;
|
||||
}>;
|
||||
}>({
|
||||
method: HttpMethod.GET,
|
||||
url: `/form/submission/?${queryParams.toString()}`,
|
||||
apiKey: auth.secret_text,
|
||||
});
|
||||
|
||||
return response.results.map((submission) => ({
|
||||
epochMilliSeconds: dayjs(submission.last_submitted).valueOf(),
|
||||
data: submission,
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
export const newSubmissionTrigger = createTrigger({
|
||||
auth: featheryAuth,
|
||||
name: 'new_submission',
|
||||
displayName: 'New Form Submission',
|
||||
description: 'Triggers when a user submits data through your form.',
|
||||
props: {
|
||||
form_id: Property.Dropdown({
|
||||
displayName: 'Form',
|
||||
description: 'Select the form to monitor for submissions.',
|
||||
required: true,
|
||||
refreshers: [],
|
||||
auth: featheryAuth,
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Connect your account first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
const forms = await featheryCommon.apiCall<
|
||||
Array<{ id: string; name: string; active: boolean }>
|
||||
>({
|
||||
method: HttpMethod.GET,
|
||||
url: '/form/',
|
||||
apiKey: auth.secret_text,
|
||||
});
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
options: forms.map((form) => ({
|
||||
label: form.name,
|
||||
value: form.id,
|
||||
})),
|
||||
};
|
||||
},
|
||||
}),
|
||||
},
|
||||
type: TriggerStrategy.POLLING,
|
||||
sampleData: {
|
||||
values: [
|
||||
{
|
||||
id: 'email_field',
|
||||
type: 'email',
|
||||
created_at: '2024-10-28T07:56:09.391398Z',
|
||||
updated_at: '2024-10-28T16:39:32.577794Z',
|
||||
value: 'user@example.com',
|
||||
hidden: false,
|
||||
display_text: '',
|
||||
internal_id: 'ef5ed054-73de-4463-ba61-82c36aca5afc',
|
||||
},
|
||||
],
|
||||
user_id: '131e7132-dg6d-4a8c-9d70-cgd493c2a368',
|
||||
submission_start: '2024-10-30T02:07:32Z',
|
||||
last_submitted: '2024-10-30T02:07:32Z',
|
||||
},
|
||||
async onEnable(context) {
|
||||
await pollingHelper.onEnable(polling, context);
|
||||
},
|
||||
async onDisable(context) {
|
||||
await pollingHelper.onDisable(polling, context);
|
||||
},
|
||||
async run(context) {
|
||||
return await pollingHelper.poll(polling, context);
|
||||
},
|
||||
async test(context) {
|
||||
return await pollingHelper.test(polling, context);
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user