This commit is contained in:
giancarlo
2024-03-24 02:23:22 +08:00
parent 648d77b430
commit bce3479368
589 changed files with 37067 additions and 9596 deletions

View File

@@ -0,0 +1,144 @@
'use client';
import { useMemo, useState } from 'react';
import { ColumnDef } from '@tanstack/react-table';
import { EllipsisIcon } from 'lucide-react';
import { Database } from '@kit/supabase/database';
import { Button } from '@kit/ui/button';
import { DataTable } from '@kit/ui/data-table';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@kit/ui/dropdown-menu';
import { If } from '@kit/ui/if';
import { ProfileAvatar } from '@kit/ui/profile-avatar';
import { RoleBadge } from '../role-badge';
import { DeleteInvitationDialog } from './delete-invitation-dialog';
import { UpdateInvitationDialog } from './update-invitation-dialog';
type Invitations =
Database['public']['Functions']['get_account_invitations']['Returns'];
type AccountInvitationsTableProps = {
invitations: Invitations;
permissions: {
canUpdateInvitation: boolean;
canRemoveInvitation: boolean;
};
};
export function AccountInvitationsTable({
invitations,
permissions,
}: AccountInvitationsTableProps) {
const columns = useMemo(() => getColumns(permissions), [permissions]);
return <DataTable columns={columns} data={invitations} />;
}
function getColumns(permissions: {
canUpdateInvitation: boolean;
canRemoveInvitation: boolean;
}): ColumnDef<Invitations[0]>[] {
return [
{
header: 'Email',
size: 200,
cell: ({ row }) => {
const member = row.original;
const email = member.email;
return (
<span className={'flex items-center space-x-4 text-left'}>
<span>
<ProfileAvatar text={email} />
</span>
<span>{email}</span>
</span>
);
},
},
{
header: 'Role',
cell: ({ row }) => {
const { role } = row.original;
return <RoleBadge role={role} />;
},
},
{
header: 'Invited At',
cell: ({ row }) => {
return new Date(row.original.created_at).toLocaleDateString();
},
},
{
header: '',
id: 'actions',
cell: ({ row }) => (
<ActionsDropdown permissions={permissions} invitation={row.original} />
),
},
];
}
function ActionsDropdown({
permissions,
invitation,
}: {
permissions: AccountInvitationsTableProps['permissions'];
invitation: Invitations[0];
}) {
const [isDeletingInvite, setIsDeletingInvite] = useState(false);
const [isUpdatingRole, setIsUpdatingRole] = useState(false);
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant={'ghost'} size={'icon'}>
<EllipsisIcon className={'h-5 w-5'} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<If condition={permissions.canUpdateInvitation}>
<DropdownMenuItem onClick={() => setIsUpdatingRole(true)}>
Update Invitation
</DropdownMenuItem>
</If>
<If condition={permissions.canRemoveInvitation}>
<DropdownMenuItem onClick={() => setIsDeletingInvite(true)}>
Remove
</DropdownMenuItem>
</If>
</DropdownMenuContent>
</DropdownMenu>
<If condition={isDeletingInvite}>
<DeleteInvitationDialog
isOpen
setIsOpen={setIsDeletingInvite}
invitationId={invitation.id}
/>
</If>
<If condition={isUpdatingRole}>
<UpdateInvitationDialog
isOpen
setIsOpen={setIsUpdatingRole}
invitationId={invitation.id}
userRole={invitation.role}
/>
</If>
</>
);
}

View File

