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:
parent
b21343601b
commit
f64e181fbe
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user