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