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,82 @@
import { confluenceAuth } from "../../index";
import { createAction, Property } from "@activepieces/pieces-framework";
import { folderIdProp, spaceIdProp, templateIdProp, templateVariablesProp } from "../common/props";
import { confluenceApiCall } from "../common";
import { HttpMethod } from "@activepieces/pieces-common";
export const createPageFromTemplateAction = createAction({
auth:confluenceAuth,
name:'create-page-from-template',
displayName:'Create Page from Template',
description:'Creates a new page from a template with the given title and variables.',
props:{
spaceId:spaceIdProp,
templateId:templateIdProp,
folderId:folderIdProp,
title:Property.ShortText({
displayName:'Title',
required:true,
}),
status:Property.StaticDropdown({
displayName:'Status',
required:true,
defaultValue:'draft',
options:{
disabled:false,
options:[
{
label:'Published ',
value:'current'
},
{
label:'Draft',
value:'draft'
}
]
}
}),
templateVariables:templateVariablesProp,
},
async run(context){
const {spaceId,templateId,title,status,folderId} = context.propsValue;
const variables = context.propsValue.templateVariables ??{};
const template = await confluenceApiCall<{ body: { storage: { value: string } } }>({
domain: context.auth.props.confluenceDomain,
username: context.auth.props.username,
password: context.auth.props.password,
method: HttpMethod.GET,
version: 'v1',
resourceUri: `/template/${templateId}`,
});
const body = template.body.storage.value;
let content = body.replace(/<at:declarations>[\s\S]*?<\/at:declarations>/, "").trim();
Object.entries(variables).forEach(([key, value]) => {
const varRegex = new RegExp(`<at:var at:name=(['"])${key}\\1\\s*\\/?>`, "g");
content = content.replace(varRegex, value);
});
const response = await confluenceApiCall({
domain: context.auth.props.confluenceDomain,
username: context.auth.props.username,
password: context.auth.props.password,
method: HttpMethod.POST,
version: 'v2',
resourceUri: '/pages',
body: {
spaceId:spaceId,
title,
parentId:folderId,
status,
body:{
representation:'storage',
value:content,
}
}
})
return response;
}
})

View File

