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,206 @@
import { typeboxResolver } from '@hookform/resolvers/typebox';
import { useMutation } from '@tanstack/react-query';
import { t } from 'i18next';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { INTERNAL_ERROR_MESSAGE } from '@/components/ui/sonner';
import { Textarea } from '@/components/ui/textarea';
import { platformHooks } from '@/hooks/platform-hooks';
import { api } from '@/lib/api';
import { authenticationSession } from '@/lib/authentication-session';
import {
ConfigureRepoRequest,
GitBranchType,
GitRepo,
} from '@activepieces/ee-shared';
import { ApErrorParams, ErrorCode } from '@activepieces/shared';
import { gitSyncApi } from '../lib/git-sync-api';
import { gitSyncHooks } from '../lib/git-sync-hooks';
type ConnectGitProps = {
open?: boolean;
setOpen?: (open: boolean) => void;
showButton?: boolean;
};
const ConnectGitDialog = ({ open, setOpen, showButton }: ConnectGitProps) => {
const projectId = authenticationSession.getProjectId()!;
const { platform } = platformHooks.useCurrentPlatform();
const form = useForm<ConfigureRepoRequest>({
defaultValues: {
remoteUrl: '',
projectId,
branchType: GitBranchType.DEVELOPMENT,
sshPrivateKey: '',
slug: '',
branch: '',
},
resolver: typeboxResolver(ConfigureRepoRequest),
});
const { refetch } = gitSyncHooks.useGitSync(
projectId,
platform.plan.environmentsEnabled,
);
const { mutate, isPending } = useMutation({
mutationFn: (request: ConfigureRepoRequest): Promise<GitRepo> => {
return gitSyncApi.configure(request);
},
onSuccess: (repo) => {
refetch();
toast.success(t('Connected successfully'), {
duration: 3000,
});
},
onError: (error) => {
let message = INTERNAL_ERROR_MESSAGE;
if (api.isError(error)) {
const responseData = error.response?.data as ApErrorParams;
if (responseData.code === ErrorCode.INVALID_GIT_CREDENTIALS) {
message = `Invalid git credentials, please check the credentials, \n ${responseData.params.message}`;
}
}
form.setError('root.serverError', {
message: message,
});
return;
},
});
return (
<Dialog open={open} onOpenChange={setOpen} modal={true}>
{showButton && (
<DialogTrigger asChild>
<Button size={'sm'} className="w-32">
{t('Connect Git')}
</Button>
</DialogTrigger>
)}
<DialogContent className="sm:max-w-[500px]">
<Form {...form}>
<form
className="flex flex-col"
onSubmit={form.handleSubmit((data) => mutate(data))}
>
<DialogHeader>
<DialogTitle>{t('Connect Git')}</DialogTitle>
</DialogHeader>
<div className="grid gap-4">
<FormField
control={form.control}
name="remoteUrl"
render={({ field }) => (
<FormItem>
<FormLabel>{t('Remote URL')}</FormLabel>
<FormControl>
<Input
placeholder="git@github.com:activepieces/activepieces.git"
{...field}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="branch"
render={({ field }) => (
<FormItem>
<FormLabel>{t('Branch')}</FormLabel>
<FormControl>
<Input placeholder="main" {...field} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="slug"
render={({ field }) => (
<FormItem>
<FormLabel>{t('Folder')}</FormLabel>
<FormControl>
<Input placeholder="activepieces" {...field} />
</FormControl>
<FormDescription>
{t(
'Folder name is the name of the folder where the project will be stored or fetched.',
)}
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="sshPrivateKey"
render={({ field }) => (
<FormItem>
<FormLabel>{t('SSH Private Key')}</FormLabel>
<FormControl>
<Textarea
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"
{...field}
/>
</FormControl>
<FormDescription>
{t('The SSH private key to use for authentication.')}
</FormDescription>
</FormItem>
)}
/>
{form?.formState?.errors?.root?.serverError && (
<FormMessage>
{form.formState.errors.root.serverError.message}
</FormMessage>
)}
</div>
<DialogFooter>
<DialogClose>
<Button type="button" variant={'outline'} loading={isPending}>
{t('Cancel')}
</Button>
</DialogClose>
<Button
type="submit"
onClick={form.handleSubmit((data) => mutate(data))}
loading={isPending}
>
{t('Connect')}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
};
ConnectGitDialog.displayName = 'ConnectGitDialog';
export { ConnectGitDialog };

View File

@@ -0,0 +1,28 @@
import { t } from 'i18next';
import React from 'react';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
export const PublishedNeededTooltip = React.forwardRef<
HTMLButtonElement,
{ children: React.ReactNode; allowPush: boolean }
>(({ children, allowPush }, ref) => {
return (
<Tooltip delayDuration={100}>
<TooltipTrigger ref={ref} asChild disabled={!allowPush}>
<div>{children}</div>
</TooltipTrigger>
{!allowPush && (
<TooltipContent side="top">
{t('Only published flows can be pushed to Git')}
</TooltipContent>
)}
</Tooltip>
);
});
PublishedNeededTooltip.displayName = 'PublishedNeededWrapper';

View File

@@ -0,0 +1,168 @@
import { typeboxResolver } from '@hookform/resolvers/typebox';
import { useMutation } from '@tanstack/react-query';
import { t } from 'i18next';
import React from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
} from '@/components/ui/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
} from '@/components/ui/form';
import { Textarea } from '@/components/ui/textarea';
import { platformHooks } from '@/hooks/platform-hooks';
import { authenticationSession } from '@/lib/authentication-session';
import {
GitBranchType,
GitPushOperationType,
PushGitRepoRequest,
PushFlowsGitRepoRequest,
PushTablesGitRepoRequest,
} from '@activepieces/ee-shared';
import {
assertNotNullOrUndefined,
PopulatedFlow,
Table,
} from '@activepieces/shared';
import { gitSyncApi } from '../lib/git-sync-api';
import { gitSyncHooks } from '../lib/git-sync-hooks';
type PushToGitDialogProps =
| {
type: 'flow';
flows: PopulatedFlow[];
children?: React.ReactNode;
}
| {
type: 'table';
tables: Table[];
children?: React.ReactNode;
};
const PushToGitDialog = (props: PushToGitDialogProps) => {
const [open, setOpen] = React.useState(false);
const { platform } = platformHooks.useCurrentPlatform();
const { gitSync } = gitSyncHooks.useGitSync(
authenticationSession.getProjectId()!,
platform.plan.environmentsEnabled,
);
const form = useForm<PushGitRepoRequest>({
defaultValues: {
type:
props.type === 'flow'
? GitPushOperationType.PUSH_FLOW
: GitPushOperationType.PUSH_TABLE,
commitMessage: '',
externalFlowIds:
props.type === 'flow' ? props.flows.map((item) => item.externalId) : [],
externalTableIds:
props.type === 'table'
? props.tables.map((item) => item.externalId)
: [],
},
resolver: typeboxResolver(
props.type === 'flow'
? PushFlowsGitRepoRequest
: PushTablesGitRepoRequest,
),
});
const { mutate, isPending } = useMutation({
mutationFn: async (request: PushGitRepoRequest) => {
assertNotNullOrUndefined(gitSync, 'gitSync');
switch (props.type) {
case 'flow':
await gitSyncApi.push(gitSync.id, {
type: GitPushOperationType.PUSH_FLOW,
commitMessage: request.commitMessage,
externalFlowIds: props.flows.map((item) => item.externalId),
});
break;
case 'table':
await gitSyncApi.push(gitSync.id, {
type: GitPushOperationType.PUSH_TABLE,
commitMessage: request.commitMessage,
externalTableIds: props.tables.map((item) => item.externalId),
});
break;
}
},
onSuccess: () => {
toast.success(t('Pushed successfully'), {
duration: 3000,
});
setOpen(false);
},
});
if (!gitSync || gitSync.branchType !== GitBranchType.DEVELOPMENT) {
return null;
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{props.children}</DialogTrigger>
<DialogContent>
<Form {...form}>
<form onSubmit={form.handleSubmit((data) => mutate(data))}>
<DialogHeader>
<DialogTitle>{t('Push to Git')}</DialogTitle>
</DialogHeader>
<FormField
control={form.control}
name="commitMessage"
render={({ field }) => (
<FormItem className="gap-2 flex flex-col">
<FormLabel>{t('Commit Message')}</FormLabel>
<FormControl>
<Textarea {...field} />
</FormControl>
</FormItem>
)}
/>
<div className="text-sm text-gray-500 mt-2">
{t(
'Enter a commit message to describe the changes you want to push.',
)}
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => {
setOpen(false);
form.reset();
}}
>
{t('Cancel')}
</Button>
<Button
type="submit"
loading={isPending}
onClick={form.handleSubmit((data) => mutate(data))}
>
{t('Push')}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
};
PushToGitDialog.displayName = 'PushToGitDialog';
export { PushToGitDialog };

View File

@@ -0,0 +1,28 @@
import { api } from '@/lib/api';
import {
ConfigureRepoRequest,
GitRepo,
PushGitRepoRequest,
} from '@activepieces/ee-shared';
import { SeekPage } from '@activepieces/shared';
export const gitSyncApi = {
async get(projectId: string): Promise<GitRepo | null> {
const response = await api.get<SeekPage<GitRepo>>(`/v1/git-repos`, {
projectId,
});
if (response.data.length === 0) {
return null;
}
return response.data[0];
},
configure(request: ConfigureRepoRequest) {
return api.post<GitRepo>(`/v1/git-repos`, request);
},
disconnect(repoId: string) {
return api.delete<void>(`/v1/git-repos/${repoId}`);
},
push(repoId: string, request: PushGitRepoRequest) {
return api.post<void>(`/v1/git-repos/${repoId}/push`, request);
},
};

View File

@@ -0,0 +1,19 @@
import { useQuery } from '@tanstack/react-query';
import { gitSyncApi } from './git-sync-api';
export const gitSyncHooks = {
useGitSync: (projectId: string, enabled: boolean) => {
const query = useQuery({
queryKey: ['git-sync', projectId],
queryFn: () => gitSyncApi.get(projectId),
staleTime: Infinity,
enabled: enabled,
});
return {
gitSync: query.data,
isLoading: query.isLoading,
refetch: query.refetch,
};
},
};

View File

@@ -0,0 +1,29 @@
import { api } from '@/lib/api';
import {
ProjectSyncPlan,
SeekPage,
CreateProjectReleaseRequestBody,
ProjectRelease,
DiffReleaseRequest,
} from '@activepieces/shared';
export const projectReleaseApi = {
async get(releaseId: string) {
return await api.get<ProjectRelease>(`/v1/project-releases/${releaseId}`);
},
async list() {
return await api.get<SeekPage<ProjectRelease>>(`/v1/project-releases`);
},
async create(requestBody: CreateProjectReleaseRequestBody) {
return await api.post<ProjectRelease>('/v1/project-releases', requestBody);
},
async delete(id: string) {
return await api.delete<void>(`/v1/project-releases/${id}`);
},
async diff(request: DiffReleaseRequest) {
return await api.post<ProjectSyncPlan>(
`/v1/project-releases/diff`,
request,
);
},
};