Fix template form: location picker auto-open and event title wording

- LocationPicker: skip initial mount effect so dropdown doesn't auto-open
  when form loads with an existing location value
- EventForm: separate templateData/templateName props from event prop so
  template-based creation shows "Create Event from X Template" title
  instead of "Edit Event", and correctly uses Create button + no Delete
- CalendarPage: pass templateName through to EventForm

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-02-23 13:10:31 +08:00
parent b21343601b
commit f64e181fbe
3 changed files with 40 additions and 17 deletions

View File

@ -43,6 +43,7 @@ export default function CalendarPage() {
const [calendarTitle, setCalendarTitle] = useState(''); const [calendarTitle, setCalendarTitle] = useState('');
const [templateEvent, setTemplateEvent] = useState<Partial<CalendarEvent> | null>(null); const [templateEvent, setTemplateEvent] = useState<Partial<CalendarEvent> | null>(null);
const [templateName, setTemplateName] = useState<string | null>(null);
// Scope dialog state // Scope dialog state
const [scopeDialogOpen, setScopeDialogOpen] = useState(false); const [scopeDialogOpen, setScopeDialogOpen] = useState(false);
@ -284,6 +285,7 @@ export default function CalendarPage() {
setShowForm(false); setShowForm(false);
setEditingEvent(null); setEditingEvent(null);
setTemplateEvent(null); setTemplateEvent(null);
setTemplateName(null);
setActiveEditScope(null); setActiveEditScope(null);
setSelectedStart(null); setSelectedStart(null);
setSelectedEnd(null); setSelectedEnd(null);
@ -300,6 +302,7 @@ export default function CalendarPage() {
is_starred: template.is_starred, is_starred: template.is_starred,
recurrence_rule: template.recurrence_rule || undefined, recurrence_rule: template.recurrence_rule || undefined,
} as Partial<CalendarEvent>); } as Partial<CalendarEvent>);
setTemplateName(template.name);
setEditingEvent(null); setEditingEvent(null);
setShowForm(true); setShowForm(true);
}; };
@ -385,7 +388,9 @@ export default function CalendarPage() {
{showForm && ( {showForm && (
<EventForm <EventForm
event={editingEvent || (templateEvent as CalendarEvent | null)} event={editingEvent}
templateData={templateEvent}
templateName={templateName}
initialStart={selectedStart} initialStart={selectedStart}
initialEnd={selectedEnd} initialEnd={selectedEnd}
initialAllDay={selectedAllDay} initialAllDay={selectedAllDay}

View File

@ -22,6 +22,8 @@ import LocationPicker from '@/components/ui/location-picker';
interface EventFormProps { interface EventFormProps {
event: CalendarEvent | null; event: CalendarEvent | null;
templateData?: Partial<CalendarEvent> | null;
templateName?: string | null;
initialStart?: string | null; initialStart?: string | null;
initialEnd?: string | null; initialEnd?: string | null;
initialAllDay?: boolean; initialAllDay?: boolean;
@ -88,10 +90,12 @@ function plusOneHour(dt: string): string {
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`; return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
} }
export default function EventForm({ event, initialStart, initialEnd, initialAllDay, editScope, onClose }: EventFormProps) { export default function EventForm({ event, templateData, templateName, initialStart, initialEnd, initialAllDay, editScope, onClose }: EventFormProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data: calendars = [] } = useCalendars(); const { data: calendars = [] } = useCalendars();
const isAllDay = event?.all_day ?? initialAllDay ?? false; // Merge template data as defaults for new events
const source = event || templateData;
const isAllDay = source?.all_day ?? initialAllDay ?? false;
// Default to current time / +1 hour when creating a new event with no selection // Default to current time / +1 hour when creating a new event with no selection
const defaultStart = nowLocal(); const defaultStart = nowLocal();
@ -100,23 +104,24 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
const rawEnd = event?.end_datetime || initialEnd || defaultEnd; const rawEnd = event?.end_datetime || initialEnd || defaultEnd;
const defaultCalendar = calendars.find((c) => c.is_default); const defaultCalendar = calendars.find((c) => c.is_default);
const initialCalendarId = event?.calendar_id?.toString() || defaultCalendar?.id?.toString() || ''; const initialCalendarId = source?.calendar_id?.toString() || defaultCalendar?.id?.toString() || '';
const isEditing = !!event?.id;
// For all-day events, adjust end date for display (FullCalendar exclusive end) // For all-day events, adjust end date for display (FullCalendar exclusive end)
const displayEnd = isAllDay ? adjustAllDayEndForDisplay(rawEnd) : rawEnd; const displayEnd = isAllDay ? adjustAllDayEndForDisplay(rawEnd) : rawEnd;
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
title: event?.title || '', title: source?.title || '',
description: event?.description || '', description: source?.description || '',
start_datetime: formatForInput(rawStart, isAllDay, '09:00'), start_datetime: formatForInput(rawStart, isAllDay, '09:00'),
end_datetime: formatForInput(displayEnd, isAllDay, '10:00'), end_datetime: formatForInput(displayEnd, isAllDay, '10:00'),
all_day: isAllDay, all_day: isAllDay,
location_id: event?.location_id?.toString() || '', location_id: source?.location_id?.toString() || '',
calendar_id: initialCalendarId, calendar_id: initialCalendarId,
is_starred: event?.is_starred || false, is_starred: source?.is_starred || false,
}); });
const existingRule = parseRecurrenceRule(event?.recurrence_rule); const existingRule = parseRecurrenceRule(source?.recurrence_rule);
const [recurrenceType, setRecurrenceType] = useState<string>(existingRule?.type || ''); const [recurrenceType, setRecurrenceType] = useState<string>(existingRule?.type || '');
const [recurrenceInterval, setRecurrenceInterval] = useState(existingRule?.interval || 2); const [recurrenceInterval, setRecurrenceInterval] = useState(existingRule?.interval || 2);
const [recurrenceWeekday, setRecurrenceWeekday] = useState(existingRule?.weekday ?? 1); const [recurrenceWeekday, setRecurrenceWeekday] = useState(existingRule?.weekday ?? 1);
@ -132,7 +137,7 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
}); });
// Location picker state // Location picker state
const existingLocation = locations.find((l) => l.id === event?.location_id); const existingLocation = locations.find((l) => l.id === source?.location_id);
const [locationSearch, setLocationSearch] = useState(existingLocation?.name || ''); const [locationSearch, setLocationSearch] = useState(existingLocation?.name || '');
const selectableCalendars = calendars.filter((c) => !c.is_system); const selectableCalendars = calendars.filter((c) => !c.is_system);
@ -176,11 +181,11 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
recurrence_rule: rule, recurrence_rule: rule,
}; };
if (event?.id) { if (isEditing) {
if (editScope) { if (editScope) {
payload.edit_scope = editScope; payload.edit_scope = editScope;
} }
const response = await api.put(`/events/${event.id}`, payload); const response = await api.put(`/events/${event!.id}`, payload);
return response.data; return response.data;
} else { } else {
const response = await api.post('/events', payload); const response = await api.post('/events', payload);
@ -191,11 +196,11 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
queryClient.invalidateQueries({ queryKey: ['calendar-events'] }); queryClient.invalidateQueries({ queryKey: ['calendar-events'] });
queryClient.invalidateQueries({ queryKey: ['dashboard'] }); queryClient.invalidateQueries({ queryKey: ['dashboard'] });
queryClient.invalidateQueries({ queryKey: ['upcoming'] }); queryClient.invalidateQueries({ queryKey: ['upcoming'] });
toast.success(event ? 'Event updated' : 'Event created'); toast.success(isEditing ? 'Event updated' : 'Event created');
onClose(); onClose();
}, },
onError: (error) => { onError: (error) => {
toast.error(getErrorMessage(error, event ? 'Failed to update event' : 'Failed to create event')); toast.error(getErrorMessage(error, isEditing ? 'Failed to update event' : 'Failed to create event'));
}, },
}); });
@ -226,7 +231,13 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
<SheetContent> <SheetContent>
<SheetClose onClick={onClose} /> <SheetClose onClick={onClose} />
<SheetHeader> <SheetHeader>
<SheetTitle>{event ? 'Edit Event' : 'New Event'}</SheetTitle> <SheetTitle>
{isEditing
? 'Edit Event'
: templateName
? `Create Event from ${templateName} Template`
: 'New Event'}
</SheetTitle>
</SheetHeader> </SheetHeader>
<form onSubmit={handleSubmit} className="flex flex-col flex-1 overflow-y-auto"> <form onSubmit={handleSubmit} className="flex flex-col flex-1 overflow-y-auto">
<div className="px-6 py-5 space-y-4 flex-1"> <div className="px-6 py-5 space-y-4 flex-1">
@ -429,7 +440,7 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
</div> </div>
<SheetFooter> <SheetFooter>
{event && ( {isEditing && (
<Button <Button
type="button" type="button"
variant="destructive" variant="destructive"
@ -444,7 +455,7 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={mutation.isPending}> <Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Saving...' : event ? 'Update' : 'Create'} {mutation.isPending ? 'Saving...' : isEditing ? 'Update' : 'Create'}
</Button> </Button>
</SheetFooter> </SheetFooter>
</form> </form>

View File

@ -25,8 +25,15 @@ export default function LocationPicker({ value, onChange, onSelect, placeholder
const debounceRef = useRef<ReturnType<typeof setTimeout>>(); const debounceRef = useRef<ReturnType<typeof setTimeout>>();
const requestIdRef = useRef(0); const requestIdRef = useRef(0);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const hasMountedRef = useRef(false);
useEffect(() => { useEffect(() => {
// Skip the initial mount to prevent auto-opening the dropdown
if (!hasMountedRef.current) {
hasMountedRef.current = true;
return;
}
if (debounceRef.current) clearTimeout(debounceRef.current); if (debounceRef.current) clearTimeout(debounceRef.current);
if (!value || value.length < 2) { if (!value || value.length < 2) {