@@ -0,0 +1,149 @@
import {
createAction,
Property,
DynamicPropsValue,
PiecePropValueSchema,
AppConnectionValueForAuthProperty,
} from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { confluenceAuth } from '../..';
import { confluenceApiCall } from '../common';
interface ConfluencePage {
id: string;
title: string;
body: any;
children?: ConfluencePage[];
}
async function getPageWithContent(
auth: AppConnectionValueForAuthProperty<typeof confluenceAuth>,
pageId: string,
): Promise<ConfluencePage> {
try {
const response = await confluenceApiCall<ConfluencePage>({
domain: auth.props.confluenceDomain,
username: auth.props.username,
password: auth.props.password,
method: HttpMethod.GET,
version: 'v2',
resourceUri: `/pages/${pageId}`,
query: {
'body-format': 'storage',
},
});
return response;
} catch (error) {
throw new Error(
`Failed to fetch page ${pageId}: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
}
}
async function getChildPages(
auth: AppConnectionValueForAuthProperty<typeof confluenceAuth>,
parentId: string,
currentDepth: number,
maxDepth: number,
): Promise<ConfluencePage[]> {
if (currentDepth >= maxDepth) {
return [];
}
try {
const childrenResponse = await confluenceApiCall<{ results: ConfluencePage[] }>({
domain: auth.props.confluenceDomain,
username: auth.props.username,
password: auth.props.password,
method: HttpMethod.GET,
version: 'v2',
resourceUri: `/pages/${parentId}/children`,
});
const childPages = await Promise.all(
childrenResponse.results.map(async (childPage) => {
const pageWithContent = await getPageWithContent(auth, childPage.id);
const children = await getChildPages(auth, childPage.id, currentDepth + 1, maxDepth);
return {
...pageWithContent,
children,
};
}),
);
return childPages;
} catch (error) {
throw new Error(
`Failed to fetch children for page ${parentId}: ${
error instanceof Error ? error.message : 'Unknown error'
}`,
);
}
}
export const getPageContent = createAction({
name: 'getPageContent',
displayName: 'Get Page Content',
description: 'Get page content and optionally all its descendants',
auth: confluenceAuth,
props: {
pageId: Property.ShortText({
displayName: 'Page ID',
description: 'Get this from the page URL of your Confluence Cloud',
required: true,
}),
includeDescendants: Property.Checkbox({
displayName: 'Include Descendants ?',
description: 'If checked, will fetch all child pages recursively.',
required: false,
defaultValue: false,
}),
dynamic: Property.DynamicProperties({
auth: confluenceAuth,
displayName: 'Dynamic Properties',
refreshers: ['includeDescendants'],
required: true,
props: async ({ includeDescendants }) => {
if (!includeDescendants) {
return {};
}
const fields: DynamicPropsValue = {
maxDepth: Property.Number({
displayName: 'Maximum Depth',
description: 'Maximum depth of child pages to fetch.',
required: true,
defaultValue: 5,
}),
};
return fields;
},
}),
},
async run(context) {
try {
const page = await getPageWithContent(context.auth, context.propsValue.pageId);
if (!context.propsValue.includeDescendants) {
return page;
}
const children = await getChildPages(
context.auth,
context.propsValue.pageId,
1,
context.propsValue.dynamic['maxDepth'],
);
return {
...page,
children,
};
} catch (error) {
throw new Error(
`Failed to fetch page ${context.propsValue.pageId}: ${
error instanceof Error ? error.message : 'Unknown error'
}`,
);
}
},
});

View File

@@ -0,0 +1,118 @@
import {
AuthenticationType,
httpClient,
HttpMessageBody,
HttpMethod,
HttpRequest,
QueryParams,
} from '@activepieces/pieces-common';
import { isNil } from '@activepieces/shared';
export type ConfluenceApiCallParams = {
domain: string;
username: string;
password: string;
version: 'v1' | 'v2';
method: HttpMethod;
resourceUri: string;
query?: QueryParams;
body?: any;
};
export type PaginatedResponse<T> = {
results: T[];
_links?: {
next?: string;
};
};
export async function confluenceApiCall<T extends HttpMessageBody>({
domain,
username,
password,
method,
version,
resourceUri,
query,
body,
}: ConfluenceApiCallParams): Promise<T> {
const baseUrl = version === 'v2' ? `${domain}/wiki/api/v2` : `${domain}/wiki/rest/api`;
const request: HttpRequest = {
method,
url: baseUrl + resourceUri,
authentication: {
type: AuthenticationType.BASIC,
username,
password,
},
queryParams: query,
body: body,
};
const response = await httpClient.sendRequest<T>(request);
return response.body;
}
export async function confluencePaginatedApiCall<T extends HttpMessageBody>({
domain,
username,
password,
method,
version,
resourceUri,
query,
body,
}: ConfluenceApiCallParams): Promise<T[]> {
const qs = query ? query : {};
const resultData: T[] = [];
if (version === 'v2') {
let nextUrl = `${domain}/wiki/api/v2${resourceUri}?limit=200`;
do {
const response = await httpClient.sendRequest<PaginatedResponse<T>>({
method,
url: nextUrl,
authentication: {
type: AuthenticationType.BASIC,
username,
password,
},
queryParams: qs,
body,
});
if (isNil(response.body.results)) {
break;
}
resultData.push(...response.body.results);
nextUrl = response.body?._links?.next ? `${domain}${response.body._links.next}` : '';
} while (nextUrl);
} else {
let start = 0;
let hasMoreData = true;
do {
const response = await httpClient.sendRequest<{ results: T[] }>({
method,
url: `${domain}/wiki/rest/api${resourceUri}?start=${start}&limit=100`,
authentication: {
type: AuthenticationType.BASIC,
username,
password,
},
queryParams: qs,
body,
});
if (isNil(response.body.results) || response.body.results.length === 0) {
hasMoreData = false;
} else {
resultData.push(...response.body.results);
start += 100;
}
} while (hasMoreData);
}
return resultData;
}

View File

@@ -0,0 +1,251 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { confluenceApiCall, confluencePaginatedApiCall } from '.';
import { confluenceAuth } from '../../index';
import {
DropdownOption,
DynamicPropsValue,
PiecePropValueSchema,
Property,
} from '@activepieces/pieces-framework';
import { parseStringPromise } from 'xml2js';
export const spaceIdProp = Property.Dropdown({
auth: confluenceAuth,
displayName: 'Space',
refreshers: [],
required: true,
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account first.',
};
}
const spaces = await confluencePaginatedApiCall<{ id: string; name: string }>({
domain: auth.props.confluenceDomain,
username: auth.props.username,
password: auth.props.password,
version: 'v2',
method: HttpMethod.GET,
resourceUri: '/spaces',
});
const options: DropdownOption<string>[] = [];
for (const space of spaces) {
options.push({
label: space.name,
value: space.id,
});
}
return {
disabled: false,
options,
};
},
});
export const templateIdProp = Property.Dropdown({
displayName: 'Template',
auth: confluenceAuth,
refreshers: ['spaceId'],
required: true,
options: async ({ auth, spaceId }) => {
if (!auth || !spaceId) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account first and select a space.',
};
}
const space = await confluenceApiCall<{ id: string; name: string; key: string }>({
domain: auth.props.confluenceDomain,
username: auth.props.username,
password: auth.props.password,
method: HttpMethod.GET,
version: 'v2',
resourceUri: `/spaces/${spaceId}`,
});
const templates = await confluencePaginatedApiCall<{ templateId: string; name: string }>({
domain: auth.props.confluenceDomain,
username: auth.props.username,
password: auth.props.password,
version: 'v1',
method: HttpMethod.GET,
resourceUri: `/template/page`,
query: { spaceKey: space.key },
});
const options: DropdownOption<string>[] = [];
for (const template of templates) {
options.push({
label: template.name,
value: template.templateId,
});
}
return {
disabled: false,
options,
};
},
});
export const folderIdProp = Property.Dropdown({
displayName:'Parent Folder',
auth: confluenceAuth,
refreshers:['spaceId'],
required:false,
options:async ({auth,spaceId})=>{
if (!auth || !spaceId) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account first and select a space.',
};
}
const space = await confluenceApiCall<{ id: string; name: string; key: string,homepageId:string }>({
domain: auth.props.confluenceDomain,
username: auth.props.username,
password: auth.props.password,
method: HttpMethod.GET,
version: 'v2',
resourceUri: `/spaces/${spaceId}`,
});
const folders = await confluencePaginatedApiCall<{id:string,title:string}>({
domain:auth.props.confluenceDomain,
username:auth.props.username,
password:auth.props.password,
version:'v1',
method:HttpMethod.GET,
resourceUri:`/content/${space.homepageId}/descendant/folder`,
})
const options:DropdownOption<string>[] = [];
for(const folder of folders){
options.push({
label:folder.title,
value:folder.id
})
}
return{
disabled:false,
options
}
}
})
export const templateVariablesProp = Property.DynamicProperties({
displayName: 'Template Variables',
auth: confluenceAuth,
refreshers: ['templateId'],
required: true,
props: async ({ auth, templateId }) => {
if (!auth) return {};
if (!templateId) return {};
const props: DynamicPropsValue = {};
const response = await confluenceApiCall<{ body: { storage: { value: string } } }>({
domain: auth.props.confluenceDomain,
username: auth.props.username,
password: auth.props.password,
method: HttpMethod.GET,
version: 'v1',
resourceUri: `/template/${templateId}`,
});
const parsedXml = await parseStringPromise(response.body.storage.value, {
explicitArray: false,
});
const declarations = parsedXml['at:declarations'];
if (!declarations) return {};
const variables: Array<{ name: string; type: string; options?: string[] }> = [];
Object.entries(declarations).forEach(([key, value]: [string, any]) => {
const type = key.replace('at:', '');
if (Array.isArray(value)) {
value.forEach((item) => {
if (item['$']) {
const varName = item['$']['at:name'];
let options: string[] | undefined;
if (type === 'list' && item['at:option']) {
options = item['at:option'].map((opt: any) => opt['$']['at:value']);
}
if (varName && type) {
variables.push({
name: varName,
type: type,
options: options,
});
}
}
});
} else if (value['$']) {
const varName = value['$']['at:name'];
let options: string[] | undefined;
if (type === 'list' && value['at:option']) {
options = value['at:option'].map((opt: any) => opt['$']['at:value']);
}
if (varName && type) {
variables.push({
name: varName,
type: type,
options: options,
});
}
}
});
for (const variable of variables) {
switch (variable.type) {
case 'list':
props[variable.name] = Property.StaticDropdown({
displayName: variable.name,
required: false,
defaultValue: '',
options: {
disabled: false,
options: variable.options
? variable.options.map((option) => {
return {
label: option,
value: option,
};
})
: [],
},
});
break;
case 'string':
props[variable.name] = Property.ShortText({
displayName: variable.name,
required: false,
defaultValue: '',
});
break;
case 'textarea':
props[variable.name] = Property.LongText({
displayName: variable.name,
required: false,
defaultValue: '',
});
break;
default:
break;
}
}
return props;
},
});

View File

@@ -0,0 +1,126 @@
import {
createTrigger,
TriggerStrategy,
PiecePropValueSchema,
AppConnectionValueForAuthProperty,
} from '@activepieces/pieces-framework';
import { DedupeStrategy, Polling, pollingHelper, HttpMethod } from '@activepieces/pieces-common';
import { confluenceAuth } from '../../index';
import { confluenceApiCall, confluencePaginatedApiCall, PaginatedResponse } from '../common';
import { isNil } from '@activepieces/shared';
import { spaceIdProp } from '../common/props';
interface ConfluencePage {
id: string;
status: string;
title: string;
spaceId: string;
createdAt: string;
version: {
number: number;
createdAt: string;
};
}
type Props = {
spaceId: string;
};
const polling: Polling<AppConnectionValueForAuthProperty<typeof confluenceAuth>, Props> = {
strategy: DedupeStrategy.TIMEBASED,
async items({ auth, propsValue, lastFetchEpochMS }) {
const pages = [];
if (lastFetchEpochMS === 0) {
const response = await confluenceApiCall<PaginatedResponse<ConfluencePage>>({
domain: auth.props.confluenceDomain,
username: auth.props.username,
password: auth.props.password,
version: 'v2',
method: HttpMethod.GET,
resourceUri: `/spaces/${propsValue.spaceId}/pages`,
query: {
limit: '10',
sort: '-created-date',
},
});
if (isNil(response.results)) {
return [];
}
pages.push(...response.results);
} else {
const response = await confluencePaginatedApiCall<ConfluencePage>({
domain: auth.props.confluenceDomain,
username: auth.props.username,
password: auth.props.password,
method: HttpMethod.GET,
version: 'v2',
resourceUri: `/spaces/${propsValue.spaceId}/pages`,
query: {
sort: '-created-date',
},
});
if (isNil(response)) {
return [];
}
pages.push(...response);
}
return pages.map((page) => {
return {
epochMilliSeconds: new Date(page.createdAt).getTime(),
data: page,
};
});
},
};
export const newPageTrigger = createTrigger({
name: 'new-page',
displayName: 'New Page',
description: 'Triggers when a new page is created.',
auth: confluenceAuth,
type: TriggerStrategy.POLLING,
props: {
spaceId: spaceIdProp,
},
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);
},
sampleData: {
parentType: 'page',
parentId: '123456',
spaceId: 'SAMPLE123',
ownerId: '12345678abcd',
lastOwnerId: null,
createdAt: '2024-01-01T12:00:00.000Z',
authorId: '12345678abcd',
position: 1000,
version: {
number: 1,
message: 'Initial version',
minorEdit: false,
authorId: '12345678abcd',
createdAt: '2024-01-01T12:00:00.000Z',
},
body: {},
status: 'current',
title: 'Sample Confluence Page',
id: '987654321',
_links: {
editui: '/pages/resumedraft.action?draftId=987654321',
webui: '/spaces/SAMPLE/pages/987654321/Sample+Confluence+Page',
edituiv2: '/spaces/SAMPLE/pages/edit-v2/987654321',
tinyui: '/x/abcd123',
},
},
});