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,360 @@
import {
createAction,
DynamicPropsValue,
Property,
} from '@activepieces/pieces-framework';
import { LEVER_BASE_URL, LeverAuth, leverAuth } from '../..';
import {
AuthenticationType,
httpClient,
HttpMethod,
} from '@activepieces/pieces-common';
import { LeverFieldMapping } from '../common';
export const addFeedbackToOpportunity = createAction({
name: 'addFeedbackToOpportunity',
displayName: 'Add feedback to opportunity',
description: 'Provide feedback to a candidate after an interview',
auth: leverAuth,
props: {
performAs: Property.Dropdown({
auth: leverAuth,
displayName: 'Feedback author',
required: true,
refreshers: ['auth'],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect first.',
options: [],
};
}
const users = [];
let cursor = undefined;
do {
const queryParams: Record<string, string> = {
include: 'name',
};
if (cursor) {
queryParams['offset'] = cursor;
}
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${LEVER_BASE_URL}/users`,
queryParams: queryParams,
authentication: {
type: AuthenticationType.BASIC,
username: auth.props.apiKey,
password: '',
},
});
cursor = response.body.next;
const usersPage = response.body.data.map(
(user: { id: string; name: string }) => {
return {
label: user.name,
value: user.id,
};
}
);
users.push(...usersPage);
} while (cursor !== undefined);
return {
options: users,
};
},
}),
opportunityId: Property.ShortText({
displayName: 'Opportunity ID',
required: true,
}),
panelId: Property.Dropdown({
auth: leverAuth,
displayName: 'Interview panel',
description: 'If you select one, you must select an interview too',
required: false,
refreshers: ['auth', 'opportunityId'],
options: async ({ auth, opportunityId }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect first.',
options: [],
};
}
if (!opportunityId) {
return {
disabled: true,
placeholder: 'Please select a candidate (opportunity).',
options: [],
};
}
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${LEVER_BASE_URL}/opportunities/${opportunityId}/panels?expand=stage&include=id&include=stage&include=start`,
authentication: {
type: AuthenticationType.BASIC,
username: auth.props.apiKey,
password: '',
},
});
return {
options: response.body.data.map(
(panel: { id: string; start: number; stage: { text: string } }) => {
const interviewDate = new Date(panel.start);
return {
label: `${interviewDate.toLocaleDateString()} - ${
panel.stage.text
}`,
value: panel.id,
};
}
),
};
},
}),
interviewId: Property.Dropdown({
auth: leverAuth,
displayName: 'Interview',
description: 'Mandatory is you select an interview panel',
required: false,
refreshers: ['auth', 'opportunityId', 'panelId'],
options: async ({ auth, opportunityId, panelId }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect first.',
options: [],
};
}
if (!opportunityId) {
return {
disabled: true,
placeholder: 'Please select a candidate (opportunity).',
options: [],
};
}
if (!panelId) {
return {
disabled: true,
placeholder: 'Please select an interview panel.',
options: [],
};
}
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${LEVER_BASE_URL}/opportunities/${opportunityId}/panels/${panelId}?include=interviews`,
authentication: {
type: AuthenticationType.BASIC,
username: auth.props.apiKey,
password: '',
},
});
return {
options: response.body.data.interviews.map(
(interview: { id: string; subject: string }) => {
return { label: interview.subject, value: interview.id };
}
),
};
},
}),
feedbackTemplateId: Property.Dropdown({
auth: leverAuth,
displayName: 'Feedback template',
description: 'Ignored if you select an interview panel and an interview',
required: false,
refreshers: ['auth'],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect first.',
options: [],
};
}
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${LEVER_BASE_URL}/feedback_templates`,
authentication: {
type: AuthenticationType.BASIC,
username: auth.props.apiKey,
password: '',
},
});
return {
options: response.body.data.map(
(template: { id: string; text: string }) => {
return {
label: template.text,
value: template.id,
};
}
),
};
},
}),
feedbackFields: Property.DynamicProperties({
auth: leverAuth,
displayName: 'Fields',
required: true,
refreshers: [
'auth',
'opportunityId',
'panelId',
'interviewId',
'feedbackTemplateId',
],
props: async ({
auth,
opportunityId,
panelId,
interviewId,
feedbackTemplateId,
}) => {
if (
!auth ||
!opportunityId ||
!(feedbackTemplateId || (panelId && interviewId))
) {
return {
disabled: true,
placeholder:
'Please connect your Lever account first and select an interview or a feedback template',
options: [],
};
}
const fields: DynamicPropsValue = {};
const templateId =
panelId && interviewId
? await getFeedbackTemplateForInterview(
opportunityId,
panelId,
interviewId,
auth
)
: feedbackTemplateId;
try {
const feedbackTemplateResponse = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${LEVER_BASE_URL}/feedback_templates/${templateId}`,
authentication: {
type: AuthenticationType.BASIC,
username: auth.props.apiKey,
password: '',
},
});
feedbackTemplateResponse.body.data.fields.map(
(field: {
id: string;
text: string;
description: string;
required: boolean;
type: string;
options?: { text: string; optionId: string }[];
scores?: { text: string; description: string }[];
}) => {
const mappedField =
LeverFieldMapping[field.type] || LeverFieldMapping['default'];
mappedField.buildActivepieceType(fields, field);
}
);
} catch (e) {
console.error(
'Unexpected error while building dynamic properties',
e
);
}
return fields;
},
}),
},
async run({ auth, propsValue }) {
const templateId =
propsValue.panelId && propsValue.interviewId
? await getFeedbackTemplateForInterview(
propsValue.opportunityId,
propsValue.panelId,
propsValue.interviewId,
auth
)
: propsValue.feedbackTemplateId;
const feedbackTemplateResponse = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${LEVER_BASE_URL}/feedback_templates/${templateId}`,
authentication: {
type: AuthenticationType.BASIC,
username: auth.props.apiKey,
password: '',
},
});
const templateFields = feedbackTemplateResponse.body.data.fields;
const groupedValues = Object.entries(propsValue.feedbackFields).reduce<
Record<string, DynamicPropsValue[]>
>((values, [fieldId, fieldValue]: [string, DynamicPropsValue]) => {
const canonicalId = fieldId.substring(0, 36);
values[canonicalId] = values[canonicalId] ?? [];
values[canonicalId].push(fieldValue);
return values;
}, {});
const payload = {
baseTemplateId: templateId,
panel: propsValue.panelId,
interview: propsValue.interviewId,
fieldValues: Object.entries(groupedValues).map(([fieldId, values]) => {
const templateField = templateFields.find(
(tf: { id: string }) => tf.id === fieldId
);
const mappedField =
templateField.type in LeverFieldMapping
? LeverFieldMapping[templateField.type]
: LeverFieldMapping['default'];
return mappedField.buildLeverType(fieldId, values);
}),
};
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${LEVER_BASE_URL}/opportunities/${propsValue.opportunityId}/feedback?perform_as=${propsValue.performAs}`,
authentication: {
type: AuthenticationType.BASIC,
username: auth.props.apiKey,
password: '',
},
body: payload,
});
return response.body.data;
},
});
async function getFeedbackTemplateForInterview(
opportunityId: string | DynamicPropsValue,
panelId: string | DynamicPropsValue,
interviewId: string | DynamicPropsValue,
auth: LeverAuth
) {
const interviewResponse = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${LEVER_BASE_URL}/opportunities/${opportunityId}/panels/${panelId}?include=interviews`,
authentication: {
type: AuthenticationType.BASIC,
username: auth.props.apiKey,
password: '',
},
});
const interview = interviewResponse.body.data.interviews.find(
(interview: { id: string }) =>
interview.id === (interviewId as unknown as string)
);
return interview.feedbackTemplate;
}

