Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,14 @@
{
"Upload, process, and manage documents programmatically with FlowParser's REST API.": "Upload, process, and manage documents programmatically with FlowParser's REST API.",
"\nTo get your API Key:\n\n1. Go to your FlowParser account\n2. Navigate to API settings\n3. Copy your API key\n4. Paste it here\n": "\nTo get your API Key:\n\n1. Go to your FlowParser account\n2. Navigate to API settings\n3. Copy your API key\n4. Paste it here\n",
"Upload Document": "Upload Document",
"Upload a new document to FlowParser for processing": "Upload a new document to FlowParser for processing",
"File": "File",
"The document file to upload": "The document file to upload",
"New Parsed Document by Template": "New Parsed Document by Template",
"New Parsed Document Found": "New Parsed Document Found",
"Triggers when a new document is parsed using a specific template": "Triggers when a new document is parsed using a specific template",
"Triggers when a document has been successfully parsed": "Triggers when a document has been successfully parsed",
"Template": "Template",
"Select a template to monitor for new parsed documents": "Select a template to monitor for new parsed documents"
}

View File

@@ -0,0 +1,18 @@
import { createPiece } from '@activepieces/pieces-framework';
import { PieceCategory } from '@activepieces/shared';
import { flowParserAuth } from './lib/common/auth';
import { uploadDocument } from './lib/actions/upload-document';
import { newParsedDocumentByTemplate } from './lib/triggers/new-parsed-document-by-template';
import { newParsedDocumentFound } from './lib/triggers/new-parsed-document-found';
export const flowParser = createPiece({
displayName: 'FlowParser',
description: 'Upload, process, and manage documents programmatically with FlowParser\'s REST API.',
auth: flowParserAuth,
minimumSupportedRelease: '0.36.1',
logoUrl: 'https://cdn.activepieces.com/pieces/flow-parser.png',
categories: [PieceCategory.DEVELOPER_TOOLS],
authors: ["onyedikachi-david"],
actions: [uploadDocument],
triggers: [newParsedDocumentByTemplate, newParsedDocumentFound],
});

View File

@@ -0,0 +1,67 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
import { flowParserAuth } from '../common/auth';
const BASE_URL = 'https://api.flowparser.one/v1';
export const uploadDocument = createAction({
auth: flowParserAuth,
name: 'upload_document',
displayName: 'Upload Document',
description: 'Upload a new document to FlowParser for processing',
props: {
file: Property.File({
displayName: 'File',
description: 'The document file to upload',
required: true,
}),
},
async run(context) {
const { auth, propsValue } = context;
const { file } = propsValue;
if (!file) {
throw new Error('File is required');
}
const formData = new FormData();
const blob = new Blob([new Uint8Array(file.data)], { type: 'application/octet-stream' });
formData.append('file', blob, file.filename);
try {
const response = await httpClient.sendRequest<{
success: boolean;
documentId: string;
message: string;
}>({
method: HttpMethod.POST,
url: `${BASE_URL}/documents`,
headers: {
flow_api_key: auth.secret_text,
},
body: formData,
});
return response.body;
} catch (error: any) {
const statusCode = error.response?.status || error.status;
const errorBody = error.response?.body || error.body;
if (statusCode === 401 || statusCode === 403) {
throw new Error('Authentication failed. Please check your API key.');
}
if (statusCode === 429) {
throw new Error('Rate limit exceeded. Please wait a moment and try again.');
}
if (statusCode >= 400 && statusCode < 500) {
const errorMessage = errorBody?.message || errorBody?.error || error.message || 'Request failed';
throw new Error(`Failed to upload document: ${errorMessage}`);
}
throw new Error(`FlowParser API error: ${error.message || String(error)}`);
}
},
});

View File

