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,85 @@
import { useMutation } from '@tanstack/react-query';
import { HttpStatusCode } from 'axios';
import { t } from 'i18next';
import { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { internalErrorToast } from '@/components/ui/sonner';
import { LoadingSpinner } from '@/components/ui/spinner';
import { api } from '../../../lib/api';
import { userInvitationApi } from '../lib/user-invitation';
const AcceptInvitation = () => {
const [isInvitationLinkValid, setIsInvitationLinkValid] = useState(true);
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { mutate, isPending } = useMutation({
mutationFn: async (token: string) => {
const { registered } = await userInvitationApi.accept(token);
return registered;
},
onSuccess: (registered) => {
setIsInvitationLinkValid(true);
if (!registered) {
setTimeout(() => {
const email = searchParams.get('email');
navigate(`/sign-up?email=${email}`);
}, 3000);
} else {
navigate('/sign-in');
}
},
onError: (error) => {
setIsInvitationLinkValid(false);
if (api.isError(error)) {
switch (error.response?.status) {
case HttpStatusCode.InternalServerError: {
console.log(error);
internalErrorToast();
break;
}
default: {
break;
}
}
}
},
});
useEffect(() => {
const invitationToken = searchParams.get('token');
if (!invitationToken) {
setIsInvitationLinkValid(false);
return;
}
mutate(invitationToken);
}, [mutate, searchParams]);
return isPending ? (
<div className="w-screen h-screen flex justify-center items-center">
<LoadingSpinner isLarge={true}></LoadingSpinner>
</div>
) : (
<div className="container mx-auto mt-10 max-w-md">
{isInvitationLinkValid ? (
<>
<p className="text-2xl font-bold text-center">
{t('Team Invitation Accepted')}
</p>
<p className="mt-4 text-lg text-center text-gray-700">
{t(
'Thank you for accepting the invitation. We are redirecting you right now...',
)}
</p>
</>
) : (
<p className="mt-4 text-lg text-center text-red-500">
{t('Invalid invitation token. Please try again.')}
</p>
)}
</div>
);
};
AcceptInvitation.displayName = 'AcceptInvitation';
export { AcceptInvitation };

View File

@@ -0,0 +1,110 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { t } from 'i18next';
import { Pencil } from 'lucide-react';
import { useState } from 'react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
} from '@/components/ui/dialog';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { internalErrorToast } from '@/components/ui/sonner';
import { projectRoleApi } from '@/features/platform-admin/lib/project-role-api';
import { ProjectMemberWithUser } from '@activepieces/ee-shared';
import { projectMembersApi } from '../lib/project-members-api';
interface EditRoleDialogProps {
member: ProjectMemberWithUser;
onSave: () => void;
disabled: boolean;
}
export function EditRoleDialog({
member,
onSave,
disabled,
}: EditRoleDialogProps) {
const [isOpen, setIsOpen] = useState(false);
const [selectedRole, setSelectedRole] = useState(member.projectRole.name);
const { data: rolesData } = useQuery({
queryKey: ['project-roles'],
queryFn: () => projectRoleApi.list(),
});
const roles = rolesData?.data ?? [];
const { mutate, isPending } = useMutation({
mutationFn: (newRole: string) => {
return projectMembersApi.update(member.id, {
role: newRole,
});
},
onSuccess: () => {
toast.success(t('Role updated successfully'), {
duration: 3000,
});
onSave();
setIsOpen(false);
},
onError: () => {
internalErrorToast();
},
});
const handleRoleChange = (newRole: string) => {
setSelectedRole(newRole);
};
const handleSave = () => {
mutate(selectedRole);
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" className="size-8 p-0" disabled={disabled}>
<Pencil className="size-4" />
</Button>
</DialogTrigger>
<DialogContent className="w-full max-w-md">
<DialogHeader>
<DialogTitle>
{t('Edit Role for')} {member.user.firstName} {member.user.lastName}
</DialogTitle>
</DialogHeader>
<div className="grid gap-2">
<Select onValueChange={handleRoleChange} defaultValue={selectedRole}>
<SelectTrigger>
<SelectValue placeholder={t('Select Role')} />
</SelectTrigger>
<SelectContent>
{roles.map((role) => (
<SelectItem key={role.name} value={role.name}>
{role.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<DialogFooter>
<Button onClick={handleSave} loading={isPending}>
{t('Save')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,64 @@
import { t } from 'i18next';
import { Trash } from 'lucide-react';
import { PermissionNeededTooltip } from '@/components/custom/permission-needed-tooltip';
import { UserAvatar } from '@/components/ui/user-avatar';
import { useAuthorization } from '@/hooks/authorization-hooks';
import { Permission, UserInvitation } from '@activepieces/shared';
import { ConfirmationDeleteDialog } from '../../../components/delete-dialog';
import { Button } from '../../../components/ui/button';
import { userInvitationApi } from '../lib/user-invitation';
import { userInvitationsHooks } from '../lib/user-invitations-hooks';
export function InvitationCard({ invitation }: { invitation: UserInvitation }) {
const { refetch } = userInvitationsHooks.useInvitations();
const { checkAccess } = useAuthorization();
const userHasPermissionToRemoveInvitation = checkAccess(
Permission.WRITE_INVITATION,
);
async function deleteInvitation() {
await userInvitationApi.delete(invitation.id);
refetch();
}
return (
<div
className="flex items-center justify-between space-x-4"
key={invitation.id}
>
<div className="flex items-center space-x-4">
<UserAvatar
name={invitation.email}
email={invitation.email}
size={32}
disableTooltip={true}
></UserAvatar>
<div>
<p className="text-sm font-medium leading-none">
{invitation.email} ({invitation.projectRole?.name})
</p>
</div>
</div>
<div className="flex gap-2">
<PermissionNeededTooltip
hasPermission={userHasPermissionToRemoveInvitation}
>
<ConfirmationDeleteDialog
mutationFn={() => deleteInvitation()}
entityName={invitation.email}
title={t('Remove {email}', { email: invitation.email })}
message={t('Are you sure you want to remove this invitation?')}
>
<Button
disabled={!userHasPermissionToRemoveInvitation}
variant="ghost"
className="size-8 p-0"
>
<Trash className="text-destructive size-4" />
</Button>
</ConfirmationDeleteDialog>
</PermissionNeededTooltip>
</div>
</div>
);
}

View File

@@ -0,0 +1,361 @@
import { typeboxResolver } from '@hookform/resolvers/typebox';
import { Static, Type } from '@sinclair/typebox';
import { useMutation, useQuery } from '@tanstack/react-query';
import { t } from 'i18next';
import { CopyIcon } from 'lucide-react';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { useEmbedding } from '@/components/embed-provider';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { FormField, FormItem, Form, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { PlatformRoleSelect } from '@/features/members/component/platform-role-select';
import { userInvitationApi } from '@/features/members/lib/user-invitation';
import { projectRoleApi } from '@/features/platform-admin/lib/project-role-api';
import { useAuthorization } from '@/hooks/authorization-hooks';
import { platformHooks } from '@/hooks/platform-hooks';
import { projectHooks } from '@/hooks/project-hooks';
import { userHooks } from '@/hooks/user-hooks';
import { HttpError } from '@/lib/api';
import { formatUtils } from '@/lib/utils';
import {
InvitationType,
isNil,
Permission,
PlatformRole,
UserInvitationWithLink,
} from '@activepieces/shared';
import { userInvitationsHooks } from '../lib/user-invitations-hooks';
const FormSchema = Type.Object({
email: Type.String({
errorMessage: t('Please enter a valid email address'),
pattern: formatUtils.emailRegex.source,
}),
type: Type.Enum(InvitationType, {
errorMessage: t('Please select invitation type'),
required: true,
}),
platformRole: Type.Enum(PlatformRole, {
errorMessage: t('Please select platform role'),
required: true,
}),
projectRole: Type.Optional(
Type.String({
required: true,
}),
),
});
type FormSchema = Static<typeof FormSchema>;
export const InviteUserDialog = ({
open,
setOpen,
}: {
open: boolean;
setOpen: (_open: boolean) => void;
}) => {
const { embedState } = useEmbedding();
const [invitationLink, setInvitationLink] = useState('');
const { platform } = platformHooks.useCurrentPlatform();
const { refetch } = userInvitationsHooks.useInvitations();
const { project } = projectHooks.useCurrentProject();
const { data: currentUser } = userHooks.useCurrentUser();
const { checkAccess } = useAuthorization();
const userHasPermissionToInviteUser = checkAccess(
Permission.WRITE_INVITATION,
);
const { mutate, isPending } = useMutation<
UserInvitationWithLink,
HttpError,
FormSchema
>({
mutationFn: (data) => {
switch (data.type) {
case InvitationType.PLATFORM:
return userInvitationApi.invite({
email: data.email.trim().toLowerCase(),
type: data.type,
platformRole: data.platformRole,
});
case InvitationType.PROJECT:
return userInvitationApi.invite({
email: data.email.trim().toLowerCase(),
type: data.type,
projectRole: data.projectRole!,
projectId: project.id,
});
}
},
onSuccess: (res) => {
if (res.link) {
setInvitationLink(res.link);
} else {
setOpen(false);
toast.success(t('Invitation sent successfully'), {
duration: 3000,
});
}
refetch();
//TODO: navigate to platform admin users
},
});
const { data: rolesData } = useQuery({
queryKey: ['project-roles'],
queryFn: () => projectRoleApi.list(),
enabled:
!isNil(platform.plan.projectRolesEnabled) &&
platform.plan.projectRolesEnabled,
});
const roles = rolesData?.data ?? [];
const form = useForm<FormSchema>({
resolver: typeboxResolver(FormSchema),
defaultValues: {
email: '',
type: platform.plan.projectRolesEnabled
? InvitationType.PROJECT
: InvitationType.PLATFORM,
platformRole: PlatformRole.ADMIN,
projectRole: roles?.[0]?.name,
},
});
const onSubmit = (data: FormSchema) => {
if (data.type === InvitationType.PROJECT && !data.projectRole) {
form.setError('projectRole', {
type: 'required',
message: t('Please select a project role'),
});
return;
}
mutate(data);
};
const copyInvitationLink = () => {
navigator.clipboard.writeText(invitationLink);
toast.success(t('Invitation link copied successfully'), {
duration: 3000,
});
};
if (embedState.isEmbedded || !userHasPermissionToInviteUser) {
return null;
}
return (
<>
{
<Dialog
open={open}
modal
onOpenChange={(open) => {
setOpen(open);
if (open) {
form.reset();
setInvitationLink('');
}
}}
>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>
{invitationLink ? t('Invitation Link') : t('Invite User')}
</DialogTitle>
<DialogDescription>
{invitationLink
? t(
'Please copy the link below and share it with the user you want to invite, the invitation expires in 24 hours.',
)
: t(
'Type the email address of the user you want to invite, the invitation expires in 24 hours.',
)}
</DialogDescription>
</DialogHeader>
{!invitationLink ? (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem className="grid gap-2">
<Label htmlFor="email">{t('Email')}</Label>
<Input
{...field}
type="text"
placeholder="jon@doe.com"
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem className="grid gap-2">
<Label>{t('Invite To')}</Label>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder={t('Invite To')} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t('Invite To')}</SelectLabel>
{currentUser?.platformRole ===
PlatformRole.ADMIN && (
<SelectItem value={InvitationType.PLATFORM}>
{t('Entire Platform')}
</SelectItem>
)}
{platform.plan.projectRolesEnabled && (
<SelectItem value={InvitationType.PROJECT}>
{project.displayName} (Current)
</SelectItem>
)}
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
></FormField>
{form.getValues().type === InvitationType.PLATFORM && (
<PlatformRoleSelect form={form} />
)}
{form.getValues().type === InvitationType.PROJECT && (
<FormField
control={form.control}
name="projectRole"
render={({ field }) => (
<FormItem className="grid gap-2">
<Label>{t('Select Project Role')}</Label>
<Select
onValueChange={(value) => {
const selectedRole = roles.find(
(role) => role.name === value,
);
field.onChange(selectedRole?.name);
}}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder={t('Select Role')} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t('Roles')}</SelectLabel>
{roles.map((role) => (
<SelectItem key={role.name} value={role.name}>
{role.name}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
{form?.formState?.errors?.root?.serverError && (
<FormMessage>
{form.formState.errors.root.serverError.message}
</FormMessage>
)}
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant={'outline'}>
{t('Cancel')}
</Button>
</DialogClose>
<Button type="submit" loading={isPending}>
{t('Invite')}
</Button>
</DialogFooter>
</form>
</Form>
) : (
<>
<Label htmlFor="invitationLink" className="mb-2">
{t('Invitation Link')}
</Label>
<div className="flex">
<Input
name="invitationLink"
type="text"
readOnly={true}
defaultValue={invitationLink}
placeholder={t('Invitation Link')}
onFocus={(event) => {
event.target.select();
copyInvitationLink();
}}
className=" rounded-l-md rounded-r-none focus-visible:ring-0! focus-visible:ring-offset-0!"
/>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant={'outline'}
className=" rounded-l-none rounded-r-md"
onClick={copyInvitationLink}
>
<CopyIcon height={15} width={15}></CopyIcon>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">{t('Copy')}</TooltipContent>
</Tooltip>
</div>
</>
)}
</DialogContent>
</Dialog>
}
</>
);
};

View File

@@ -0,0 +1,47 @@
import { t } from 'i18next';
import { UseFormReturn } from 'react-hook-form';
import { FormField, FormItem, FormMessage } from '@/components/ui/form';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { PlatformRole } from '@activepieces/shared';
type PlatformRoleSelectProps = {
form: UseFormReturn<any>;
};
export const PlatformRoleSelect = ({ form }: PlatformRoleSelectProps) => {
return (
<FormField
control={form.control}
name="platformRole"
render={({ field }) => (
<FormItem className="grid gap-3">
<Label>{t('Platform Role')}</Label>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<SelectTrigger>
<SelectValue placeholder={t('Select a platform role')} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t('Platform Role')}</SelectLabel>
<SelectItem value={PlatformRole.ADMIN}>{t('Admin')}</SelectItem>
<SelectItem value={PlatformRole.OPERATOR}>
{t('Operator')}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
></FormField>
);
};

View File

@@ -0,0 +1,92 @@
import { t } from 'i18next';
import { Trash } from 'lucide-react';
import { PermissionNeededTooltip } from '@/components/custom/permission-needed-tooltip';
import { UserAvatar } from '@/components/ui/user-avatar';
import { useAuthorization } from '@/hooks/authorization-hooks';
import { projectHooks } from '@/hooks/project-hooks';
import { ProjectMemberWithUser } from '@activepieces/ee-shared';
import { Permission } from '@activepieces/shared';
import { ConfirmationDeleteDialog } from '../../../components/delete-dialog';
import { Button } from '../../../components/ui/button';
import { projectMembersApi } from '../lib/project-members-api';
import { projectMembersHooks } from '../lib/project-members-hooks';
import { EditRoleDialog } from './edit-role-dialog';
type ProjectMemberCardProps = {
member: ProjectMemberWithUser;
onUpdate: () => void;
};
export function ProjectMemberCard({
member,
onUpdate,
}: ProjectMemberCardProps) {
const { refetch } = projectMembersHooks.useProjectMembers();
const { checkAccess } = useAuthorization();
const userHasPermissionToRemoveMember = checkAccess(
Permission.WRITE_PROJECT_MEMBER,
);
const { project } = projectHooks.useCurrentProject();
const deleteMember = async () => {
await projectMembersApi.delete(member.id);
refetch();
onUpdate();
};
return (
<div
className="w-full flex items-center justify-between space-x-4"
key={member.id}
>
<div className="flex items-center space-x-4">
<UserAvatar
name={member.user.firstName + ' ' + member.user.lastName}
email={member.user.email}
size={32}
disableTooltip={true}
></UserAvatar>
<div className="flex flex-col gap-1">
<p className="text-sm font-medium leading-none">
{member.user.firstName} {member.user.lastName} (
{member.projectRole.name})
</p>
<p className="text-sm text-muted-foreground">{member.user.email}</p>
</div>
</div>
<div className="flex items-center gap-2">
{project.ownerId !== member.userId && (
<PermissionNeededTooltip
hasPermission={userHasPermissionToRemoveMember}
>
<EditRoleDialog
member={member}
onSave={() => {
refetch();
}}
disabled={!userHasPermissionToRemoveMember}
/>
<ConfirmationDeleteDialog
title={`${t('Remove')} ${member.user.firstName} ${
member.user.lastName
}`}
message={t('Are you sure you want to remove this member?')}
mutationFn={() => deleteMember()}
entityName={`${member.user.firstName} ${member.user.lastName}`}
>
<Button
disabled={!userHasPermissionToRemoveMember}
variant="ghost"
className="size-8 p-0"
>
<Trash className="text-destructive size-4" />
</Button>
</ConfirmationDeleteDialog>
</PermissionNeededTooltip>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,79 @@
import { t } from 'i18next';
import { UseFormReturn } from 'react-hook-form';
import { FormField, FormItem, FormMessage } from '@/components/ui/form';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { platformHooks } from '@/hooks/platform-hooks';
import { DefaultProjectRole } from '@activepieces/shared';
type ProjectRoleSelectProps = {
form: UseFormReturn<any>;
};
const RolesDisplayNames: { [k: string]: string } = {
[DefaultProjectRole.ADMIN]: t('Admin'),
[DefaultProjectRole.EDITOR]: t('Editor'),
[DefaultProjectRole.OPERATOR]: t('Operator'),
[DefaultProjectRole.VIEWER]: t('Viewer'),
};
const ProjectRoleSelect = ({ form }: ProjectRoleSelectProps) => {
const { platform } = platformHooks.useCurrentPlatform();
const invitationRoles = Object.values(DefaultProjectRole)
.filter((f) => {
if (f === DefaultProjectRole.ADMIN) {
return true;
}
const showNonAdmin = platform.plan.projectRolesEnabled;
return showNonAdmin;
})
.map((role) => {
return {
value: role,
name: RolesDisplayNames[role],
};
})
.map((r) => {
return (
<SelectItem key={r.value} value={r.value}>
{r.name}
</SelectItem>
);
});
return (
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem className="grid gap-3">
<Label>{t('Project Role')}</Label>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<SelectTrigger>
<SelectValue placeholder={t('Select a project role')} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>{t('Project Role')}</SelectLabel>
{invitationRoles}
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
></FormField>
);
};
export { ProjectRoleSelect };

View File

@@ -0,0 +1,22 @@
import { api } from '@/lib/api';
import {
ListProjectMembersRequestQuery,
ProjectMemberWithUser,
UpdateProjectMemberRoleRequestBody,
} from '@activepieces/ee-shared';
import { SeekPage } from '@activepieces/shared';
export const projectMembersApi = {
list(request: ListProjectMembersRequestQuery) {
return api.get<SeekPage<ProjectMemberWithUser>>(
'/v1/project-members',
request,
);
},
update(memberId: string, request: UpdateProjectMemberRoleRequestBody) {
return api.post<void>(`/v1/project-members/${memberId}`, request);
},
delete(id: string): Promise<void> {
return api.delete<void>(`/v1/project-members/${id}`);
},
};

View File

@@ -0,0 +1,33 @@
import { useQuery } from '@tanstack/react-query';
import { ProjectMemberWithUser } from '@activepieces/ee-shared';
import { assertNotNullOrUndefined } from '@activepieces/shared';
import { authenticationSession } from '../../../lib/authentication-session';
import { projectMembersApi } from './project-members-api';
export const projectMembersHooks = {
useProjectMembers: () => {
const query = useQuery<ProjectMemberWithUser[]>({
queryKey: ['project-members', authenticationSession.getProjectId()],
queryFn: async () => {
const projectId = authenticationSession.getProjectId();
assertNotNullOrUndefined(projectId, 'Project ID is null');
const res = await projectMembersApi.list({
projectId: projectId,
projectRoleId: undefined,
cursor: undefined,
limit: 100,
});
return res.data;
},
staleTime: Infinity,
});
return {
projectMembers: query.data,
isLoading: query.isLoading,
refetch: query.refetch,
};
},
};

View File

@@ -0,0 +1,26 @@
import {
ListUserInvitationsRequest,
SeekPage,
SendUserInvitationRequest,
UserInvitation,
UserInvitationWithLink,
} from '@activepieces/shared';
import { api } from '../../../lib/api';
export const userInvitationApi = {
invite: (request: SendUserInvitationRequest) => {
return api.post<UserInvitationWithLink>('/v1/user-invitations', request);
},
list: (request: ListUserInvitationsRequest) => {
return api.get<SeekPage<UserInvitation>>('/v1/user-invitations', request);
},
delete(id: string): Promise<void> {
return api.delete<void>(`/v1/user-invitations/${id}`);
},
accept(token: string): Promise<{ registered: boolean }> {
return api.post<{ registered: boolean }>(`/v1/user-invitations/accept`, {
invitationToken: token,
});
},
};

View File

@@ -0,0 +1,30 @@
import { useQuery } from '@tanstack/react-query';
import { InvitationType, UserInvitation } from '@activepieces/shared';
import { userInvitationApi } from './user-invitation';
const userInvitationsQueryKey = 'user-invitations';
export const userInvitationsHooks = {
useInvitations: () => {
const query = useQuery<UserInvitation[]>({
queryFn: () => {
return userInvitationApi
.list({
type: InvitationType.PROJECT,
cursor: undefined,
limit: 100,
})
.then((res) => res.data);
},
queryKey: [userInvitationsQueryKey],
staleTime: 0,
});
return {
invitations: query.data,
isLoading: query.isLoading,
refetch: query.refetch,
};
},
};