mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
feat: allow admins to remove organisation and team members (#2705)
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type AdminOrganisationMemberDeleteDialogProps = {
|
||||
organisationId: string;
|
||||
organisationName: string;
|
||||
organisationMemberId: string;
|
||||
organisationMemberName: string;
|
||||
organisationMemberEmail: string;
|
||||
};
|
||||
|
||||
export const AdminOrganisationMemberDeleteDialog = ({
|
||||
organisationId,
|
||||
organisationName,
|
||||
organisationMemberId,
|
||||
organisationMemberName,
|
||||
organisationMemberEmail,
|
||||
}: AdminOrganisationMemberDeleteDialogProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { mutateAsync: deleteOrganisationMember, isPending } =
|
||||
trpc.admin.organisationMember.delete.useMutation({
|
||||
onSuccess: async () => {
|
||||
toast({
|
||||
title: _(msg`Success`),
|
||||
description: _(msg`Member has been removed from the organisation.`),
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
|
||||
// Refresh the page to show updated data
|
||||
await navigate(0);
|
||||
},
|
||||
onError: (err) => {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
console.error(error);
|
||||
|
||||
toast({
|
||||
title: _(msg`An error occurred`),
|
||||
description: _(msg`We couldn't remove this member. Please try again later.`),
|
||||
variant: 'destructive',
|
||||
duration: 10000,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(value) => !isPending && setOpen(value)}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="destructive">
|
||||
<Trans>Remove</Trans>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader className="space-y-4">
|
||||
<DialogTitle>
|
||||
<Trans>Remove Organisation Member</Trans>
|
||||
</DialogTitle>
|
||||
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription className="selection:bg-red-100">
|
||||
<Trans>This action is not reversible. Please be certain.</Trans>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</DialogHeader>
|
||||
|
||||
<div>
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
You are about to remove the following user from the organisation{' '}
|
||||
<span className="font-semibold">{organisationName}</span>:
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
|
||||
<Alert className="mt-4" variant="neutral" padding="tight">
|
||||
<AvatarWithText
|
||||
avatarClass="h-12 w-12"
|
||||
avatarFallback={organisationMemberName.slice(0, 1).toUpperCase()}
|
||||
primaryText={<span className="font-semibold">{organisationMemberName}</span>}
|
||||
secondaryText={organisationMemberEmail}
|
||||
/>
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
<fieldset disabled={isPending}>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
loading={isPending}
|
||||
onClick={async () =>
|
||||
deleteOrganisationMember({
|
||||
organisationId,
|
||||
organisationMemberId,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Trans>Remove member</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type AdminTeamMemberDeleteDialogProps = {
|
||||
teamId: number;
|
||||
teamName: string;
|
||||
memberId: string;
|
||||
memberName: string;
|
||||
memberEmail: string;
|
||||
};
|
||||
|
||||
export const AdminTeamMemberDeleteDialog = ({
|
||||
teamId,
|
||||
teamName,
|
||||
memberId,
|
||||
memberName,
|
||||
memberEmail,
|
||||
}: AdminTeamMemberDeleteDialogProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { mutateAsync: deleteTeamMember, isPending } = trpc.admin.teamMember.delete.useMutation({
|
||||
onSuccess: async () => {
|
||||
toast({
|
||||
title: _(msg`Success`),
|
||||
description: _(msg`Member has been removed from the team.`),
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
|
||||
// Refresh the page to show updated data
|
||||
await navigate(0);
|
||||
},
|
||||
onError: (err) => {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
console.error(error);
|
||||
|
||||
toast({
|
||||
title: _(msg`An error occurred`),
|
||||
description: _(msg`We couldn't remove this member. Please try again later.`),
|
||||
variant: 'destructive',
|
||||
duration: 10000,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(value) => !isPending && setOpen(value)}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="destructive">
|
||||
<Trans>Remove</Trans>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogHeader className="space-y-4">
|
||||
<DialogTitle>
|
||||
<Trans>Remove Team Member</Trans>
|
||||
</DialogTitle>
|
||||
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription className="selection:bg-red-100">
|
||||
<Trans>This action is not reversible. Please be certain.</Trans>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</DialogHeader>
|
||||
|
||||
<div>
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
You are about to remove the following user from the team{' '}
|
||||
<span className="font-semibold">{teamName}</span>:
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
|
||||
<Alert className="mt-4" variant="neutral" padding="tight">
|
||||
<AvatarWithText
|
||||
avatarClass="h-12 w-12"
|
||||
avatarFallback={memberName.slice(0, 1).toUpperCase()}
|
||||
primaryText={<span className="font-semibold">{memberName}</span>}
|
||||
secondaryText={memberEmail}
|
||||
/>
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
<fieldset disabled={isPending}>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
loading={isPending}
|
||||
onClick={async () => deleteTeamMember({ teamId, memberId })}
|
||||
>
|
||||
<Trans>Remove member</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -44,6 +44,7 @@ import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { AdminOrganisationMemberDeleteDialog } from '~/components/dialogs/admin-organisation-member-delete-dialog';
|
||||
import { AdminOrganisationMemberUpdateDialog } from '~/components/dialogs/admin-organisation-member-update-dialog';
|
||||
import { DetailsCard, DetailsValue } from '~/components/general/admin-details';
|
||||
import { AdminGlobalSettingsSection } from '~/components/general/admin-global-settings-section';
|
||||
@@ -135,6 +136,10 @@ export default function OrganisationGroupSettingsPage({
|
||||
}, [i18n, t]);
|
||||
|
||||
const organisationMembersColumns = useMemo(() => {
|
||||
if (!organisation) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
header: t`Member`,
|
||||
@@ -164,10 +169,6 @@ export default function OrganisationGroupSettingsPage({
|
||||
{
|
||||
header: t`Role`,
|
||||
cell: ({ row }) => {
|
||||
if (!organisation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isOwner = row.original.userId === organisation.ownerUserId;
|
||||
|
||||
if (isOwner) {
|
||||
@@ -201,7 +202,9 @@ export default function OrganisationGroupSettingsPage({
|
||||
{
|
||||
header: t`Actions`,
|
||||
cell: ({ row }) => {
|
||||
const isOwner = row.original.userId === organisation?.ownerUserId;
|
||||
const isOwner = row.original.userId === organisation.ownerUserId;
|
||||
|
||||
const memberName = row.original.user.name ?? row.original.user.email;
|
||||
|
||||
return (
|
||||
<div className="flex justify-end space-x-2">
|
||||
@@ -215,6 +218,16 @@ export default function OrganisationGroupSettingsPage({
|
||||
organisationMember={row.original}
|
||||
isOwner={isOwner}
|
||||
/>
|
||||
|
||||
{!isOwner && (
|
||||
<AdminOrganisationMemberDeleteDialog
|
||||
organisationId={organisationId}
|
||||
organisationName={organisation.name}
|
||||
organisationMemberId={row.original.id}
|
||||
organisationMemberName={memberName}
|
||||
organisationMemberEmail={row.original.user.email}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -22,6 +22,7 @@ import { DataTable, type DataTableColumnDef } from '@documenso/ui/primitives/dat
|
||||
import { SpinnerBox } from '@documenso/ui/primitives/spinner';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { AdminTeamMemberDeleteDialog } from '~/components/dialogs/admin-team-member-delete-dialog';
|
||||
import { DetailsCard, DetailsValue } from '~/components/general/admin-details';
|
||||
import { AdminGlobalSettingsSection } from '~/components/general/admin-global-settings-section';
|
||||
import { GenericErrorLayout } from '~/components/general/generic-error-layout';
|
||||
@@ -53,6 +54,10 @@ export default function AdminTeamPage({ params }: Route.ComponentProps) {
|
||||
};
|
||||
|
||||
const teamMembersColumns = useMemo(() => {
|
||||
if (!team) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
header: _(msg`Member`),
|
||||
@@ -87,7 +92,7 @@ export default function AdminTeamPage({ params }: Route.ComponentProps) {
|
||||
header: _(msg`Organisation role`),
|
||||
accessorKey: 'organisationRole',
|
||||
cell: ({ row }) => {
|
||||
const isOwner = row.original.userId === team?.organisation.ownerUserId;
|
||||
const isOwner = row.original.userId === team.organisation.ownerUserId;
|
||||
|
||||
if (isOwner) {
|
||||
return <Badge>{_(msg`Owner`)}</Badge>;
|
||||
@@ -105,6 +110,30 @@ export default function AdminTeamPage({ params }: Route.ComponentProps) {
|
||||
accessorKey: 'createdAt',
|
||||
cell: ({ row }) => i18n.date(row.original.createdAt),
|
||||
},
|
||||
{
|
||||
header: _(msg`Actions`),
|
||||
cell: ({ row }) => {
|
||||
const isOwner = row.original.userId === team.organisation.ownerUserId;
|
||||
|
||||
if (isOwner) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const memberName = row.original.user.name ?? row.original.user.email;
|
||||
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<AdminTeamMemberDeleteDialog
|
||||
teamId={team.id}
|
||||
teamName={team.name}
|
||||
memberId={row.original.id}
|
||||
memberName={memberName}
|
||||
memberEmail={row.original.user.email}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
] satisfies DataTableColumnDef<TGetAdminTeamResponse['teamMembers'][number]>[];
|
||||
}, [team, _, i18n]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user