@@ -0,0 +1,61 @@
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
import { PieceAuth } from '@activepieces/pieces-framework';
const BASE_URL = 'https://api.flowparser.one/v1';
export const flowParserAuth = PieceAuth.SecretText({
displayName: 'API Key',
description: `
To get your API Key:
1. Go to your FlowParser account
2. Navigate to API settings
3. Copy your API key
4. Paste it here
`,
required: true,
validate: async ({ auth }) => {
if (!auth) {
return {
valid: false,
error: 'API key is required',
};
}
try {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${BASE_URL}/me`,
headers: {
flow_api_key: auth,
},
});
if (response.status === 200) {
return {
valid: true,
};
}
return {
valid: false,
error: 'Invalid API key',
};
} catch (error: any) {
const statusCode = error.response?.status || error.status;
if (statusCode === 401 || statusCode === 403) {
return {
valid: false,
error: 'Invalid API key. Please check your API key and try again.',
};
}
return {
valid: false,
error: 'Failed to validate API key. Please check your API key and try again.',
};
}
},
});

View File

@@ -0,0 +1,76 @@
import { HttpMethod, httpClient, HttpMessageBody, QueryParams } from '@activepieces/pieces-common';
import { PiecePropValueSchema } from '@activepieces/pieces-framework';
import { flowParserAuth } from './auth';
const BASE_URL = 'https://api.flowparser.one/v1';
export type FlowParserApiCallParams = {
method: HttpMethod;
path: string;
queryParams?: Record<string, string | number | string[] | undefined>;
body?: any;
auth: PiecePropValueSchema<typeof flowParserAuth>;
};
export async function flowParserApiCall<T extends HttpMessageBody>({
method,
path,
queryParams,
body,
auth,
}: FlowParserApiCallParams): Promise<T> {
const url = `${BASE_URL}${path}`;
const headers: Record<string, string> = {
flow_api_key: auth,
};
if (body) {
headers['Content-Type'] = 'application/json';
}
const qs: QueryParams = {};
if (queryParams) {
for (const [key, value] of Object.entries(queryParams)) {
if (value !== null && value !== undefined) {
qs[key] = String(value);
}
}
}
try {
const response = await httpClient.sendRequest<T>({
method,
url,
headers,
queryParams: qs,
body,
});
return response.body;
} catch (error: any) {
const statusCode = error.response?.status || error.status;
const errorBody = error.response?.body || error.body;
if (statusCode === 401 || statusCode === 403) {
throw new Error('Authentication failed. Please check your API key.');
}
if (statusCode === 429) {
throw new Error('Rate limit exceeded. Please wait a moment and try again.');
}
if (statusCode === 404) {
throw new Error('Endpoint not found. Please check the API endpoint path.');
}
if (statusCode >= 400 && statusCode < 500) {
const errorMessage = errorBody?.message || errorBody?.error || error.message || 'Request failed';
throw new Error(`Request failed: ${errorMessage}`);
}
const originalMessage = error.message || String(error);
throw new Error(`FlowParser API error: ${originalMessage}`);
}
}

View File

@@ -0,0 +1,59 @@
import { Property } from '@activepieces/pieces-framework';
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
import { flowParserAuth } from './auth';
const BASE_URL = 'https://api.flowparser.one/v1';
export const templateDropdown = Property.Dropdown({
displayName: 'Template',
description: 'Select a template to monitor for new parsed documents',
required: true,
auth: flowParserAuth,
refreshers: ['auth'],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account first',
};
}
try {
const response = await httpClient.sendRequest<{
templates: Array<{
id: string;
name: string;
}>;
}>({
method: HttpMethod.GET,
url: `${BASE_URL}/documents/templates`,
headers: {
flow_api_key: auth.secret_text,
},
});
if (!response.body.templates || response.body.templates.length === 0) {
return {
options: [],
placeholder: 'No templates found',
};
}
return {
disabled: false,
options: response.body.templates.map((template) => ({
label: template.name,
value: template.id,
})),
};
} catch (error: any) {
return {
disabled: true,
placeholder: 'Failed to load templates. Please check your API key.',
options: [],
};
}
},
});

View File

@@ -0,0 +1,114 @@
import {
DedupeStrategy,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import {
AppConnectionValueForAuthProperty,
StaticPropsValue,
TriggerStrategy,
createTrigger,
} from '@activepieces/pieces-framework';
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
import { flowParserAuth } from '../common/auth';
import { templateDropdown } from '../common/props';
import dayjs from 'dayjs';
const BASE_URL = 'https://api.flowparser.one/v1';
const props = {
templateId: templateDropdown,
};
const polling: Polling<
AppConnectionValueForAuthProperty<typeof flowParserAuth>,
StaticPropsValue<typeof props>
> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, propsValue, lastFetchEpochMS }) => {
const { templateId } = propsValue;
if (!templateId) {
return [];
}
try {
const queryParams: Record<string, string> = {
status: 'parsed',
template_id: templateId,
};
if (lastFetchEpochMS) {
queryParams['since'] = new Date(lastFetchEpochMS).toISOString();
}
const response = await httpClient.sendRequest<
Array<{
id: string;
status: string;
[key: string]: any;
}>
>({
method: HttpMethod.GET,
url: `${BASE_URL}/documents/status-changes`,
headers: {
flow_api_key: auth.secret_text,
},
queryParams,
});
const statusChanges = Array.isArray(response.body) ? response.body : [];
// Filter to only include parsed status changes for this template
const parsedDocuments = statusChanges.filter(
(doc) => doc.status === 'parsed'
);
return parsedDocuments.map((doc) => {
// Use the status change timestamp or current time as fallback
const timestamp = doc['updatedAt'] || doc['createdAt'] || new Date().toISOString();
return {
epochMilliSeconds: dayjs(timestamp).valueOf(),
data: doc,
};
});
} catch (error: any) {
console.error('Error fetching parsed documents:', error);
return [];
}
},
};
export const newParsedDocumentByTemplate = createTrigger({
auth: flowParserAuth,
name: 'new_parsed_document_by_template',
displayName: 'New Parsed Document by Template',
description: 'Triggers when a new document is parsed using a specific template',
props,
sampleData: {
id: 'uuid',
documentId: 'uuid',
templateId: 'uuid',
createdAt: '2024-01-01T00:00:00Z',
parsedAt: '2024-01-01T00:00:00Z',
status: 'parsed',
},
type: TriggerStrategy.POLLING,
async test(context) {
const { store, auth, propsValue, files } = context;
return await pollingHelper.test(polling, { store, auth, propsValue, files });
},
async onEnable(context) {
const { store, auth, propsValue } = context;
await pollingHelper.onEnable(polling, { store, auth, propsValue });
},
async onDisable(context) {
const { store, auth, propsValue } = context;
await pollingHelper.onDisable(polling, { store, auth, propsValue });
},
async run(context) {
const { store, auth, propsValue, files } = context;
return await pollingHelper.poll(polling, { store, auth, propsValue, files });
},
});

View File

@@ -0,0 +1,98 @@
import {
DedupeStrategy,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import {
AppConnectionValueForAuthProperty,
TriggerStrategy,
createTrigger,
} from '@activepieces/pieces-framework';
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
import { flowParserAuth } from '../common/auth';
import dayjs from 'dayjs';
const BASE_URL = 'https://api.flowparser.one/v1';
const polling: Polling<AppConnectionValueForAuthProperty<typeof flowParserAuth>, Record<string, never>> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, lastFetchEpochMS }) => {
try {
const queryParams: Record<string, string> = {
status: 'parsed',
};
if (lastFetchEpochMS) {
queryParams['since'] = new Date(lastFetchEpochMS).toISOString();
}
const response = await httpClient.sendRequest<
Array<{
id: string;
status: string;
[key: string]: any;
}>
>({
method: HttpMethod.GET,
url: `${BASE_URL}/documents/status-changes`,
headers: {
flow_api_key: auth.secret_text,
},
queryParams,
});
const statusChanges = Array.isArray(response.body) ? response.body : [];
// Filter to only include parsed status changes
const parsedDocuments = statusChanges.filter(
(doc) => doc.status === 'parsed'
);
return parsedDocuments.map((doc) => {
// Use the status change timestamp or current time as fallback
const timestamp = doc['updatedAt'] || doc['createdAt'] || new Date().toISOString();
return {
epochMilliSeconds: dayjs(timestamp).valueOf(),
data: doc,
};
});
} catch (error: any) {
console.error('Error fetching parsed documents:', error);
return [];
}
},
};
export const newParsedDocumentFound = createTrigger({
auth: flowParserAuth,
name: 'new_parsed_document_found',
displayName: 'New Parsed Document Found',
description: 'Triggers when a document has been successfully parsed',
props: {},
sampleData: {
id: 'uuid',
documentId: 'uuid',
templateId: 'uuid',
createdAt: '2024-01-01T00:00:00Z',
parsedAt: '2024-01-01T00:00:00Z',
status: 'parsed',
},
type: TriggerStrategy.POLLING,
async test(context) {
const { store, auth, propsValue, files } = context;
return await pollingHelper.test(polling, { store, auth, propsValue, files });
},
async onEnable(context) {
const { store, auth, propsValue } = context;
await pollingHelper.onEnable(polling, { store, auth, propsValue });
},
async onDisable(context) {
const { store, auth, propsValue } = context;
await pollingHelper.onDisable(polling, { store, auth, propsValue });
},
async run(context) {
const { store, auth, propsValue, files } = context;
return await pollingHelper.poll(polling, { store, auth, propsValue, files });
},
});