Next.js Supabase V3 (#463)
Version 3 of the kit: - Radix UI replaced with Base UI (using the Shadcn UI patterns) - next-intl replaces react-i18next - enhanceAction deprecated; usage moved to next-safe-action - main layout now wrapped with [locale] path segment - Teams only mode - Layout updates - Zod v4 - Next.js 16.2 - Typescript 6 - All other dependencies updated - Removed deprecated Edge CSRF - Dynamic Github Action runner
This commit is contained in:
committed by
GitHub
parent
4912e402a3
commit
7ebff31475
@@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
|
||||
@@ -7,7 +9,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { EllipsisVertical } from 'lucide-react';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { Tables } from '@kit/supabase/database';
|
||||
import { Button } from '@kit/ui/button';
|
||||
@@ -21,7 +23,6 @@ import {
|
||||
} from '@kit/ui/dropdown-menu';
|
||||
import { DataTable } from '@kit/ui/enhanced-data-table';
|
||||
import { Form, FormControl, FormField, FormItem } from '@kit/ui/form';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import {
|
||||
Select,
|
||||
@@ -77,7 +78,7 @@ export function AdminAccountsTable(
|
||||
}
|
||||
|
||||
function AccountsTableFilters(props: {
|
||||
filters: z.infer<typeof FiltersSchema>;
|
||||
filters: z.output<typeof FiltersSchema>;
|
||||
}) {
|
||||
const form = useForm({
|
||||
resolver: zodResolver(FiltersSchema),
|
||||
@@ -92,7 +93,7 @@ function AccountsTableFilters(props: {
|
||||
const router = useRouter();
|
||||
const pathName = usePathname();
|
||||
|
||||
const onSubmit = ({ type, query }: z.infer<typeof FiltersSchema>) => {
|
||||
const onSubmit = ({ type, query }: z.output<typeof FiltersSchema>) => {
|
||||
const params = new URLSearchParams({
|
||||
account_type: type,
|
||||
query: query ?? '',
|
||||
@@ -105,6 +106,12 @@ function AccountsTableFilters(props: {
|
||||
|
||||
const type = useWatch({ control: form.control, name: 'type' });
|
||||
|
||||
const options = {
|
||||
all: 'All Accounts',
|
||||
team: 'Team',
|
||||
personal: 'Personal',
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -116,7 +123,7 @@ function AccountsTableFilters(props: {
|
||||
onValueChange={(value) => {
|
||||
form.setValue(
|
||||
'type',
|
||||
value as z.infer<typeof FiltersSchema>['type'],
|
||||
value as z.output<typeof FiltersSchema>['type'],
|
||||
{
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
@@ -128,16 +135,20 @@ function AccountsTableFilters(props: {
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={'Account Type'} />
|
||||
<SelectValue placeholder={'Account Type'}>
|
||||
{(value: keyof typeof options) => options[value]}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Account Type</SelectLabel>
|
||||
|
||||
<SelectItem value={'all'}>All accounts</SelectItem>
|
||||
<SelectItem value={'team'}>Team</SelectItem>
|
||||
<SelectItem value={'personal'}>Personal</SelectItem>
|
||||
{Object.entries(options).map(([key, value]) => (
|
||||
<SelectItem key={key} value={key}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -157,6 +168,8 @@ function AccountsTableFilters(props: {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<button type="submit" hidden />
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
@@ -194,75 +207,143 @@ function getColumns(): ColumnDef<Account>[] {
|
||||
{
|
||||
id: 'created_at',
|
||||
header: 'Created At',
|
||||
accessorKey: 'created_at',
|
||||
cell: ({ row }) => {
|
||||
return new Date(row.original.created_at!).toLocaleDateString(
|
||||
undefined,
|
||||
{
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'updated_at',
|
||||
header: 'Updated At',
|
||||
accessorKey: 'updated_at',
|
||||
cell: ({ row }) => {
|
||||
return row.original.updated_at
|
||||
? new Date(row.original.updated_at).toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: '',
|
||||
cell: ({ row }) => {
|
||||
const isPersonalAccount = row.original.is_personal_account;
|
||||
const userId = row.original.id;
|
||||
|
||||
return (
|
||||
<div className={'flex justify-end'}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant={'outline'} size={'icon'}>
|
||||
<EllipsisVertical className={'h-4'} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align={'end'}>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
|
||||
<DropdownMenuItem>
|
||||
<Link
|
||||
className={'h-full w-full'}
|
||||
href={`/admin/accounts/${userId}`}
|
||||
>
|
||||
View
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<If condition={isPersonalAccount}>
|
||||
<AdminResetPasswordDialog userId={userId}>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
Send Reset Password link
|
||||
</DropdownMenuItem>
|
||||
</AdminResetPasswordDialog>
|
||||
|
||||
<AdminImpersonateUserDialog userId={userId}>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
Impersonate User
|
||||
</DropdownMenuItem>
|
||||
</AdminImpersonateUserDialog>
|
||||
|
||||
<AdminDeleteUserDialog userId={userId}>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
Delete Personal Account
|
||||
</DropdownMenuItem>
|
||||
</AdminDeleteUserDialog>
|
||||
</If>
|
||||
|
||||
<If condition={!isPersonalAccount}>
|
||||
<AdminDeleteAccountDialog accountId={row.original.id}>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
Delete Team Account
|
||||
</DropdownMenuItem>
|
||||
</AdminDeleteAccountDialog>
|
||||
</If>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => <ActionsCell account={row.original} />,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
type ActiveDialog =
|
||||
| 'reset-password'
|
||||
| 'impersonate'
|
||||
| 'delete-user'
|
||||
| 'delete-account'
|
||||
| null;
|
||||
|
||||
function ActionsCell({ account }: { account: Account }) {
|
||||
const [activeDialog, setActiveDialog] = useState<ActiveDialog>(null);
|
||||
const isPersonalAccount = account.is_personal_account;
|
||||
|
||||
return (
|
||||
<div className={'flex justify-end'}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button variant={'outline'} size={'icon'}>
|
||||
<EllipsisVertical className={'h-4'} />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<DropdownMenuContent className="min-w-52">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
|
||||
<DropdownMenuItem
|
||||
render={
|
||||
<Link
|
||||
className={'h-full w-full'}
|
||||
href={`/admin/accounts/${account.id}`}
|
||||
>
|
||||
View
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
{isPersonalAccount && (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setActiveDialog('reset-password')}
|
||||
>
|
||||
Send Reset Password link
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() => setActiveDialog('impersonate')}
|
||||
>
|
||||
Impersonate User
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
onClick={() => setActiveDialog('delete-user')}
|
||||
>
|
||||
Delete Personal Account
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isPersonalAccount && (
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
onClick={() => setActiveDialog('delete-account')}
|
||||
>
|
||||
Delete Team Account
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{isPersonalAccount && (
|
||||
<>
|
||||
<AdminResetPasswordDialog
|
||||
userId={account.id}
|
||||
open={activeDialog === 'reset-password'}
|
||||
onOpenChange={(open) => !open && setActiveDialog(null)}
|
||||
/>
|
||||
|
||||
<AdminImpersonateUserDialog
|
||||
userId={account.id}
|
||||
open={activeDialog === 'impersonate'}
|
||||
onOpenChange={(open) => !open && setActiveDialog(null)}
|
||||
/>
|
||||
|
||||
<AdminDeleteUserDialog
|
||||
userId={account.id}
|
||||
open={activeDialog === 'delete-user'}
|
||||
onOpenChange={(open) => !open && setActiveDialog(null)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isPersonalAccount && (
|
||||
<AdminDeleteAccountDialog
|
||||
accountId={account.id}
|
||||
open={activeDialog === 'delete-account'}
|
||||
onOpenChange={(open) => !open && setActiveDialog(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user