View File

@@ -0,0 +1,42 @@
import qs from 'qs';
import { createAction, Property } from '@activepieces/pieces-framework';
import { LEVER_BASE_URL, leverAuth } from '../..';
import {
AuthenticationType,
httpClient,
HttpMethod,
} from '@activepieces/pieces-common';
export const getOpportunity = createAction({
name: 'getOpportunity',
displayName: 'Get opportunity',
description:
"Retrieve a single opportunity, i.e. an individual's unique candidacy or journey for a given job position",
auth: leverAuth,
props: {
opportunityId: Property.ShortText({
displayName: 'Opportunity ID',
required: true,
}),
expand: Property.Array({
displayName: 'Expand',
required: false,
}),
},
async run({ auth, propsValue }) {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${LEVER_BASE_URL}/opportunities/${
propsValue.opportunityId
}?${decodeURIComponent(
qs.stringify({ expand: propsValue.expand }, { arrayFormat: 'repeat' })
)}`,
authentication: {
type: AuthenticationType.BASIC,
username: auth.props.apiKey,
password: '',
},
});
return response.body.data;
},
});

View File

@@ -0,0 +1,71 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { LEVER_BASE_URL, LeverAuth, leverAuth } from '../..';
import {
AuthenticationType,
httpClient,
HttpMethod,
} from '@activepieces/pieces-common';
export const listOpportunityFeedback = createAction({
name: 'listOpportunityFeedback',
displayName: 'List opportunity feedback',
description:
'Get all feedback for a given opportunity, optionally for a given template',
auth: leverAuth,
props: {
opportunityId: Property.ShortText({
displayName: 'Opportunity ID',
required: true,
}),
template: Property.Dropdown({
auth: leverAuth,
displayName: 'Feedback template',
required: false,
refreshers: ['auth'],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect first.',
options: [],
};
}
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${LEVER_BASE_URL}/feedback_templates?include=text`,
authentication: {
type: AuthenticationType.BASIC,
username: auth.props.apiKey,
password: '',
},
});
return {
options: response.body.data.map(
(template: { text: string; id: string }) => {
return { label: template.text, value: template.id };
}
),
};
},
}),
},
async run({ auth, propsValue }) {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${LEVER_BASE_URL}/opportunities/${propsValue.opportunityId}/feedback`,
authentication: {
type: AuthenticationType.BASIC,
username: auth.props.apiKey,
password: '',
},
});
const feedback = response.body.data;
if (propsValue.template) {
return feedback.filter(
(form: { baseTemplateId: string }) =>
form.baseTemplateId === propsValue.template
);
}
return feedback;
},
});

View File

@@ -0,0 +1,71 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { LEVER_BASE_URL, LeverAuth, leverAuth } from '../..';
import {
AuthenticationType,
httpClient,
HttpMethod,
} from '@activepieces/pieces-common';
export const listOpportunityForms = createAction({
name: 'listOpportunityForms',
displayName: 'List opportunity forms',
description:
'Get all forms for a given opportunity, optionally for a given form template',
auth: leverAuth,
props: {
opportunityId: Property.ShortText({
displayName: 'Opportunity ID',
required: true,
}),
template: Property.Dropdown({
auth: leverAuth,
displayName: 'Form template',
required: false,
refreshers: ['auth'],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect first.',
options: [],
};
}
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${LEVER_BASE_URL}/form_templates?include=text`,
authentication: {
type: AuthenticationType.BASIC,
username: auth.props.apiKey,
password: '',
},
});
return {
options: response.body.data.map(
(template: { text: string; id: string }) => {
return { label: template.text, value: template.id };
}
),
};
},
}),
},
async run({ auth, propsValue }) {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${LEVER_BASE_URL}/opportunities/${propsValue.opportunityId}/forms`,
authentication: {
type: AuthenticationType.BASIC,
username: auth.props.apiKey,
password: '',
},
});
const forms = response.body.data;
if (propsValue.template) {
return forms.filter(
(form: { baseTemplateId: string }) =>
form.baseTemplateId === propsValue.template
);
}
return forms;
},
});

View File

@@ -0,0 +1,65 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
AuthenticationType,
httpClient,
HttpMethod,
} from '@activepieces/pieces-common';
import { LEVER_BASE_URL, LeverAuth, leverAuth } from '../..';
export const updateOpportunityStage = createAction({
name: 'updateOpportunityStage',
displayName: 'Update opportunity stage',
description: "Change an Opportunity's current stage",
auth: leverAuth,
props: {
opportunityId: Property.ShortText({
displayName: 'Opportunity ID',
required: true,
}),
stage: Property.Dropdown({
auth: leverAuth,
displayName: 'Stage',
required: true,
refreshers: ['auth'],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect first.',
options: [],
};
}
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${LEVER_BASE_URL}/stages`,
authentication: {
type: AuthenticationType.BASIC,
username: auth.props.apiKey,
password: '',
},
});
return {
options: response.body.data.map(
(stage: { text: string; id: string }) => {
return { label: stage.text, value: stage.id };
}
),
};
},
}),
},
async run({ auth, propsValue }) {
const response = await httpClient.sendRequest({
method: HttpMethod.PUT,
url: `${LEVER_BASE_URL}/opportunities/${propsValue.opportunityId}/stage`,
authentication: {
type: AuthenticationType.BASIC,
username: auth.props.apiKey,
password: '',
},
body: { stage: propsValue.stage },
});
return response.body.data;
},
});

