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,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 };
|
||||
@@ -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';
|
||||
@@ -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 };
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user