Files
new-api/web/default/src/features/users/components/users-mutate-drawer.tsx
T

423 lines
14 KiB
TypeScript
Vendored

import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { useQuery } from '@tanstack/react-query'
import { Pencil } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { getCurrencyDisplay, getCurrencyLabel } from '@/lib/currency'
import { formatQuota, parseQuotaFromDollars } from '@/lib/format'
import { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet'
import { Textarea } from '@/components/ui/textarea'
import { createUser, updateUser, getUser, getGroups } from '../api'
import { BINDING_FIELDS, ERROR_MESSAGES, SUCCESS_MESSAGES } from '../constants'
import {
userFormSchema,
type UserFormValues,
USER_FORM_DEFAULT_VALUES,
transformFormDataToPayload,
transformUserToFormDefaults,
} from '../lib'
import { type User } from '../types'
import { UserQuotaDialog } from './user-quota-dialog'
import { useUsers } from './users-provider'
type UsersMutateDrawerProps = {
open: boolean
onOpenChange: (open: boolean) => void
currentRow?: User
}
export function UsersMutateDrawer({
open,
onOpenChange,
currentRow,
}: UsersMutateDrawerProps) {
const { t } = useTranslation()
const isUpdate = !!currentRow
const { triggerRefresh } = useUsers()
const [isSubmitting, setIsSubmitting] = useState(false)
const [quotaDialogOpen, setQuotaDialogOpen] = useState(false)
// Fetch groups
const { data: groupsData } = useQuery({
queryKey: ['groups'],
queryFn: getGroups,
staleTime: 5 * 60 * 1000,
})
const groups = groupsData?.data || []
const form = useForm<UserFormValues>({
resolver: zodResolver(userFormSchema),
defaultValues: USER_FORM_DEFAULT_VALUES,
})
// Load existing data when updating
useEffect(() => {
if (open && isUpdate && currentRow) {
// For update, fetch fresh data
getUser(currentRow.id).then((result) => {
if (result.success && result.data) {
form.reset(transformUserToFormDefaults(result.data))
}
})
} else if (open && !isUpdate) {
// For create, reset to defaults
form.reset(USER_FORM_DEFAULT_VALUES)
}
}, [open, isUpdate, currentRow, form])
const { meta: currencyMeta } = getCurrencyDisplay()
const currencyLabel = getCurrencyLabel()
const tokensOnly = currencyMeta.kind === 'tokens'
const currentQuotaRaw = form.watch('quota_dollars') || 0
const onSubmit = async (data: UserFormValues) => {
setIsSubmitting(true)
try {
const payload = transformFormDataToPayload(data, currentRow?.id)
const result = isUpdate
? await updateUser(payload as typeof payload & { id: number })
: await createUser(payload)
if (result.success) {
toast.success(
isUpdate
? t(SUCCESS_MESSAGES.USER_UPDATED)
: t(SUCCESS_MESSAGES.USER_CREATED)
)
onOpenChange(false)
triggerRefresh()
} else {
toast.error(
result.message ||
(isUpdate
? t(ERROR_MESSAGES.UPDATE_FAILED)
: t(ERROR_MESSAGES.CREATE_FAILED))
)
}
} catch (_error) {
toast.error(t(ERROR_MESSAGES.UNEXPECTED))
} finally {
setIsSubmitting(false)
}
}
const refreshUserData = async () => {
if (!currentRow) return
const result = await getUser(currentRow.id)
if (result.success && result.data) {
form.reset(transformUserToFormDefaults(result.data))
}
triggerRefresh()
}
return (
<>
<Sheet
open={open}
onOpenChange={(v) => {
onOpenChange(v)
if (!v) {
form.reset()
}
}}
>
<SheetContent className='flex h-dvh w-full flex-col gap-0 overflow-hidden p-0 sm:max-w-[600px]'>
<SheetHeader className='border-b px-4 py-3 text-start sm:px-6 sm:py-4'>
<SheetTitle>
{isUpdate ? t('Update') : t('Create')} {t('User')}
</SheetTitle>
<SheetDescription>
{isUpdate
? t('Update the user by providing necessary info.')
: t('Add a new user by providing necessary info.')}
</SheetDescription>
</SheetHeader>
<Form {...form}>
<form
id='user-form'
onSubmit={form.handleSubmit(onSubmit)}
className='flex-1 space-y-4 overflow-y-auto px-3 py-3 pb-4 sm:space-y-6 sm:px-4'
>
{/* Basic Information */}
<div className='space-y-4'>
<h3 className='text-sm font-medium'>
{t('Basic Information')}
</h3>
<FormField
control={form.control}
name='username'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Username')}</FormLabel>
<FormControl>
<Input
{...field}
placeholder={t('Enter username')}
disabled={isUpdate}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{!isUpdate && (
<FormField
control={form.control}
name='role'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Role')}</FormLabel>
<Select
onValueChange={(value) =>
field.onChange(parseInt(value))
}
value={String(field.value)}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('Select a role')} />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value='1'>
{t('Common User')}
</SelectItem>
<SelectItem value='10'>{t('Admin')}</SelectItem>
</SelectContent>
</Select>
<FormDescription>
{t("Set the user's role (cannot be Root)")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name='display_name'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Display Name')}</FormLabel>
<FormControl>
<Input
{...field}
placeholder={t('Enter display name')}
/>
</FormControl>
<FormDescription>
{t('Leave empty to use username')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='password'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Password')}</FormLabel>
<FormControl>
<Input
{...field}
type='password'
placeholder={
isUpdate
? t('Leave empty to keep unchanged')
: t('Enter password (min 8 characters)')
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Group & Quota Settings (Update only) */}
{isUpdate && (
<div className='space-y-4'>
<h3 className='text-sm font-medium'>{t('Group & Quota')}</h3>
<FormField
control={form.control}
name='group'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Group')}</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('Select a group')} />
</SelectTrigger>
</FormControl>
<SelectContent>
{groups.map((group) => (
<SelectItem key={group} value={group}>
{group}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='quota_dollars'
render={({ field }) => (
<FormItem>
<FormLabel>
{t('Remaining Quota ({{currency}})', {
currency: currencyLabel,
})}
</FormLabel>
<div className='flex gap-2'>
<FormControl>
<Input
value={
tokensOnly
? String(field.value || 0)
: (field.value || 0).toFixed(6)
}
readOnly
className='flex-1'
/>
</FormControl>
<Button
type='button'
variant='outline'
onClick={() => setQuotaDialogOpen(true)}
>
<Pencil className='mr-1 h-4 w-4' />
{t('Adjust Quota')}
</Button>
</div>
<FormDescription>
{formatQuota(parseQuotaFromDollars(field.value || 0))}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='remark'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Remark')}</FormLabel>
<FormControl>
<Textarea
{...field}
placeholder={t(
'Admin notes (only visible to admins)'
)}
rows={3}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
{/* Binding Information (Read-only) */}
{isUpdate && (
<div className='space-y-4'>
<h3 className='text-sm font-medium'>
{t('Binding Information')}
</h3>
<p className='text-muted-foreground text-xs'>
{t(
'Third-party account bindings (read-only, managed by user in profile settings)'
)}
</p>
<div className='space-y-3'>
{BINDING_FIELDS.map(({ key, label }) => (
<div key={key}>
<Label className='text-muted-foreground text-xs'>
{t(label)}
</Label>
<Input
value={
(currentRow?.[key as keyof User] as string) || '-'
}
disabled
className='mt-1'
/>
</div>
))}
</div>
</div>
)}
</form>
</Form>
<SheetFooter className='grid grid-cols-2 gap-2 border-t px-4 py-3 sm:flex sm:px-6 sm:py-4'>
<SheetClose asChild>
<Button variant='outline'>{t('Close')}</Button>
</SheetClose>
<Button form='user-form' type='submit' disabled={isSubmitting}>
{isSubmitting ? t('Saving...') : t('Save changes')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
{/* Adjust Quota Dialog */}
{currentRow && (
<UserQuotaDialog
open={quotaDialogOpen}
onOpenChange={setQuotaDialogOpen}
userId={currentRow.id}
currentQuota={parseQuotaFromDollars(currentQuotaRaw || 0)}
onSuccess={refreshUserData}
/>
)}
</>
)
}