Convert TemplateForm from Dialog to Sheet side panel

Matches the EventForm UI pattern for consistency — same slide-in panel,
same layout structure with scrollable content area and pinned footer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-02-23 11:58:55 +08:00
parent 4a8c44ab80
commit b21343601b

View File

@ -5,13 +5,13 @@ import api, { getErrorMessage } from '@/lib/api';
import type { EventTemplate, Location } from '@/types'; import type { EventTemplate, Location } from '@/types';
import { useCalendars } from '@/hooks/useCalendars'; import { useCalendars } from '@/hooks/useCalendars';
import { import {
Dialog, Sheet,
DialogContent, SheetContent,
DialogHeader, SheetHeader,
DialogTitle, SheetTitle,
DialogFooter, SheetFooter,
DialogClose, SheetClose,
} from '@/components/ui/dialog'; } from '@/components/ui/sheet';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Select } from '@/components/ui/select'; import { Select } from '@/components/ui/select';
@ -87,119 +87,123 @@ export default function TemplateForm({ template, onClose }: TemplateFormProps) {
}; };
return ( return (
<Dialog open={true} onOpenChange={onClose}> <Sheet open={true} onOpenChange={onClose}>
<DialogContent className="max-w-2xl"> <SheetContent>
<DialogClose onClick={onClose} /> <SheetClose onClick={onClose} />
<DialogHeader> <SheetHeader>
<DialogTitle>{template ? 'Edit Template' : 'New Template'}</DialogTitle> <SheetTitle>{template ? 'Edit Template' : 'New Template'}</SheetTitle>
</DialogHeader> </SheetHeader>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="flex flex-col flex-1 overflow-y-auto">
<div className="space-y-2"> <div className="px-6 py-5 space-y-4 flex-1">
<Label htmlFor="tmpl-name" required>Template Name</Label> <div className="space-y-2">
<Input <Label htmlFor="tmpl-name" required>Template Name</Label>
id="tmpl-name" <Input
value={formData.name} id="tmpl-name"
onChange={(e) => setFormData({ ...formData, name: e.target.value })} value={formData.name}
placeholder="e.g., Weekly standup" onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required placeholder="e.g., Weekly standup"
/> required
</div>
<div className="space-y-2">
<Label htmlFor="tmpl-title" required>Event Title</Label>
<Input
id="tmpl-title"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
placeholder="Title for created events"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="tmpl-desc">Description</Label>
<Textarea
id="tmpl-desc"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className="min-h-[60px]"
/>
</div>
<div className="space-y-2">
<Label htmlFor="tmpl-calendar">Calendar</Label>
<Select
id="tmpl-calendar"
value={formData.calendar_id}
onChange={(e) => setFormData({ ...formData, calendar_id: e.target.value })}
>
<option value="">Default</option>
{selectableCalendars.map((cal) => (
<option key={cal.id} value={cal.id}>{cal.name}</option>
))}
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="tmpl-location">Location</Label>
<LocationPicker
id="tmpl-location"
value={locationSearch}
onChange={(val) => {
setLocationSearch(val);
if (!val) setFormData({ ...formData, location_id: '' });
}}
onSelect={async (result) => {
if (result.source === 'local' && result.location_id) {
setFormData({ ...formData, location_id: result.location_id.toString() });
} else if (result.source === 'nominatim') {
try {
const { data: newLoc } = await api.post('/locations', {
name: result.name,
address: result.address,
category: 'other',
});
queryClient.invalidateQueries({ queryKey: ['locations'] });
setFormData({ ...formData, location_id: newLoc.id.toString() });
toast.success(`Location "${result.name}" created`);
} catch {
toast.error('Failed to create location');
}
}
}}
placeholder="Search for a location..."
/>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Checkbox
id="tmpl-allday"
checked={formData.all_day}
onChange={(e) => setFormData({ ...formData, all_day: (e.target as HTMLInputElement).checked })}
/> />
<Label htmlFor="tmpl-allday">All day</Label>
</div> </div>
<div className="flex items-center gap-2">
<Checkbox <div className="space-y-2">
id="tmpl-starred" <Label htmlFor="tmpl-title" required>Event Title</Label>
checked={formData.is_starred} <Input
onChange={(e) => setFormData({ ...formData, is_starred: (e.target as HTMLInputElement).checked })} id="tmpl-title"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
placeholder="Title for created events"
required
/> />
<Label htmlFor="tmpl-starred">Starred</Label> </div>
<div className="space-y-2">
<Label htmlFor="tmpl-desc">Description</Label>
<Textarea
id="tmpl-desc"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className="min-h-[80px] flex-1"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="tmpl-calendar">Calendar</Label>
<Select
id="tmpl-calendar"
value={formData.calendar_id}
onChange={(e) => setFormData({ ...formData, calendar_id: e.target.value })}
>
<option value="">Default</option>
{selectableCalendars.map((cal) => (
<option key={cal.id} value={cal.id}>{cal.name}</option>
))}
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="tmpl-location">Location</Label>
<LocationPicker
id="tmpl-location"
value={locationSearch}
onChange={(val) => {
setLocationSearch(val);
if (!val) setFormData({ ...formData, location_id: '' });
}}
onSelect={async (result) => {
if (result.source === 'local' && result.location_id) {
setFormData({ ...formData, location_id: result.location_id.toString() });
} else if (result.source === 'nominatim') {
try {
const { data: newLoc } = await api.post('/locations', {
name: result.name,
address: result.address,
category: 'other',
});
queryClient.invalidateQueries({ queryKey: ['locations'] });
setFormData({ ...formData, location_id: newLoc.id.toString() });
toast.success(`Location "${result.name}" created`);
} catch {
toast.error('Failed to create location');
}
}
}}
placeholder="Search for a location..."
/>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Checkbox
id="tmpl-allday"
checked={formData.all_day}
onChange={(e) => setFormData({ ...formData, all_day: (e.target as HTMLInputElement).checked })}
/>
<Label htmlFor="tmpl-allday">All day</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="tmpl-starred"
checked={formData.is_starred}
onChange={(e) => setFormData({ ...formData, is_starred: (e.target as HTMLInputElement).checked })}
/>
<Label htmlFor="tmpl-starred">Starred</Label>
</div>
</div> </div>
</div> </div>
<DialogFooter> <SheetFooter>
<Button type="button" variant="outline" onClick={onClose}> <Button type="button" variant="outline" onClick={onClose}>
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={mutation.isPending}> <Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Saving...' : template ? 'Update' : 'Create'} {mutation.isPending ? 'Saving...' : template ? 'Update' : 'Create'}
</Button> </Button>
</DialogFooter> </SheetFooter>
</form> </form>
</DialogContent> </SheetContent>
</Dialog> </Sheet>
); );
} }