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

View File

@ -22,6 +22,8 @@ import LocationPicker from '@/components/ui/location-picker';
interface EventFormProps {
event: CalendarEvent | null;
templateData?: Partial<CalendarEvent> | null;
templateName?: string | null;
initialStart?: string | null;
initialEnd?: string | null;
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())}`;
}
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 { 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
const defaultStart = nowLocal();
@ -100,23 +104,24 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
const rawEnd = event?.end_datetime || initialEnd || defaultEnd;
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)
const displayEnd = isAllDay ? adjustAllDayEndForDisplay(rawEnd) : rawEnd;
const [formData, setFormData] = useState({
title: event?.title || '',
description: event?.description || '',
title: source?.title || '',
description: source?.description || '',
start_datetime: formatForInput(rawStart, isAllDay, '09:00'),
end_datetime: formatForInput(displayEnd, isAllDay, '10:00'),
all_day: isAllDay,
location_id: event?.location_id?.toString() || '',
location_id: source?.location_id?.toString() || '',
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 [recurrenceInterval, setRecurrenceInterval] = useState(existingRule?.interval || 2);
const [recurrenceWeekday, setRecurrenceWeekday] = useState(existingRule?.weekday ?? 1);
@ -132,7 +137,7 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
});
// 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 selectableCalendars = calendars.filter((c) => !c.is_system);
@ -176,11 +181,11 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
recurrence_rule: rule,
};
if (event?.id) {
if (isEditing) {
if (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;
} else {
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: ['dashboard'] });
queryClient.invalidateQueries({ queryKey: ['upcoming'] });
toast.success(event ? 'Event updated' : 'Event created');
toast.success(isEditing ? 'Event updated' : 'Event created');
onClose();
},
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>
<SheetClose onClick={onClose} />
<SheetHeader>
<SheetTitle>{event ? 'Edit Event' : 'New Event'}</SheetTitle>
<SheetTitle>
{isEditing
? 'Edit Event'
: templateName
? `Create Event from ${templateName} Template`
: 'New Event'}
</SheetTitle>
</SheetHeader>
<form onSubmit={handleSubmit} className="flex flex-col flex-1 overflow-y-auto">
<div className="px-6 py-5 space-y-4 flex-1">
@ -429,7 +440,7 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
</div>
<SheetFooter>
{event && (
{isEditing && (
<Button
type="button"
variant="destructive"
@ -444,7 +455,7 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
Cancel
</Button>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Saving...' : event ? 'Update' : 'Create'}
{mutation.isPending ? 'Saving...' : isEditing ? 'Update' : 'Create'}
</Button>
</SheetFooter>
</form>

View File

@ -25,8 +25,15 @@ export default function LocationPicker({ value, onChange, onSelect, placeholder
const debounceRef = useRef<ReturnType<typeof setTimeout>>();
const requestIdRef = useRef(0);
const containerRef = useRef<HTMLDivElement>(null);
const hasMountedRef = useRef(false);
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 (!value || value.length < 2) {