refactor: Align form validation UX across management dialogs

This commit is contained in:
2026-06-18 11:05:31 +05:30
parent d7c4027328
commit 9695c80e20
23 changed files with 1458 additions and 1017 deletions

View File

@@ -1,9 +1,10 @@
'use client';
import type { ComponentProps } from 'react';
import type { UseFormRegister } from 'react-hook-form';
import type { FieldErrors, UseFormRegister } from 'react-hook-form';
import { Loader2 } from 'lucide-react';
import { FormField } from '@/components/form';
import { Button } from '@/components/ui/button';
import {
Dialog,
@@ -14,7 +15,6 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
@@ -32,7 +32,10 @@ interface TenantSheetProps {
tenantId?: number;
isEditMode: boolean;
register: UseFormRegister<TenantFormValues>;
errors: FieldErrors<TenantFormValues>;
isSubmitted: boolean;
onSubmit: ComponentProps<'form'>['onSubmit'];
canSubmit: boolean;
clientId: string;
planId: string;
onClientChange: (clientId: string) => void;
@@ -51,7 +54,10 @@ export function TenantSheet({
tenantId,
isEditMode,
register,
errors,
isSubmitted,
onSubmit,
canSubmit,
clientId,
planId,
onClientChange,
@@ -63,181 +69,234 @@ export function TenantSheet({
isSaving,
adminEmail,
}: TenantSheetProps) {
const fieldError = (field: keyof TenantFormValues) =>
isSubmitted ? errors[field]?.message : undefined;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-h-[calc(100vh-2rem)] overflow-y-auto sm:max-w-3xl">
<DialogHeader>
<DialogTitle>
<DialogContent className="flex max-h-[calc(100vh-2rem)] flex-col gap-0 p-0 sm:max-w-3xl">
<DialogHeader className="shrink-0 border-b px-6 py-4 pr-12">
<DialogTitle className="text-lg leading-none font-semibold tracking-tight">
{tenantId ? 'Edit Tenant' : 'Create Tenant'}
</DialogTitle>
<DialogDescription>
<DialogDescription className="text-sm">
Bind a client and subscription plan, then invite the tenant
administrator.
</DialogDescription>
</DialogHeader>
<form onSubmit={onSubmit} className="flex flex-1 flex-col gap-5 px-4">
<div className="space-y-1">
<p>Basic Information</p>
<p className="text-muted-foreground">
Tenant identity and subscription binding.
</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="tenant-name">Tenant Name</Label>
<Input
id="tenant-name"
placeholder="Acme Corp"
{...register('name', {
required: true,
onChange: (event) => onNameChange(event.target.value),
})}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="tenant-slug">Slug</Label>
<Input
id="tenant-slug"
placeholder="acme-corp"
{...register('slug', { required: true })}
required
readOnly={isEditMode}
/>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>Client</Label>
<Select
value={clientId}
onValueChange={onClientChange}
disabled={isLookupsLoading}
>
<SelectTrigger>
<SelectValue
placeholder={
isLookupsLoading ? 'Loading clients...' : 'Select client'
}
/>
</SelectTrigger>
<SelectContent>
{clients.map((client) => (
<SelectItem key={client.id} value={String(client.id)}>
{client.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Subscription Plan</Label>
<Select
value={planId}
onValueChange={onPlanChange}
disabled={isLookupsLoading}
>
<SelectTrigger>
<SelectValue
placeholder={
isLookupsLoading ? 'Loading plans...' : 'Select plan'
}
/>
</SelectTrigger>
<SelectContent>
{plans.map((plan) => (
<SelectItem key={plan.id} value={String(plan.id)}>
{plan.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="tenant-domain">Domain</Label>
<Input
id="tenant-domain"
placeholder="acme.example.com"
{...register('domain')}
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="tenant-description">Description</Label>
<textarea
id="tenant-description"
placeholder="North zone operations"
{...register('description')}
className="min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 outline-none focus-visible:border-ring"
/>
</div>
</div>
{!isEditMode ? (
<>
<div className="space-y-1">
<p>Tenant Administrator</p>
<p className="text-muted-foreground">
Invitation details for the primary tenant admin account.
</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="admin-first-name">First Name</Label>
<Input
id="admin-first-name"
placeholder="Jane"
{...register('admin_first_name', { required: !isEditMode })}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="admin-last-name">Last Name</Label>
<Input
id="admin-last-name"
placeholder="Doe"
{...register('admin_last_name', { required: !isEditMode })}
required
/>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="admin-email">Admin Email</Label>
<Input
id="admin-email"
type="email"
placeholder="jane@acme.com"
{...register('admin_email', { required: !isEditMode })}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="admin-phone">Phone Number</Label>
<Input
id="admin-phone"
placeholder="+91-9000011111"
{...register('admin_phone_number')}
/>
</div>
</div>
</>
) : adminEmail ? (
<div className="rounded-md border bg-muted/40 p-4 text-muted-foreground">
Admin invitation details cannot be changed after tenant creation.
<p className="mt-1 text-foreground">
Current admin email: <span>{adminEmail}</span>
<form onSubmit={onSubmit} className="flex min-h-0 flex-1 flex-col">
<div className="max-h-[50vh] space-y-5 overflow-y-auto px-6 py-4">
<div className="space-y-1">
<p>Basic Information</p>
<p className="text-muted-foreground">
Tenant identity and subscription binding.
</p>
</div>
) : null}
<DialogFooter className="px-0">
<div className="grid gap-4 md:grid-cols-2">
<FormField
id="tenant-name"
label="Tenant Name"
required
error={fieldError('name')}
>
<Input
id="tenant-name"
placeholder="Enter tenant name"
aria-invalid={!!fieldError('name')}
{...register('name', {
onChange: (event) => onNameChange(event.target.value),
})}
/>
</FormField>
<FormField
id="tenant-slug"
label="Slug"
required
error={fieldError('slug')}
>
<Input
id="tenant-slug"
placeholder="acme-corp"
aria-invalid={!!fieldError('slug')}
{...register('slug')}
readOnly={isEditMode}
/>
</FormField>
</div>
<div className="grid gap-4 md:grid-cols-2">
<FormField label="Client" required error={fieldError('client_id')}>
<Select
value={clientId}
onValueChange={onClientChange}
disabled={isLookupsLoading}
>
<SelectTrigger>
<SelectValue
placeholder={
isLookupsLoading
? 'Loading clients...'
: 'Select client'
}
/>
</SelectTrigger>
<SelectContent>
{clients.map((client) => (
<SelectItem key={client.id} value={String(client.id)}>
{client.name}
</SelectItem>
))}
</SelectContent>
</Select>
<input
type="hidden"
{...register('client_id')}
value={clientId}
readOnly
/>
</FormField>
<FormField
label="Subscription Plan"
required
error={fieldError('plan_id')}
>
<Select
value={planId}
onValueChange={onPlanChange}
disabled={isLookupsLoading}
>
<SelectTrigger>
<SelectValue
placeholder={
isLookupsLoading ? 'Loading plans...' : 'Select plan'
}
/>
</SelectTrigger>
<SelectContent>
{plans.map((plan) => (
<SelectItem key={plan.id} value={String(plan.id)}>
{plan.name}
</SelectItem>
))}
</SelectContent>
</Select>
<input
type="hidden"
{...register('plan_id')}
value={planId}
readOnly
/>
</FormField>
</div>
<div className="grid gap-4 md:grid-cols-2">
<FormField id="tenant-domain" label="Domain">
<Input
id="tenant-domain"
placeholder="acme.example.com"
{...register('domain')}
/>
</FormField>
<FormField
id="tenant-description"
label="Description"
className="md:col-span-2"
>
<textarea
id="tenant-description"
placeholder="North zone operations"
{...register('description')}
className="min-h-20 w-full rounded-md border border-input bg-background px-3 py-2 outline-none focus-visible:border-ring"
/>
</FormField>
</div>
{!isEditMode ? (
<>
<div className="space-y-1">
<p>Tenant Administrator</p>
<p className="text-muted-foreground">
Invitation details for the primary tenant admin account.
</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
<FormField
id="admin-first-name"
label="First Name"
required
error={fieldError('admin_first_name')}
>
<Input
id="admin-first-name"
placeholder="Enter first name"
aria-invalid={!!fieldError('admin_first_name')}
{...register('admin_first_name')}
/>
</FormField>
<FormField
id="admin-last-name"
label="Last Name"
required
error={fieldError('admin_last_name')}
>
<Input
id="admin-last-name"
placeholder="Enter last name"
aria-invalid={!!fieldError('admin_last_name')}
{...register('admin_last_name')}
/>
</FormField>
</div>
<div className="grid gap-4 md:grid-cols-2">
<FormField
id="admin-email"
label="Admin Email"
required
error={fieldError('admin_email')}
>
<Input
id="admin-email"
type="email"
placeholder="admin@example.com"
aria-invalid={!!fieldError('admin_email')}
{...register('admin_email')}
/>
</FormField>
<FormField
id="admin-phone"
label="Phone Number"
error={fieldError('admin_phone_number')}
>
<Input
id="admin-phone"
placeholder="+91-9000011111"
aria-invalid={!!fieldError('admin_phone_number')}
{...register('admin_phone_number')}
/>
</FormField>
</div>
</>
) : adminEmail ? (
<div className="rounded-md border bg-muted/40 p-4 text-muted-foreground">
Admin invitation details cannot be changed after tenant
creation.
<p className="mt-1 text-foreground">
Current admin email: <span>{adminEmail}</span>
</p>
</div>
) : null}
</div>
<DialogFooter className="shrink-0 border-t px-6 py-4">
<Button
type="button"
variant="outline"
@@ -246,7 +305,10 @@ export function TenantSheet({
>
Cancel
</Button>
<Button type="submit" disabled={isSaving || isLookupsLoading}>
<Button
type="submit"
disabled={isSaving || isLookupsLoading || !canSubmit}
>
{isSaving ? (
<Loader2 className="mr-2 size-4 animate-spin" />
) : null}