From bc2da09be58adaf1a2a66cfab937871656a018df Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 4 Nov 2026 17:47:25 +0100 Subject: [PATCH] feat: save a quick event without a title as "(No title)" --- .../calendar/__tests__/calendar-app.test.tsx | 15 +++++++++++++++ components/calendar/calendar-app.tsx | 11 +++++++---- components/calendar/quick-create-popover.tsx | 3 ++- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/components/calendar/__tests__/calendar-app.test.tsx b/components/calendar/__tests__/calendar-app.test.tsx index f02537e85..3819a6c36 100644 --- a/components/calendar/__tests__/calendar-app.test.tsx +++ b/components/calendar/__tests__/calendar-app.test.tsx @@ -595,6 +595,21 @@ describe('CalendarApp task feedback', () => { }); }); +describe('CalendarApp quick editor', () => { + // Usability report 2026-11-04: Save stayed off until a title was typed. + // Google Calendar saves "(No title)"; so does the quick editor now. + it('saves an event without a title as "(No title)"', async () => { + renderApp(); + fireEvent.click(slots()[3]); + const quick = await screen.findByRole('dialog', { name: 'quick_create.aria_label' }, { timeout: 2000 }); + const save = within(quick).getByRole('button', { name: 'form.save' }); + expect(save).toBeEnabled(); + fireEvent.click(save); + await waitFor(() => expect(store.createEvent).toHaveBeenCalledTimes(1)); + expect(store.createEvent.mock.calls[0][1]).toMatchObject({ title: 'events.no_title', showWithoutTime: false }); + }); +}); + describe('CalendarApp creating in the month view', () => { it('opens the quick editor for an all-day event on a single click', async () => { setUp([], 'month'); diff --git a/components/calendar/calendar-app.tsx b/components/calendar/calendar-app.tsx index 9bda4c125..1a47f33d7 100644 --- a/components/calendar/calendar-app.tsx +++ b/components/calendar/calendar-app.tsx @@ -1287,12 +1287,15 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = const handleQuickCreateSave = useCallback(async () => { if (!quickDraft) return; - const { start, end, title, calendarId, allDay } = quickDraft; + const { start, end, calendarId, allDay } = quickDraft; + // Saving without a title is allowed, as in Google Calendar: the event + // is called "(No title)" in the user's language. + const title = quickDraft.title.trim() || t("events.no_title"); if (allDay) { setQuickDraft(null); await handleSaveEvent({ uid: generateUUID(), - title: title.trim(), + title, start: format(start, "yyyy-MM-dd'T'00:00:00"), duration: "P1D", timeZone: null, @@ -1313,7 +1316,7 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = // as the full editor's fields, so they are labelled with that zone. await handleSaveEvent({ uid: generateUUID(), - title: title.trim(), + title, start: format(start, "yyyy-MM-dd'T'HH:mm:ss"), duration, timeZone: getEffectiveTimeZone(), @@ -1323,7 +1326,7 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = freeBusyStatus: "busy", privacy: "public", }); - }, [quickDraft, handleSaveEvent]); + }, [quickDraft, handleSaveEvent, t]); // While the quick editor is open its placeholder takes the preview slot. const gridPreview = useMemo(() => { diff --git a/components/calendar/quick-create-popover.tsx b/components/calendar/quick-create-popover.tsx index 913afae41..87f969096 100644 --- a/components/calendar/quick-create-popover.tsx +++ b/components/calendar/quick-create-popover.tsx @@ -148,7 +148,8 @@ export function QuickCreatePopover({ }; }, [onClose]); - const canSave = draft.title.trim().length > 0 && !!draft.calendarId; + // An empty title saves as "(No title)", as in Google Calendar. + const canSave = !!draft.calendarId; const handleSubmit = useCallback((e: React.FormEvent) => { e.preventDefault(); if (canSave) onSave();