View File

@@ -0,0 +1,176 @@
import { DynamicPropsValue, Property } from '@activepieces/pieces-framework';
export type LeverFieldType = {
id: string;
text: string;
description: string;
required: boolean;
type: string;
options?: { text: string; optionId: string }[];
scores?: { text: string; description: string }[];
};
export const LeverFieldMapping: Record<
string,
{
buildActivepieceType: (
fields: DynamicPropsValue,
field: LeverFieldType
) => void;
buildLeverType: (
id: string,
propsValues: DynamicPropsValue[]
) => {
id: string;
value:
| string
| string[]
| number
| number[]
| { score: number; comment?: string }[];
};
}
> = {
default: {
buildActivepieceType: (fields, field) =>
(fields[field.id] = Property.ShortText({
displayName: field.text,
description: field.description,
required: field.required,
})),
buildLeverType: (id, propsValues) => ({
id,
value: propsValues[0] as unknown as string,
}),
},
textarea: {
buildActivepieceType: (fields, field) =>
(fields[field.id] = Property.LongText({
displayName: field.text,
description: field.description,
required: field.required,
})),
buildLeverType: (id, propsValues) => ({
id,
value: propsValues[0] as unknown as string,
}),
},
'yes-no': {
buildActivepieceType: (fields, field) =>
(fields[field.id] = Property.Checkbox({
displayName: field.text,
description: field.description,
required: field.required,
})),
buildLeverType: (id, propsValues) => {
const value = propsValues[0] as unknown as boolean;
return {
id,
value: value === true ? 'yes' : value === false ? 'no' : 'null',
};
},
},
dropdown: {
buildActivepieceType: (fields, field) =>
(fields[field.id] = Property.StaticDropdown({
displayName: field.text,
description: field.description,
required: field.required,
options: {
disabled: false,
options:
field.options?.map((option: { text: string; optionId: string }) => {
return { value: option.text, label: option.text };
}) || [],
},
})),
buildLeverType: (id, propsValues) => ({
id,
value: propsValues[0] as unknown as string,
}),
},
'multiple-choice': {
buildActivepieceType: (fields, field) =>
(fields[field.id] = Property.StaticDropdown({
displayName: field.text,
description: field.description,
required: field.required,
options: {
disabled: false,
options:
field.options?.map((option: { text: string; optionId: string }) => {
return { value: option.text, label: option.text };
}) || [],
},
})),
buildLeverType: (id, propsValues) => ({
id,
value: propsValues[0] as unknown as string,
}),
},
'multiple-select': {
buildActivepieceType: (fields, field) =>
(fields[field.id] = Property.StaticMultiSelectDropdown({
displayName: field.text,
description: field.description,
required: field.required,
options: {
disabled: false,
options:
field.options?.map((option: { text: string; optionId: string }) => {
return { value: option.text, label: option.text };
}) || [],
},
})),
buildLeverType: (id, propsValues) => ({
id,
value: propsValues[0] as unknown as string[],
}),
},
'score-system': {
buildActivepieceType: (fields, field) =>
(fields[field.id] = Property.StaticDropdown({
displayName: field.text,
description: field.description,
required: field.required,
options: {
options:
field.options?.map((option) => {
return { value: option.text, label: option.text };
}) || [],
},
})),
buildLeverType: (id, propsValues) => ({
id,
value: propsValues[0] as unknown as string,
}),
},
scorecard: {
buildActivepieceType: (fields, field) =>
field.scores?.map((score, index) => {
fields[`${field.id}-${index}`] = Property.StaticDropdown({
displayName: score.text,
description: score.description,
required: field.required,
options: {
options: [
{ label: 'n.a.', value: 0 },
{ label: '👎👎', value: 1 },
{ label: '👎', value: 2 },
{ label: '👍', value: 3 },
{ label: '👍👍', value: 4 },
],
},
});
}),
buildLeverType: (id, propsValues) => ({
id,
value: propsValues.map((propsValue: DynamicPropsValue) => {
return {
score: propsValue as unknown as number,
comment: '',
};
}),
}),
},
};