🎉 chore(init): project initialization

This commit is contained in:
web@ppanel
2024-11-14 01:22:43 +07:00
commit 829edfa824
479 changed files with 61413 additions and 0 deletions
@@ -0,0 +1,135 @@
import { Icon } from '@iconify/react';
import { EnhancedInput } from '@repo/ui/enhanced-input';
import { Button } from '@shadcn/ui/button';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@shadcn/ui/form';
import { useForm } from '@shadcn/ui/lib/react-hook-form';
import { z, zodResolver } from '@shadcn/ui/lib/zod';
import { ScrollArea } from '@shadcn/ui/scroll-area';
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@shadcn/ui/sheet';
import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react';
const formSchema = z.object({
name: z.string(),
description: z.string().optional(),
});
interface GroupFormProps<T> {
onSubmit: (data: T) => Promise<boolean> | boolean;
initialValues?: T;
loading?: boolean;
trigger: string;
title: string;
}
export default function GroupForm<T extends Record<string, any>>({
onSubmit,
initialValues,
loading,
trigger,
title,
}: GroupFormProps<T>) {
const t = useTranslations('subscribe');
const [open, setOpen] = useState(false);
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
...initialValues,
},
});
useEffect(() => {
form?.reset(initialValues);
}, [form, initialValues]);
async function handleSubmit(data: { [x: string]: any }) {
const bool = await onSubmit(data as T);
if (bool) setOpen(false);
}
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
onClick={() => {
form.reset();
setOpen(true);
}}
>
{trigger}
</Button>
</SheetTrigger>
<SheetContent className='w-[500px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{title}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))]'>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className='space-y-4 px-6 pt-4'>
<FormField
control={form.control}
name='name'
render={({ field }) => (
<FormItem>
<FormLabel>{t('group.form.name')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='description'
render={({ field }) => (
<FormItem>
<FormLabel>{t('group.form.description')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button
variant='outline'
disabled={loading}
onClick={() => {
setOpen(false);
}}
>
{t('group.form.cancel')}
</Button>
<Button disabled={loading} onClick={form.handleSubmit(handleSubmit)}>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}{' '}
{t('group.form.confirm')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}
@@ -0,0 +1,141 @@
'use client';
import { ProTable, ProTableActions } from '@/components/pro-table';
import {
batchDeleteSubscribeGroup,
createSubscribeGroup,
deleteSubscribeGroup,
getSubscribeGroupList,
updateSubscribeGroup,
} from '@/services/admin/subscribe';
import { ConfirmButton } from '@repo/ui/confirm-button';
import { formatDate } from '@repo/ui/utils';
import { Button } from '@shadcn/ui/button';
import { toast } from '@shadcn/ui/lib/sonner';
import { useTranslations } from 'next-intl';
import { useRef, useState } from 'react';
import GroupForm from './group-form';
const GroupTable = () => {
const t = useTranslations('subscribe');
const [loading, setLoading] = useState(false);
const ref = useRef<ProTableActions>();
return (
<ProTable<API.SubscribeGroup, any>
action={ref}
header={{
title: t('group.title'),
toolbar: (
<GroupForm<API.CreateSubscribeGroupRequest>
trigger={t('group.create')}
title={t('group.createSubscribeGroup')}
loading={loading}
onSubmit={async (values) => {
setLoading(true);
try {
await createSubscribeGroup(values);
toast.success(t('group.createSuccess'));
ref.current?.refresh();
setLoading(false);
return true;
} catch (error) {
setLoading(false);
return false;
}
}}
/>
),
}}
columns={[
{
accessorKey: 'name',
header: t('group.name'),
},
{
accessorKey: 'description',
header: t('group.description'),
},
{
accessorKey: 'updated_at',
header: t('group.updatedAt'),
cell: ({ row }) => formatDate(row.getValue('updated_at')),
},
]}
request={async () => {
const { data } = await getSubscribeGroupList();
return {
list: data.data?.list || [],
total: data.data?.total || 0,
};
}}
actions={{
render: (row) => [
<GroupForm<API.SubscribeGroup>
key='edit'
trigger={t('group.edit')}
title={t('group.editSubscribeGroup')}
loading={loading}
initialValues={row}
onSubmit={async (values) => {
setLoading(true);
try {
await updateSubscribeGroup({
...row,
...values,
});
toast.success(t('group.updateSuccess'));
ref.current?.refresh();
setLoading(false);
return true;
} catch (error) {
setLoading(false);
return false;
}
}}
/>,
<ConfirmButton
key='delete'
trigger={<Button variant='destructive'>{t('group.delete')}</Button>}
title={t('group.confirmDelete')}
description={t('group.deleteWarning')}
onConfirm={async () => {
await deleteSubscribeGroup({
id: row.id,
});
toast.success(t('group.deleteSuccess'));
ref.current?.refresh();
}}
cancelText={t('group.cancel')}
confirmText={t('group.confirm')}
/>,
],
batchRender(rows) {
return [
<ConfirmButton
key='delete'
trigger={<Button variant='destructive'>{t('group.delete')}</Button>}
title={t('group.confirmDelete')}
description={t('group.deleteWarning')}
onConfirm={async () => {
await batchDeleteSubscribeGroup({
ids: rows.map((item) => item.id),
});
toast.success(t('group.deleteSuccess'));
ref.current?.refresh();
}}
cancelText={t('group.cancel')}
confirmText={t('group.confirm')}
/>,
];
},
}}
/>
);
};
export default GroupTable;
@@ -0,0 +1,25 @@
import { getTranslations } from 'next-intl/server';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@shadcn/ui/tabs';
import GroupTable from './group-table';
import SubscribeTable from './subscribe-table';
export default async function Page() {
const t = await getTranslations('subscribe');
return (
<Tabs defaultValue='subscribe'>
<TabsList>
<TabsTrigger value='subscribe'>{t('tabs.subscribe')}</TabsTrigger>
<TabsTrigger value='group'>{t('tabs.subscribeGroup')}</TabsTrigger>
</TabsList>
<TabsContent value='subscribe'>
<SubscribeTable />
</TabsContent>
<TabsContent value='group'>
<GroupTable />
</TabsContent>
</Tabs>
);
}
@@ -0,0 +1,586 @@
import { getNodeGroupList, getNodeList } from '@/services/admin/server';
import { getSubscribeGroupList } from '@/services/admin/subscribe';
import { Icon } from '@iconify/react';
import { Combobox } from '@repo/ui/combobox';
import { ArrayInput } from '@repo/ui/dynamic-Inputs';
import { JSONEditor } from '@repo/ui/editor';
import { EnhancedInput } from '@repo/ui/enhanced-input';
import { unitConversion } from '@repo/ui/utils';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@shadcn/ui/accordion';
import { Button } from '@shadcn/ui/button';
import { Checkbox } from '@shadcn/ui/checkbox';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@shadcn/ui/form';
import { Label } from '@shadcn/ui/label';
import { useForm } from '@shadcn/ui/lib/react-hook-form';
import { z, zodResolver } from '@shadcn/ui/lib/zod';
import { ScrollArea } from '@shadcn/ui/scroll-area';
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@shadcn/ui/sheet';
import { useQuery } from '@tanstack/react-query';
import { useTranslations } from 'next-intl';
import { useTheme } from 'next-themes';
import { assign, shake } from 'radash';
import { useEffect, useState } from 'react';
interface SubscribeFormProps<T> {
onSubmit: (data: T) => Promise<boolean> | boolean;
initialValues?: T;
loading?: boolean;
trigger: string;
title: string;
}
const defaultValues = {
inventory: -1,
speed_limit: 0,
device_limit: 0,
traffic: 0,
quota: 0,
discount: [],
server_group: [],
server: [],
};
export default function SubscribeForm<T extends Record<string, any>>({
onSubmit,
initialValues,
loading,
trigger,
title,
}: SubscribeFormProps<T>) {
const t = useTranslations('subscribe');
const { resolvedTheme } = useTheme();
const [open, setOpen] = useState(false);
const formSchema = z.object({
name: z.string(),
description: z.string().optional(),
unit_price: z.number(),
replacement: z.number().optional(),
discount: z
.array(
z.object({
months: z.number(),
discount: z.number(),
}),
)
.optional(),
inventory: z.number().optional(),
speed_limit: z.number().optional(),
device_limit: z.number().optional(),
traffic: z.number().optional(),
quota: z.number().optional(),
group_id: z.number().optional().nullish(),
server_group: z.array(z.number()).optional(),
server: z.array(z.number()).optional(),
});
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: assign(
defaultValues,
shake(initialValues, (value) => value === null) as Record<string, any>,
),
});
useEffect(() => {
form?.reset(
assign(defaultValues, shake(initialValues, (value) => value === null) as Record<string, any>),
);
}, [form, initialValues]);
async function handleSubmit(data: { [x: string]: any }) {
const bool = await onSubmit(data as T);
if (bool) setOpen(false);
}
const { data: group } = useQuery({
queryKey: ['getSubscribeGroupList'],
queryFn: async () => {
const { data } = await getSubscribeGroupList();
return data.data?.list as API.SubscribeGroup[];
},
});
const { data: server } = useQuery({
queryKey: ['getNodeList', 'all'],
queryFn: async () => {
const { data } = await getNodeList({
page: 1,
size: 9999,
});
return data.data?.list;
},
});
const { data: server_groups } = useQuery({
queryKey: ['getNodeGroupList'],
queryFn: async () => {
const { data } = await getNodeGroupList();
return (data.data?.list || []) as API.ServerGroup[];
},
});
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
onClick={() => {
form.reset();
setOpen(true);
}}
>
{trigger}
</Button>
</SheetTrigger>
<SheetContent className='w-screen max-w-full md:max-w-full'>
<SheetHeader>
<SheetTitle>{title}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))]'>
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleSubmit)}
className='grid gap-4 px-6 pt-4 lg:grid-cols-3'
>
<div className='flex flex-col gap-4'>
<FormField
control={form.control}
name='name'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.name')}</FormLabel>
<FormControl>
<EnhancedInput
{...field}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='description'
render={({ field }) => (
<FormItem>
<FormControl>
<JSONEditor
title={t('form.description')}
value={field.value}
onChange={(value) => {
form.setValue(field.name, value);
}}
placeholder={{
description: 'description',
features: [
{
type: 'default',
icon: '',
label: 'label',
},
],
}}
schema={{
type: 'object',
properties: {
description: {
type: 'string',
description: 'A brief description of the item.',
},
features: {
type: 'array',
items: {
type: 'object',
properties: {
icon: {
type: 'string',
description:
"Enter an Iconify icon identifier (e.g., 'mdi:account').",
pattern: '^[a-z0-9]+:[a-z0-9-]+$',
examples: [
'uil:shield-check',
'uil:shield-exclamation',
'uil:database',
'uil:server',
'Visit https://icon-sets.iconify.design to browse available icons.',
],
},
label: {
type: 'string',
description: 'The label describing the feature.',
},
type: {
type: 'string',
enum: ['default', 'success', 'destructive'],
description:
'The type of feature, limited to specific values.',
},
},
},
description: 'A list of feature objects.',
},
},
required: ['description', 'features'],
additionalProperties: false,
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className='flex flex-col gap-4'>
<div className='grid grid-cols-3 gap-4'>
<FormField
control={form.control}
name='unit_price'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.unit_price')}</FormLabel>
<FormControl>
<EnhancedInput
type='number'
{...field}
min={0}
formatInput={(value) => unitConversion('centsToDollars', value)}
formatOutput={(value) => unitConversion('dollarsToCents', value)}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='replacement'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.replacement')}</FormLabel>
<FormControl>
<EnhancedInput
type='number'
{...field}
min={0}
formatInput={(value) => unitConversion('centsToDollars', value)}
formatOutput={(value) => unitConversion('dollarsToCents', value)}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='traffic'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.traffic')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('form.noLimit')}
type='number'
{...field}
formatInput={(value) => unitConversion('bytesToGb', value)}
formatOutput={(value) => unitConversion('gbToBytes', value)}
suffix='GB'
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='speed_limit'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.speedLimit')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('form.noLimit')}
type='number'
{...field}
formatInput={(value) => unitConversion('bitsToMb', value)}
formatOutput={(value) => unitConversion('mbToBits', value)}
suffix='MB'
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='device_limit'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.deviceLimit')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('form.noLimit')}
type='number'
{...field}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='inventory'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.inventory')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={`-1 ${t('form.noLimit')}`}
type='number'
{...field}
min={-1}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='quota'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.quota')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('form.noLimit')}
type='number'
{...field}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='group_id'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.subscribeGroup')}</FormLabel>
<FormControl>
<Combobox<number, false>
placeholder={t('form.selectSubscribeGroup')}
{...field}
onChange={(value) => {
form.setValue(field.name, value);
}}
options={group?.map((item) => ({
label: item.name,
value: item.id,
}))}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className='col-span-3'>
<FormField
control={form.control}
name='discount'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.discount')}</FormLabel>
<FormControl>
<ArrayInput<API.SubscribeDiscount>
fields={[
{
name: 'months',
type: 'number',
min: 1,
suffix: t('form.discountMonths'),
},
{
name: 'discount',
type: 'number',
min: 1,
max: 100,
placeholder: t('form.discountPercent'),
suffix: '%',
},
]}
value={field.value}
onChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormDescription>{t('form.discountDescription')}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
</div>
<div className='grid grid-cols-2 gap-4'>
<FormField
control={form.control}
name='server_group'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.serverGroup')}</FormLabel>
<FormControl>
<Accordion type='single' collapsible className='w-full'>
{server_groups?.map((group: API.ServerGroup) => {
const value = field.value || [];
return (
<AccordionItem key={group.id} value={String(group.id)}>
<AccordionTrigger>
<div className='flex items-center gap-2'>
<Checkbox
checked={value.includes(group.id!)}
onCheckedChange={(checked) => {
return checked
? form.setValue(field.name, [...value, group.id])
: form.setValue(
field.name,
value.filter((value: number) => value !== group.id),
);
}}
/>
<Label>{group.name}</Label>
</div>
</AccordionTrigger>
<AccordionContent>
<ul className='list-disc [&>li]:mt-2'>
{server
?.filter((server: API.Server) => server.groupId === group.id)
?.map((node: API.Server) => {
return (
<li
key={node.id}
className='flex items-center justify-between *:flex-1'
>
<span>{node.name}</span>
<span>{node.server_addr}</span>
<span className='text-right'>{node.protocol}</span>
</li>
);
})}
</ul>
</AccordionContent>
</AccordionItem>
);
})}
</Accordion>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='server'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.server')}</FormLabel>
<FormControl>
<div className='flex flex-col gap-2'>
{server
?.filter((item: API.Server) => !item.groupId)
?.map((item: API.Server) => {
const value = field.value || [];
return (
<div className='flex items-center gap-2' key={item.id}>
<Checkbox
checked={value.includes(item.id!)}
onCheckedChange={(checked) => {
return checked
? form.setValue(field.name, [...value, item.id])
: form.setValue(
field.name,
value.filter((value: number) => value !== item.id),
);
}}
/>
<Label className='flex w-full items-center justify-between *:flex-1'>
<span>{item.name}</span>
<span>{item.server_addr}</span>
<span className='text-right'>{item.protocol}</span>
</Label>
</div>
);
})}
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</form>
</Form>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button
variant='outline'
disabled={loading}
onClick={() => {
setOpen(false);
}}
>
{t('form.cancel')}
</Button>
<Button disabled={loading} onClick={form.handleSubmit(handleSubmit)}>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}{' '}
{t('form.confirm')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}
@@ -0,0 +1,242 @@
'use client';
import { Display } from '@/components/display';
import { ProTable, ProTableActions } from '@/components/pro-table';
import {
batchDeleteSubscribe,
createSubscribe,
deleteSubscribe,
getSubscribeGroupList,
getSubscribeList,
updateSubscribe,
} from '@/services/admin/subscribe';
import { ConfirmButton } from '@repo/ui/confirm-button';
import { Badge } from '@shadcn/ui/badge';
import { Button } from '@shadcn/ui/button';
import { toast } from '@shadcn/ui/lib/sonner';
import { Switch } from '@shadcn/ui/switch';
import { useQuery } from '@tanstack/react-query';
import { useTranslations } from 'next-intl';
import { useRef, useState } from 'react';
import SubscribeForm from './subscribe-form';
export default function SubscribeTable() {
const t = useTranslations('subscribe');
const [loading, setLoading] = useState(false);
const { data: groups } = useQuery({
queryKey: ['getSubscribeGroupList', 'all'],
queryFn: async () => {
const { data } = await getSubscribeGroupList({
page: 1,
size: 9999,
});
return data.data?.list as API.SubscribeGroup[];
},
});
const ref = useRef<ProTableActions>();
return (
<ProTable<API.Subscribe, { group_id: number; query: string }>
action={ref}
header={{
toolbar: (
<SubscribeForm<API.CreateSubscribeRequest>
trigger={t('create')}
title={t('createSubscribe')}
loading={loading}
onSubmit={async (values) => {
setLoading(true);
try {
await createSubscribe({
...values,
show: false,
sell: false,
});
toast.success(t('createSuccess'));
ref.current?.refresh();
setLoading(false);
return true;
} catch (error) {
setLoading(false);
return false;
}
}}
/>
),
}}
params={[
{
key: 'search',
},
{
key: 'group_id',
placeholder: t('subscribeGroup'),
options: groups?.map((item) => ({
label: item.name,
value: String(item.id),
})),
},
]}
request={async (pagination, filters) => {
const { data } = await getSubscribeList({
...pagination,
...filters,
});
return {
list: data.data?.list || [],
total: data.data?.total || 0,
};
}}
columns={[
{
accessorKey: 'show',
header: t('show'),
cell: ({ row }) => {
return (
<Switch
defaultChecked={row.getValue('show')}
onCheckedChange={async (checked) => {
await updateSubscribe({
...row.original,
show: checked,
} as API.UpdateSubscribeRequest);
ref.current?.refresh();
}}
/>
);
},
},
{
accessorKey: 'sell',
header: t('sell'),
cell: ({ row }) => {
return (
<Switch
defaultChecked={row.getValue('sell')}
onCheckedChange={async (checked) => {
await updateSubscribe({
...row.original,
sell: checked,
} as API.UpdateSubscribeRequest);
ref.current?.refresh();
}}
/>
);
},
},
{
accessorKey: 'name',
header: t('name'),
},
{
accessorKey: 'unit_price',
header: t('unitPrice'),
cell: ({ row }) => <Display type='currency' value={row.getValue('unit_price')} />,
},
{
accessorKey: 'replacement',
header: t('replacement'),
cell: ({ row }) => <Display type='currency' value={row.getValue('replacement')} />,
},
{
accessorKey: 'traffic',
header: t('traffic'),
cell: ({ row }) => <Display type='traffic' value={row.getValue('traffic')} unlimited />,
},
{
accessorKey: 'device_limit',
header: t('deviceLimit'),
cell: ({ row }) => (
<Display type='number' value={row.getValue('device_limit')} unlimited />
),
},
{
accessorKey: 'inventory',
header: t('inventory'),
cell: ({ row }) => (
<Display
type='number'
value={row.getValue('inventory') === -1 ? 0 : row.getValue('inventory')}
unlimited
/>
),
},
{
accessorKey: 'quota',
header: t('quota'),
cell: ({ row }) => <Display type='number' value={row.getValue('quota')} unlimited />,
},
{
accessorKey: 'group_id',
header: t('subscribeGroup'),
cell: ({ row }) => {
const name = groups?.find((group) => group.id === row.getValue('group_id'))?.name;
return name ? <Badge variant='outline'>{name}</Badge> : '--';
},
},
]}
actions={{
render: (row) => [
<SubscribeForm<API.Subscribe>
key='edit'
trigger={t('edit')}
title={t('editSubscribe')}
loading={loading}
initialValues={row}
onSubmit={async (values) => {
setLoading(true);
try {
await updateSubscribe({
...row,
...values,
} as API.UpdateSubscribeRequest);
toast.success(t('updateSuccess'));
ref.current?.refresh();
setLoading(false);
return true;
} catch (error) {
setLoading(false);
return false;
}
}}
/>,
<ConfirmButton
key='delete'
trigger={<Button variant='destructive'>{t('delete')}</Button>}
title={t('confirmDelete')}
description={t('deleteWarning')}
onConfirm={async () => {
await deleteSubscribe({
id: row.id,
});
toast.success(t('deleteSuccess'));
ref.current?.refresh();
}}
cancelText={t('cancel')}
confirmText={t('confirm')}
/>,
],
batchRender: (rows) => [
<ConfirmButton
key='delete'
trigger={<Button variant='destructive'>{t('delete')}</Button>}
title={t('confirmDelete')}
description={t('deleteWarning')}
onConfirm={async () => {
await batchDeleteSubscribe({
ids: rows.map((item) => item.id),
});
toast.success(t('deleteSuccess'));
ref.current?.reset();
}}
cancelText={t('cancel')}
confirmText={t('confirm')}
/>,
],
}}
/>
);
}