@@ -0,0 +1,101 @@
import { useState, useTransition } from 'react';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { Button } from '@kit/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@kit/ui/dialog';
import { If } from '@kit/ui/if';
import { Trans } from '@kit/ui/trans';
import { deleteInvitationAction } from '../../actions/account-invitations-server-actions';
export const DeleteInvitationDialog: React.FC<{
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
invitationId: string;
}> = ({ isOpen, setIsOpen, invitationId }) => {
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans i18nKey="organization:deleteInvitationDialogTitle" />
</DialogTitle>
<DialogDescription>
Remove the invitation to join this account.
</DialogDescription>
</DialogHeader>
<DeleteInvitationForm
setIsOpen={setIsOpen}
invitationId={invitationId}
/>
</DialogContent>
</Dialog>
);
};
function DeleteInvitationForm({
invitationId,
setIsOpen,
}: {
invitationId: string;
setIsOpen: (isOpen: boolean) => void;
}) {
const [isSubmitting, startTransition] = useTransition();
const [error, setError] = useState<boolean>();
const onInvitationRemoved = () => {
startTransition(async () => {
try {
await deleteInvitationAction({ invitationId });
setIsOpen(false);
} catch (e) {
setError(true);
}
});
};
return (
<form action={onInvitationRemoved}>
<div className={'flex flex-col space-y-6'}>
<p className={'text-sm'}>
<Trans i18nKey={'common:modalConfirmationQuestion'} />
</p>
<If condition={error}>
<RemoveInvitationErrorAlert />
</If>
<Button
data-test={'confirm-delete-invitation'}
variant={'destructive'}
disabled={isSubmitting}
>
Delete Invitation
</Button>
</div>
</form>
);
}
function RemoveInvitationErrorAlert() {
return (
<Alert variant={'destructive'}>
<AlertTitle>
<Trans i18nKey={'organization:deleteInvitationErrorTitle'} />
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'organization:deleteInvitationErrorMessage'} />
</AlertDescription>
</Alert>
);
}

View File

@@ -0,0 +1,158 @@
import { useState, useTransition } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { Database } from '@kit/supabase/database';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { Button } from '@kit/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@kit/ui/dialog';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@kit/ui/form';
import { If } from '@kit/ui/if';
import { Trans } from '@kit/ui/trans';
import { updateInvitationAction } from '../../actions/account-invitations-server-actions';
import { UpdateRoleSchema } from '../../schema/update-role-schema';
import { MembershipRoleSelector } from '../membership-role-selector';
type Role = Database['public']['Enums']['account_role'];
export const UpdateInvitationDialog: React.FC<{
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
invitationId: string;
userRole: Role;
}> = ({ isOpen, setIsOpen, invitationId, userRole }) => {
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
<Trans i18nKey={'organization:updateMemberRoleModalHeading'} />
</DialogTitle>
<DialogDescription>
<Trans i18nKey={'organization:updateMemberRoleModalDescription'} />
</DialogDescription>
</DialogHeader>
<UpdateInvitationForm
setIsOpen={setIsOpen}
invitationId={invitationId}
userRole={userRole}
/>
</DialogContent>
</Dialog>
);
};
function UpdateInvitationForm({
invitationId,
userRole,
setIsOpen,
}: React.PropsWithChildren<{
invitationId: string;
userRole: Role;
setIsOpen: (isOpen: boolean) => void;
}>) {
const [pending, startTransition] = useTransition();
const [error, setError] = useState<boolean>();
const onSubmit = ({ role }: { role: Role }) => {
startTransition(async () => {
try {
await updateInvitationAction({ invitationId, role });
setIsOpen(false);
} catch (e) {
setError(true);
}
});
};
const form = useForm({
resolver: zodResolver(
UpdateRoleSchema.refine(
(data) => {
return data.role !== userRole;
},
{
message: 'Role must be different from the current role.',
path: ['role'],
},
),
),
reValidateMode: 'onChange',
mode: 'onChange',
defaultValues: {
role: userRole,
},
});
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className={'flex flex-col space-y-6'}
>
<If condition={error}>
<UpdateRoleErrorAlert />
</If>
<FormField
name={'role'}
render={({ field }) => {
return (
<FormItem>
<FormLabel>New Role</FormLabel>
<FormControl>
<MembershipRoleSelector
currentUserRole={userRole}
value={field.value}
onChange={(newRole) => form.setValue('role', newRole)}
/>
</FormControl>
<FormDescription>Pick a role for this member.</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
<Button data-test={'confirm-update-member-role'} disabled={pending}>
<Trans i18nKey={'organization:updateRoleSubmitLabel'} />
</Button>
</form>
</Form>
);
}
function UpdateRoleErrorAlert() {
return (
<Alert variant={'destructive'}>
<AlertTitle>
<Trans i18nKey={'organization:updateRoleErrorHeading'} />
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'organization:updateRoleErrorMessage'} />
</AlertDescription>
</Alert>
);
}