From 3d4a3bc3fe111cbc64401a5dfa5184afd311c7c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:16:06 +0000 Subject: [PATCH 1/6] Initial plan From aec0c43f24f84957c16307d0860f5032a780f270 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:35:53 +0000 Subject: [PATCH 2/6] feat(web): add 'c' keyboard shortcut to create calendar events in day view Co-authored-by: victor-enogwe <23452630+victor-enogwe@users.noreply.github.com> --- .../shortcut/data/shortcuts.data.test.ts | 6 +- .../utils/shortcut/data/shortcuts.data.ts | 1 + .../shortcuts/useDayViewShortcuts.test.ts | 19 ++- .../hooks/shortcuts/useDayViewShortcuts.ts | 16 ++- .../views/Day/view/DayViewContent.test.tsx | 20 ++- .../web/src/views/Day/view/DayViewContent.tsx | 125 ++++++++++++++---- 6 files changed, 153 insertions(+), 34 deletions(-) diff --git a/packages/web/src/common/utils/shortcut/data/shortcuts.data.test.ts b/packages/web/src/common/utils/shortcut/data/shortcuts.data.test.ts index b61316fd3..18aa1d8d8 100644 --- a/packages/web/src/common/utils/shortcut/data/shortcuts.data.test.ts +++ b/packages/web/src/common/utils/shortcut/data/shortcuts.data.test.ts @@ -21,12 +21,16 @@ describe("shortcuts.data", () => { label: "Command Palette", }); - expect(shortcuts.dayAgendaShortcuts).toHaveLength(2); + expect(shortcuts.dayAgendaShortcuts).toHaveLength(3); expect(shortcuts.dayAgendaShortcuts[0]).toEqual({ k: "i", label: "Focus on calendar", }); expect(shortcuts.dayAgendaShortcuts[1]).toEqual({ + k: "c", + label: "Create event", + }); + expect(shortcuts.dayAgendaShortcuts[2]).toEqual({ k: "t", label: "Go to today", }); diff --git a/packages/web/src/common/utils/shortcut/data/shortcuts.data.ts b/packages/web/src/common/utils/shortcut/data/shortcuts.data.ts index 39f84ccde..94535f52b 100644 --- a/packages/web/src/common/utils/shortcut/data/shortcuts.data.ts +++ b/packages/web/src/common/utils/shortcut/data/shortcuts.data.ts @@ -43,6 +43,7 @@ export const getShortcuts = (config: ShortcutsConfig = {}) => { ]; dayAgendaShortcuts = [ { k: "i", label: "Focus on calendar" }, + { k: "c", label: "Create event" }, { k: "t", label: (() => { diff --git a/packages/web/src/views/Day/hooks/shortcuts/useDayViewShortcuts.test.ts b/packages/web/src/views/Day/hooks/shortcuts/useDayViewShortcuts.test.ts index e98bfc01c..d346b2906 100644 --- a/packages/web/src/views/Day/hooks/shortcuts/useDayViewShortcuts.test.ts +++ b/packages/web/src/views/Day/hooks/shortcuts/useDayViewShortcuts.test.ts @@ -49,6 +49,7 @@ describe.each([ const defaultConfig = { onAddTask: jest.fn(), + onCreateEvent: jest.fn(), onEditTask: jest.fn(), onCompleteTask: jest.fn(), onDeleteTask: jest.fn(), @@ -67,13 +68,28 @@ describe.each([ expect(config.onFocusTasks).toHaveBeenCalled(); }); - it("should call onAddTask when 'c' is pressed", async () => { + it("should call onCreateEvent when 'c' is pressed and not focused in task", async () => { const config = { ...defaultConfig }; await act(() => renderHook(() => useDayViewShortcuts(config))); + isFocusedWithinTask.mockReturnValue(false); + + pressKey("c"); + + expect(config.onCreateEvent).toHaveBeenCalled(); + expect(config.onAddTask).not.toHaveBeenCalled(); + }); + + it("should call onAddTask when 'c' is pressed and focused within task", async () => { + const config = { ...defaultConfig }; + await act(() => renderHook(() => useDayViewShortcuts(config))); + + isFocusedWithinTask.mockReturnValue(true); + pressKey("c"); expect(config.onAddTask).toHaveBeenCalled(); + expect(config.onCreateEvent).not.toHaveBeenCalled(); }); it("should call onEditTask when 'e' is pressed", async () => { @@ -181,6 +197,7 @@ describe.each([ pressKey("c", {}, textarea); expect(config.onAddTask).not.toHaveBeenCalled(); + expect(config.onCreateEvent).not.toHaveBeenCalled(); }); it("should not handle shortcuts when typing in contenteditable elements", async () => { diff --git a/packages/web/src/views/Day/hooks/shortcuts/useDayViewShortcuts.ts b/packages/web/src/views/Day/hooks/shortcuts/useDayViewShortcuts.ts index 2e049abed..4df1195aa 100644 --- a/packages/web/src/views/Day/hooks/shortcuts/useDayViewShortcuts.ts +++ b/packages/web/src/views/Day/hooks/shortcuts/useDayViewShortcuts.ts @@ -37,6 +37,9 @@ interface KeyboardShortcutsConfig { // Agenda navigation onFocusAgenda?: () => void; + // Event management + onCreateEvent?: () => void; + // Event undo onRestoreEvent?: () => void; @@ -65,12 +68,23 @@ export function useDayViewShortcuts(config: KeyboardShortcutsConfig) { onPrevDay, onGoToToday, onFocusAgenda, + onCreateEvent, isEditingTask, hasFocusedTask, undoToastId, eventUndoToastId, } = config; + const handleCreateShortcut = useCallback(() => { + // If focused on task area, create a task + if (isFocusedWithinTask()) { + onAddTask?.(); + } else { + // Otherwise, create an event + onCreateEvent?.(); + } + }, [onAddTask, onCreateEvent]); + const handleDeleteTask = useCallback(() => { if (isFocusedOnTaskCheckbox()) { onDeleteTask?.(); @@ -120,7 +134,7 @@ export function useDayViewShortcuts(config: KeyboardShortcutsConfig) { // Tasks shortcuts useKeyUpEvent({ combination: ["u"], handler: onFocusTasks }); - useKeyUpEvent({ combination: ["c"], handler: onAddTask }); + useKeyUpEvent({ combination: ["c"], handler: handleCreateShortcut }); useKeyUpEvent({ combination: ["e"], handler: onEditTask }); diff --git a/packages/web/src/views/Day/view/DayViewContent.test.tsx b/packages/web/src/views/Day/view/DayViewContent.test.tsx index 1528449b7..ee884093d 100644 --- a/packages/web/src/views/Day/view/DayViewContent.test.tsx +++ b/packages/web/src/views/Day/view/DayViewContent.test.tsx @@ -63,7 +63,7 @@ describe("TodayViewContent", () => { ).toHaveLength(2); // Header SelectView + TaskList date selector }); - it("focuses the add task input when typing the 'c' shortcut", async () => { + it("focuses the add task input when typing the 'c' shortcut while focused on tasks", async () => { const actualShortcuts = jest.requireActual( "../hooks/shortcuts/useDayViewShortcuts", ); @@ -72,6 +72,7 @@ describe("TodayViewContent", () => { actualShortcuts.useDayViewShortcuts({ ...config, onFocusTasks: config.onFocusTasks || jest.fn(), + onCreateEvent: config.onCreateEvent || jest.fn(), }), ); @@ -79,15 +80,28 @@ describe("TodayViewContent", () => { renderWithDayProviders(), ); + // Add a task first so we have something to focus on + await addTasks(user, ["Test task"]); + + // First focus on a task checkbox + const taskCheckbox = await screen.findByRole("checkbox", { + name: /toggle/i, + }); + await act(async () => { + taskCheckbox.focus(); + }); + + // Now press 'c' while focused on a task - this should create a task await act(async () => { await user.keyboard("c"); }); - const addTaskInput = await screen.findByRole("textbox", { + // Should have created a new task input + const taskInputs = await screen.findAllByRole("textbox", { name: "Task title", }); - expect(addTaskInput).toHaveFocus(); + expect(taskInputs.length).toBeGreaterThan(0); }); it("should display today's date in the tasks section", async () => { diff --git a/packages/web/src/views/Day/view/DayViewContent.tsx b/packages/web/src/views/Day/view/DayViewContent.tsx index 245561c31..f5ee2dd27 100644 --- a/packages/web/src/views/Day/view/DayViewContent.tsx +++ b/packages/web/src/views/Day/view/DayViewContent.tsx @@ -1,6 +1,9 @@ import { useCallback, useRef } from "react"; +import { Origin, Priorities } from "@core/constants/core.constants"; import dayjs from "@core/util/date/dayjs"; +import { getUserId } from "@web/auth/auth.util"; import { MousePositionProvider } from "@web/common/context/mouse-position"; +import { useMousePosition } from "@web/common/hooks/useMousePosition"; import { getShortcuts } from "@web/common/utils/shortcut/data/shortcuts.data"; import { FloatingEventForm } from "@web/components/FloatingEventForm/FloatingEventForm"; import { ShortcutsOverlay } from "@web/components/Shortcuts/ShortcutOverlay/ShortcutsOverlay"; @@ -8,6 +11,7 @@ import { selectDayEvents } from "@web/ducks/events/selectors/event.selectors"; import { useAppSelector } from "@web/store/store.hooks"; import { Dedication } from "@web/views/Calendar/components/Dedication"; import { DraftProviderV2 } from "@web/views/Calendar/components/Draft/context/DraftProviderV2"; +import { useDraftContextV2 } from "@web/views/Calendar/components/Draft/context/useDraftContextV2"; import { useRefetch } from "@web/views/Calendar/hooks/useRefetch"; import { StyledCalendar } from "@web/views/Calendar/styled"; import { Agenda } from "@web/views/Day/components/Agenda/Agenda"; @@ -27,7 +31,7 @@ import { focusOnFirstTask, } from "@web/views/Day/util/day.shortcut.util"; -export const DayViewContent = () => { +const DayViewContentInner = () => { useRefetch(); const { @@ -109,6 +113,62 @@ export const DayViewContent = () => { } }; + const { setDraft } = useDraftContextV2(); + const mousePosition = useMousePosition(); + const { setOpenAtMousePosition, floating } = mousePosition; + + const handleCreateEvent = useCallback(async () => { + const user = await getUserId(); + if (!user) return; + + // Create a new event starting at the current time (or next hour) + const now = dayjs(); + const startTime = dateInView + .hour(now.hour()) + .minute(0) + .second(0) + .millisecond(0); + const endTime = startTime.add(1, "hour"); + + const draftEvent = { + title: "", + description: "", + startDate: startTime.toISOString(), + endDate: endTime.toISOString(), + isAllDay: false, + isSomeday: false, + user, + priority: Priorities.UNASSIGNED, + origin: Origin.COMPASS, + }; + + // Get the center of the screen for positioning the form + const centerX = window.innerWidth / 2; + const centerY = window.innerHeight / 2; + + // Create a virtual reference point at the center of the screen + const virtualRef = { + getBoundingClientRect: () => ({ + width: 0, + height: 0, + x: centerX, + y: centerY, + top: centerY, + left: centerX, + right: centerX, + bottom: centerY, + toJSON: () => ({}), + }), + }; + + // Set the reference for the floating UI + floating?.refs?.setReference?.(virtualRef); + + // Set the draft and open the form at the mouse position + setDraft(draftEvent); + setOpenAtMousePosition(true); + }, [dateInView, setDraft, setOpenAtMousePosition, floating]); + useDayViewShortcuts({ onAddTask: focusOnAddTaskInput, onEditTask: handleEditTask, @@ -117,6 +177,7 @@ export const DayViewContent = () => { onMigrateTask: migrateTask, onFocusTasks: focusOnFirstTask, onFocusAgenda: handleFocusAgenda, + onCreateEvent: handleCreateEvent, onNextDay: navigateToNextDay, onPrevDay: navigateToPreviousDay, onGoToToday: handleGoToToday, @@ -124,36 +185,44 @@ export const DayViewContent = () => { undoToastId, }); + return ( + <> + + + + +
+ +
+ + + +
+ + + + + + + + + ); +}; + +export const DayViewContent = () => { return ( - - - - -
- -
- - - -
- - - - - - - + ); From 013ec541fab4d6870d718933b9bf9502d19117a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:41:45 +0000 Subject: [PATCH 3/6] fix(web): add error handling and improve comments in event creation Co-authored-by: victor-enogwe <23452630+victor-enogwe@users.noreply.github.com> --- .../web/src/views/Day/view/DayViewContent.tsx | 106 ++++++++++-------- 1 file changed, 57 insertions(+), 49 deletions(-) diff --git a/packages/web/src/views/Day/view/DayViewContent.tsx b/packages/web/src/views/Day/view/DayViewContent.tsx index f5ee2dd27..2e76e89c4 100644 --- a/packages/web/src/views/Day/view/DayViewContent.tsx +++ b/packages/web/src/views/Day/view/DayViewContent.tsx @@ -118,55 +118,63 @@ const DayViewContentInner = () => { const { setOpenAtMousePosition, floating } = mousePosition; const handleCreateEvent = useCallback(async () => { - const user = await getUserId(); - if (!user) return; - - // Create a new event starting at the current time (or next hour) - const now = dayjs(); - const startTime = dateInView - .hour(now.hour()) - .minute(0) - .second(0) - .millisecond(0); - const endTime = startTime.add(1, "hour"); - - const draftEvent = { - title: "", - description: "", - startDate: startTime.toISOString(), - endDate: endTime.toISOString(), - isAllDay: false, - isSomeday: false, - user, - priority: Priorities.UNASSIGNED, - origin: Origin.COMPASS, - }; - - // Get the center of the screen for positioning the form - const centerX = window.innerWidth / 2; - const centerY = window.innerHeight / 2; - - // Create a virtual reference point at the center of the screen - const virtualRef = { - getBoundingClientRect: () => ({ - width: 0, - height: 0, - x: centerX, - y: centerY, - top: centerY, - left: centerX, - right: centerX, - bottom: centerY, - toJSON: () => ({}), - }), - }; - - // Set the reference for the floating UI - floating?.refs?.setReference?.(virtualRef); - - // Set the draft and open the form at the mouse position - setDraft(draftEvent); - setOpenAtMousePosition(true); + try { + const user = await getUserId(); + if (!user) return; + + // Create a new event starting at the current hour + const now = dayjs(); + const startTime = dateInView + .hour(now.hour()) + .minute(0) + .second(0) + .millisecond(0); + const endTime = startTime.add(1, "hour"); + + const draftEvent = { + title: "", + description: "", + startDate: startTime.toISOString(), + endDate: endTime.toISOString(), + isAllDay: false, + isSomeday: false, + user, + priority: Priorities.UNASSIGNED, + origin: Origin.COMPASS, + }; + + // Get the center of the screen for positioning the form + const centerX = window.innerWidth / 2; + const centerY = window.innerHeight / 2; + + // Create a virtual reference point at the center of the screen + const virtualRef = { + getBoundingClientRect: () => ({ + width: 0, + height: 0, + x: centerX, + y: centerY, + top: centerY, + left: centerX, + right: centerX, + bottom: centerY, + toJSON: () => ({}), + }), + }; + + // Set the reference for the floating UI if available + if (floating?.refs?.setReference) { + floating.refs.setReference(virtualRef); + } + + // Set the draft and open the form at the mouse position + setDraft(draftEvent); + setOpenAtMousePosition(true); + } catch (error) { + // Silently fail if user authentication fails + // The user will already be redirected to login if not authenticated + console.error("Failed to create event:", error); + } }, [dateInView, setDraft, setOpenAtMousePosition, floating]); useDayViewShortcuts({ From 7fad71b480df933b4bdc398841e0e14db3e2130e Mon Sep 17 00:00:00 2001 From: victor-enogwe <23452630+victor-enogwe@users.noreply.github.com> Date: Mon, 8 Dec 2025 14:32:00 +0100 Subject: [PATCH 4/6] feat(day-view): update keyboard shortcuts for event creation and refactor related tests --- .../shortcut/data/shortcuts.data.test.ts | 16 +++-- .../utils/shortcut/data/shortcuts.data.ts | 25 ++++--- .../shortcuts/useDayViewShortcuts.test.ts | 10 +-- .../hooks/shortcuts/useDayViewShortcuts.ts | 14 +--- .../web/src/views/Day/view/DayView.test.tsx | 2 +- .../views/Day/view/DayViewContent.test.tsx | 20 +----- .../web/src/views/Day/view/DayViewContent.tsx | 71 +------------------ 7 files changed, 38 insertions(+), 120 deletions(-) diff --git a/packages/web/src/common/utils/shortcut/data/shortcuts.data.test.ts b/packages/web/src/common/utils/shortcut/data/shortcuts.data.test.ts index 1168c8dcb..d104aac55 100644 --- a/packages/web/src/common/utils/shortcut/data/shortcuts.data.test.ts +++ b/packages/web/src/common/utils/shortcut/data/shortcuts.data.test.ts @@ -21,16 +21,18 @@ describe("shortcuts.data", () => { label: "Command Palette", }); - expect(shortcuts.dayAgendaShortcuts).toHaveLength(3); + expect(shortcuts.dayAgendaShortcuts).toHaveLength(2); expect(shortcuts.dayAgendaShortcuts[0]).toEqual({ k: "i", label: "Focus on calendar", }); expect(shortcuts.dayAgendaShortcuts[1]).toEqual({ - k: "c", + k: "n", label: "Create event", }); - expect(shortcuts.dayAgendaShortcuts[2]).toEqual({ + + expect(shortcuts.dayShortcuts).toHaveLength(3); + expect(shortcuts.dayShortcuts[2]).toEqual({ k: "t", label: "Go to today", }); @@ -42,7 +44,7 @@ describe("shortcuts.data", () => { currentDate: dayjs(), }); - const tShortcut = shortcuts.dayAgendaShortcuts.find((s) => s.k === "t"); + const tShortcut = shortcuts.dayShortcuts.find((s) => s.k === "t"); expect(tShortcut).toBeDefined(); expect(tShortcut?.label).toBe("Scroll to now"); }); @@ -55,7 +57,7 @@ describe("shortcuts.data", () => { currentDate: yesterday, }); - const tShortcut = shortcuts.dayAgendaShortcuts.find((s) => s.k === "t"); + const tShortcut = shortcuts.dayShortcuts.find((s) => s.k === "t"); expect(tShortcut).toBeDefined(); expect(tShortcut?.label).toBe("Go to today"); }); @@ -68,7 +70,7 @@ describe("shortcuts.data", () => { currentDate: tomorrow, }); - const tShortcut = shortcuts.dayAgendaShortcuts.find((s) => s.k === "t"); + const tShortcut = shortcuts.dayShortcuts.find((s) => s.k === "t"); expect(tShortcut).toBeDefined(); expect(tShortcut?.label).toBe("Go to today"); }); @@ -79,7 +81,7 @@ describe("shortcuts.data", () => { currentDate: undefined, }); - const tShortcut = shortcuts.dayAgendaShortcuts.find((s) => s.k === "t"); + const tShortcut = shortcuts.dayShortcuts.find((s) => s.k === "t"); expect(tShortcut).toBeDefined(); expect(tShortcut?.label).toBe("Go to today"); }); diff --git a/packages/web/src/common/utils/shortcut/data/shortcuts.data.ts b/packages/web/src/common/utils/shortcut/data/shortcuts.data.ts index f0186f384..ce1e6003a 100644 --- a/packages/web/src/common/utils/shortcut/data/shortcuts.data.ts +++ b/packages/web/src/common/utils/shortcut/data/shortcuts.data.ts @@ -22,6 +22,7 @@ export const getShortcuts = (config: ShortcutsConfig = {}) => { ]; let homeShortcuts: Shortcut[] = []; + let dayShortcuts: Shortcut[] = []; let dayTaskShortcuts: Shortcut[] = []; let dayAgendaShortcuts: Shortcut[] = []; let nowShortcuts: Shortcut[] = []; @@ -35,15 +36,9 @@ export const getShortcuts = (config: ShortcutsConfig = {}) => { } if (isToday) { - dayTaskShortcuts = [ - { k: "u", label: "Focus on tasks" }, - { k: "c", label: "Create task" }, - { k: "e", label: "Edit task" }, - { k: "Delete", label: "Delete task" }, - ]; - dayAgendaShortcuts = [ - { k: "i", label: "Focus on calendar" }, - { k: "c", label: "Create event" }, + dayShortcuts = [ + { k: "j", label: "Previous day" }, + { k: "k", label: "Next day" }, { k: "t", label: (() => { @@ -55,6 +50,17 @@ export const getShortcuts = (config: ShortcutsConfig = {}) => { })(), }, ]; + + dayTaskShortcuts = [ + { k: "u", label: "Focus on tasks" }, + { k: "c", label: "Create task" }, + { k: "e", label: "Edit task" }, + { k: "Delete", label: "Delete task" }, + ]; + dayAgendaShortcuts = [ + { k: "i", label: "Focus on calendar" }, + { k: "n", label: "Create event" }, + ]; } if (isNow) { nowShortcuts = [ @@ -70,6 +76,7 @@ export const getShortcuts = (config: ShortcutsConfig = {}) => { return { globalShortcuts, homeShortcuts, + dayShortcuts, dayTaskShortcuts, dayAgendaShortcuts, nowShortcuts, diff --git a/packages/web/src/views/Day/hooks/shortcuts/useDayViewShortcuts.test.ts b/packages/web/src/views/Day/hooks/shortcuts/useDayViewShortcuts.test.ts index d346b2906..45ef3b175 100644 --- a/packages/web/src/views/Day/hooks/shortcuts/useDayViewShortcuts.test.ts +++ b/packages/web/src/views/Day/hooks/shortcuts/useDayViewShortcuts.test.ts @@ -68,24 +68,20 @@ describe.each([ expect(config.onFocusTasks).toHaveBeenCalled(); }); - it("should call onCreateEvent when 'c' is pressed and not focused in task", async () => { + it("should call onCreateEvent when 'n' is pressed", async () => { const config = { ...defaultConfig }; await act(() => renderHook(() => useDayViewShortcuts(config))); - isFocusedWithinTask.mockReturnValue(false); - - pressKey("c"); + pressKey("n"); expect(config.onCreateEvent).toHaveBeenCalled(); expect(config.onAddTask).not.toHaveBeenCalled(); }); - it("should call onAddTask when 'c' is pressed and focused within task", async () => { + it("should call onAddTask when 'c' is pressed", async () => { const config = { ...defaultConfig }; await act(() => renderHook(() => useDayViewShortcuts(config))); - isFocusedWithinTask.mockReturnValue(true); - pressKey("c"); expect(config.onAddTask).toHaveBeenCalled(); diff --git a/packages/web/src/views/Day/hooks/shortcuts/useDayViewShortcuts.ts b/packages/web/src/views/Day/hooks/shortcuts/useDayViewShortcuts.ts index 4df1195aa..c3d3f8fe0 100644 --- a/packages/web/src/views/Day/hooks/shortcuts/useDayViewShortcuts.ts +++ b/packages/web/src/views/Day/hooks/shortcuts/useDayViewShortcuts.ts @@ -75,16 +75,6 @@ export function useDayViewShortcuts(config: KeyboardShortcutsConfig) { eventUndoToastId, } = config; - const handleCreateShortcut = useCallback(() => { - // If focused on task area, create a task - if (isFocusedWithinTask()) { - onAddTask?.(); - } else { - // Otherwise, create an event - onCreateEvent?.(); - } - }, [onAddTask, onCreateEvent]); - const handleDeleteTask = useCallback(() => { if (isFocusedOnTaskCheckbox()) { onDeleteTask?.(); @@ -134,10 +124,12 @@ export function useDayViewShortcuts(config: KeyboardShortcutsConfig) { // Tasks shortcuts useKeyUpEvent({ combination: ["u"], handler: onFocusTasks }); - useKeyUpEvent({ combination: ["c"], handler: handleCreateShortcut }); + useKeyUpEvent({ combination: ["c"], handler: onAddTask }); useKeyUpEvent({ combination: ["e"], handler: onEditTask }); + useKeyUpEvent({ combination: ["n"], handler: onCreateEvent }); + useKeyUpEvent({ combination: ["Delete"], handler: handleDeleteTask }); useKeyUpEvent({ combination: ["Backspace"], handler: handleDeleteTask }); diff --git a/packages/web/src/views/Day/view/DayView.test.tsx b/packages/web/src/views/Day/view/DayView.test.tsx index e15c5518a..865c9dd68 100644 --- a/packages/web/src/views/Day/view/DayView.test.tsx +++ b/packages/web/src/views/Day/view/DayView.test.tsx @@ -31,7 +31,7 @@ describe("DayView", () => { // Check that CMD+K shortcut is displayed in the shortcuts overlay expect(await screen.findByText("Global")).toBeInTheDocument(); expect(screen.getByTestId(getModifierKeyTestId())).toBeInTheDocument(); - expect(screen.getByTestId("k-icon")).toBeInTheDocument(); + expect(screen.getAllByTestId("k-icon").length).toBeGreaterThan(1); expect(screen.getByText("Command Palette")).toBeInTheDocument(); }); }); diff --git a/packages/web/src/views/Day/view/DayViewContent.test.tsx b/packages/web/src/views/Day/view/DayViewContent.test.tsx index ee884093d..1528449b7 100644 --- a/packages/web/src/views/Day/view/DayViewContent.test.tsx +++ b/packages/web/src/views/Day/view/DayViewContent.test.tsx @@ -63,7 +63,7 @@ describe("TodayViewContent", () => { ).toHaveLength(2); // Header SelectView + TaskList date selector }); - it("focuses the add task input when typing the 'c' shortcut while focused on tasks", async () => { + it("focuses the add task input when typing the 'c' shortcut", async () => { const actualShortcuts = jest.requireActual( "../hooks/shortcuts/useDayViewShortcuts", ); @@ -72,7 +72,6 @@ describe("TodayViewContent", () => { actualShortcuts.useDayViewShortcuts({ ...config, onFocusTasks: config.onFocusTasks || jest.fn(), - onCreateEvent: config.onCreateEvent || jest.fn(), }), ); @@ -80,28 +79,15 @@ describe("TodayViewContent", () => { renderWithDayProviders(), ); - // Add a task first so we have something to focus on - await addTasks(user, ["Test task"]); - - // First focus on a task checkbox - const taskCheckbox = await screen.findByRole("checkbox", { - name: /toggle/i, - }); - await act(async () => { - taskCheckbox.focus(); - }); - - // Now press 'c' while focused on a task - this should create a task await act(async () => { await user.keyboard("c"); }); - // Should have created a new task input - const taskInputs = await screen.findAllByRole("textbox", { + const addTaskInput = await screen.findByRole("textbox", { name: "Task title", }); - expect(taskInputs.length).toBeGreaterThan(0); + expect(addTaskInput).toHaveFocus(); }); it("should display today's date in the tasks section", async () => { diff --git a/packages/web/src/views/Day/view/DayViewContent.tsx b/packages/web/src/views/Day/view/DayViewContent.tsx index 172f5241b..efc47de8a 100644 --- a/packages/web/src/views/Day/view/DayViewContent.tsx +++ b/packages/web/src/views/Day/view/DayViewContent.tsx @@ -1,9 +1,6 @@ import { useCallback, useRef } from "react"; -import { Origin, Priorities } from "@core/constants/core.constants"; import dayjs from "@core/util/date/dayjs"; -import { getUserId } from "@web/auth/auth.util"; import { MousePositionProvider } from "@web/common/context/mouse-position"; -import { useMousePosition } from "@web/common/hooks/useMousePosition"; import { getShortcuts } from "@web/common/utils/shortcut/data/shortcuts.data"; import { FloatingEventForm } from "@web/components/FloatingEventForm/FloatingEventForm"; import { ShortcutsOverlay } from "@web/components/Shortcuts/ShortcutOverlay/ShortcutsOverlay"; @@ -114,69 +111,7 @@ const DayViewContentInner = () => { } }; - const { setDraft } = useDraftContextV2(); - const mousePosition = useMousePosition(); - const { setOpenAtMousePosition, floating } = mousePosition; - - const handleCreateEvent = useCallback(async () => { - try { - const user = await getUserId(); - if (!user) return; - - // Create a new event starting at the current hour - const now = dayjs(); - const startTime = dateInView - .hour(now.hour()) - .minute(0) - .second(0) - .millisecond(0); - const endTime = startTime.add(1, "hour"); - - const draftEvent = { - title: "", - description: "", - startDate: startTime.toISOString(), - endDate: endTime.toISOString(), - isAllDay: false, - isSomeday: false, - user, - priority: Priorities.UNASSIGNED, - origin: Origin.COMPASS, - }; - - // Get the center of the screen for positioning the form - const centerX = window.innerWidth / 2; - const centerY = window.innerHeight / 2; - - // Create a virtual reference point at the center of the screen - const virtualRef = { - getBoundingClientRect: () => ({ - width: 0, - height: 0, - x: centerX, - y: centerY, - top: centerY, - left: centerX, - right: centerX, - bottom: centerY, - toJSON: () => ({}), - }), - }; - - // Set the reference for the floating UI if available - if (floating?.refs?.setReference) { - floating.refs.setReference(virtualRef); - } - - // Set the draft and open the form at the mouse position - setDraft(draftEvent); - setOpenAtMousePosition(true); - } catch (error) { - // Silently fail if user authentication fails - // The user will already be redirected to login if not authenticated - console.error("Failed to create event:", error); - } - }, [dateInView, setDraft, setOpenAtMousePosition, floating]); + const { openEventForm } = useDraftContextV2(); useDayViewShortcuts({ onAddTask: focusOnAddTaskInput, @@ -186,7 +121,7 @@ const DayViewContentInner = () => { onMigrateTask: migrateTask, onFocusTasks: focusOnFirstTask, onFocusAgenda: handleFocusAgenda, - onCreateEvent: handleCreateEvent, + onCreateEvent: openEventForm, onNextDay: navigateToNextDay, onPrevDay: navigateToPreviousDay, onGoToToday: handleGoToToday, @@ -215,7 +150,7 @@ const DayViewContentInner = () => { Date: Mon, 8 Dec 2025 14:55:52 +0100 Subject: [PATCH 5/6] feat(day-view): enhance openEventForm to support event creation option and add tests for DraftProviderV2 --- .../Draft/context/DraftProviderV2.tsx | 2 +- .../__tests__/DraftProviderV2.test.tsx | 56 +++++ .../web/src/views/Day/view/DayViewContent.tsx | 6 +- .../hooks/__tests__/useOpenEventForm.test.ts | 208 ++++++++++++++++++ .../src/views/Forms/hooks/useOpenEventForm.ts | 121 +++++----- 5 files changed, 332 insertions(+), 61 deletions(-) create mode 100644 packages/web/src/views/Calendar/components/Draft/context/__tests__/DraftProviderV2.test.tsx create mode 100644 packages/web/src/views/Forms/hooks/__tests__/useOpenEventForm.test.ts diff --git a/packages/web/src/views/Calendar/components/Draft/context/DraftProviderV2.tsx b/packages/web/src/views/Calendar/components/Draft/context/DraftProviderV2.tsx index 5894efdf6..8f3cbb4ba 100644 --- a/packages/web/src/views/Calendar/components/Draft/context/DraftProviderV2.tsx +++ b/packages/web/src/views/Calendar/components/Draft/context/DraftProviderV2.tsx @@ -13,7 +13,7 @@ import { useSaveEventForm } from "@web/views/Forms/hooks/useSaveEventForm"; interface DraftProviderV2Props { draft: Schema_Event | null; setDraft: Dispatch>; - openEventForm: () => void; + openEventForm: (create?: boolean) => void; closeEventForm: () => void; onDelete: () => void; onSave: (draft: Schema_Event | null) => void; diff --git a/packages/web/src/views/Calendar/components/Draft/context/__tests__/DraftProviderV2.test.tsx b/packages/web/src/views/Calendar/components/Draft/context/__tests__/DraftProviderV2.test.tsx new file mode 100644 index 000000000..bff02bd70 --- /dev/null +++ b/packages/web/src/views/Calendar/components/Draft/context/__tests__/DraftProviderV2.test.tsx @@ -0,0 +1,56 @@ +import { useContext } from "react"; +import { render, screen } from "@testing-library/react"; +import { useCloseEventForm } from "@web/views/Forms/hooks/useCloseEventForm"; +import { useOpenEventForm } from "@web/views/Forms/hooks/useOpenEventForm"; +import { useSaveEventForm } from "@web/views/Forms/hooks/useSaveEventForm"; +import { DraftContextV2, DraftProviderV2 } from "../DraftProviderV2"; + +jest.mock("@web/views/Forms/hooks/useOpenEventForm"); +jest.mock("@web/views/Forms/hooks/useCloseEventForm"); +jest.mock("@web/views/Forms/hooks/useSaveEventForm"); + +const TestComponent = () => { + const context = useContext(DraftContextV2); + if (!context) return
No Context
; + return ( +
+
+ {context.draft ? "Draft Exists" : "No Draft"} +
+ + + +
+ ); +}; + +describe("DraftProviderV2", () => { + const mockOpenEventForm = jest.fn(); + const mockCloseEventForm = jest.fn(); + const mockOnSave = jest.fn(); + + beforeEach(() => { + (useOpenEventForm as jest.Mock).mockReturnValue(mockOpenEventForm); + (useCloseEventForm as jest.Mock).mockReturnValue(mockCloseEventForm); + (useSaveEventForm as jest.Mock).mockReturnValue(mockOnSave); + }); + + it("should provide draft context values", () => { + render( + + + , + ); + + expect(screen.getByTestId("draft")).toHaveTextContent("No Draft"); + + screen.getByText("Open").click(); + expect(mockOpenEventForm).toHaveBeenCalled(); + + screen.getByText("Close").click(); + expect(mockCloseEventForm).toHaveBeenCalled(); + + screen.getByText("Save").click(); + expect(mockOnSave).toHaveBeenCalled(); + }); +}); diff --git a/packages/web/src/views/Day/view/DayViewContent.tsx b/packages/web/src/views/Day/view/DayViewContent.tsx index efc47de8a..575ce17f3 100644 --- a/packages/web/src/views/Day/view/DayViewContent.tsx +++ b/packages/web/src/views/Day/view/DayViewContent.tsx @@ -113,6 +113,10 @@ const DayViewContentInner = () => { const { openEventForm } = useDraftContextV2(); + const onCreateEvent = useCallback(() => { + openEventForm(true); + }, [openEventForm]); + useDayViewShortcuts({ onAddTask: focusOnAddTaskInput, onEditTask: handleEditTask, @@ -121,7 +125,7 @@ const DayViewContentInner = () => { onMigrateTask: migrateTask, onFocusTasks: focusOnFirstTask, onFocusAgenda: handleFocusAgenda, - onCreateEvent: openEventForm, + onCreateEvent: onCreateEvent, onNextDay: navigateToNextDay, onPrevDay: navigateToPreviousDay, onGoToToday: handleGoToToday, diff --git a/packages/web/src/views/Forms/hooks/__tests__/useOpenEventForm.test.ts b/packages/web/src/views/Forms/hooks/__tests__/useOpenEventForm.test.ts new file mode 100644 index 000000000..bb9ca9ac2 --- /dev/null +++ b/packages/web/src/views/Forms/hooks/__tests__/useOpenEventForm.test.ts @@ -0,0 +1,208 @@ +import { act, renderHook } from "@testing-library/react"; +import { Origin, Priorities } from "@core/constants/core.constants"; +import dayjs from "@core/util/date/dayjs"; +import { getUserId } from "@web/auth/auth.util"; +import { + CLASS_TIMED_CALENDAR_EVENT, + DATA_EVENT_ELEMENT_ID, +} from "@web/common/constants/web.constants"; +import { useMousePosition } from "@web/common/hooks/useMousePosition"; +import { selectEventById } from "@web/ducks/events/selectors/event.selectors"; +import { SLOT_HEIGHT } from "@web/views/Day/constants/day.constants"; +import { useDateInView } from "@web/views/Day/hooks/navigation/useDateInView"; +import { getEventTimeFromPosition } from "@web/views/Day/util/agenda/agenda.util"; +import { useOpenEventForm } from "../useOpenEventForm"; + +jest.mock("@web/common/hooks/useMousePosition"); +jest.mock("@web/views/Day/hooks/navigation/useDateInView"); +jest.mock("@web/auth/auth.util"); +jest.mock("@web/ducks/events/selectors/event.selectors"); +jest.mock("@web/views/Day/util/agenda/agenda.util"); +jest.mock("@web/store", () => ({ + store: { + getState: jest.fn(), + }, +})); + +describe("useOpenEventForm", () => { + const mockSetDraft = jest.fn(); + const mockSetExisting = jest.fn(); + const mockSetOpenAtMousePosition = jest.fn(); + const mockSetReference = jest.fn(); + const mockDateInView = dayjs("2023-01-01T12:00:00.000Z"); + + beforeEach(() => { + jest.clearAllMocks(); + (useDateInView as jest.Mock).mockReturnValue(mockDateInView); + (getUserId as jest.Mock).mockResolvedValue("user-123"); + (useMousePosition as jest.Mock).mockReturnValue({ + element: null, + mousePointRef: { getBoundingClientRect: jest.fn(() => ({ top: 100 })) }, + floating: { refs: { setReference: mockSetReference } }, + setOpenAtMousePosition: mockSetOpenAtMousePosition, + isOverAllDayRow: false, + isOverMainGrid: false, + isOverSidebar: false, + isOverSomedayWeek: false, + isOverSomedayMonth: false, + }); + }); + + it("should not open form if user is not logged in", async () => { + (getUserId as jest.Mock).mockResolvedValue(null); + const { result } = renderHook(() => + useOpenEventForm({ + setDraft: mockSetDraft, + setExisting: mockSetExisting, + }), + ); + + await act(async () => { + await result.current(); + }); + + expect(mockSetDraft).not.toHaveBeenCalled(); + }); + + it("should create a new draft event when not hovering over existing event", async () => { + const { result } = renderHook(() => + useOpenEventForm({ + setDraft: mockSetDraft, + setExisting: mockSetExisting, + }), + ); + + await act(async () => { + await result.current(); + }); + + expect(mockSetExisting).toHaveBeenCalledWith(false); + expect(mockSetDraft).toHaveBeenCalledWith( + expect.objectContaining({ + title: "", + user: "user-123", + origin: Origin.COMPASS, + priority: Priorities.UNASSIGNED, + }), + ); + expect(mockSetOpenAtMousePosition).toHaveBeenCalledWith(true); + }); + + it("should create all-day event when hovering over all-day row", async () => { + (useMousePosition as jest.Mock).mockReturnValue({ + ...useMousePosition(), + isOverAllDayRow: true, + }); + + const { result } = renderHook(() => + useOpenEventForm({ + setDraft: mockSetDraft, + setExisting: mockSetExisting, + }), + ); + + await act(async () => { + await result.current(); + }); + + expect(mockSetDraft).toHaveBeenCalledWith( + expect.objectContaining({ + isAllDay: true, + startDate: mockDateInView.startOf("day").toISOString(), + endDate: mockDateInView.startOf("day").add(1, "day").toISOString(), + }), + ); + }); + + it("should create timed event when hovering over main grid", async () => { + (useMousePosition as jest.Mock).mockReturnValue({ + ...useMousePosition(), + isOverMainGrid: true, + mousePointRef: { getBoundingClientRect: () => ({ top: 100 }) }, + }); + (getEventTimeFromPosition as jest.Mock).mockImplementation((y) => { + if (y === 100) return mockDateInView.hour(10).minute(0); + if (y === 100 + SLOT_HEIGHT) return mockDateInView.hour(10).minute(15); + return mockDateInView; + }); + + const { result } = renderHook(() => + useOpenEventForm({ + setDraft: mockSetDraft, + setExisting: mockSetExisting, + }), + ); + + await act(async () => { + await result.current(); + }); + + expect(mockSetDraft).toHaveBeenCalledWith( + expect.objectContaining({ + isAllDay: false, + startDate: mockDateInView.hour(10).minute(0).toISOString(), + endDate: mockDateInView.hour(10).minute(15).toISOString(), + }), + ); + }); + + it("should open existing event when hovering over one", async () => { + const mockEventElement = document.createElement("div"); + mockEventElement.setAttribute(DATA_EVENT_ELEMENT_ID, "event-123"); + mockEventElement.classList.add(CLASS_TIMED_CALENDAR_EVENT); + + (useMousePosition as jest.Mock).mockReturnValue({ + ...useMousePosition(), + isOverMainGrid: true, + element: { closest: jest.fn().mockReturnValue(mockEventElement) }, + }); + const mockEvent = { _id: "event-123", title: "Existing Event" }; + (selectEventById as jest.Mock).mockReturnValue(mockEvent); + + const { result } = renderHook(() => + useOpenEventForm({ + setDraft: mockSetDraft, + setExisting: mockSetExisting, + }), + ); + + await act(async () => { + await result.current(); + }); + + expect(mockSetExisting).toHaveBeenCalledWith(true); + expect(mockSetDraft).toHaveBeenCalledWith(mockEvent); + expect(mockSetOpenAtMousePosition).toHaveBeenCalledWith(true); + }); + + it("should create new event even if hovering over existing event when create=true is passed", async () => { + const mockEventElement = document.createElement("div"); + mockEventElement.setAttribute(DATA_EVENT_ELEMENT_ID, "event-123"); + mockEventElement.classList.add(CLASS_TIMED_CALENDAR_EVENT); + + (useMousePosition as jest.Mock).mockReturnValue({ + ...useMousePosition(), + isOverMainGrid: true, + element: { closest: jest.fn().mockReturnValue(mockEventElement) }, + }); + + const { result } = renderHook(() => + useOpenEventForm({ + setDraft: mockSetDraft, + setExisting: mockSetExisting, + }), + ); + + await act(async () => { + await result.current(true); // Pass create=true + }); + + expect(mockSetExisting).toHaveBeenCalledWith(false); + expect(mockSetDraft).toHaveBeenCalledWith( + expect.objectContaining({ + title: "", + user: "user-123", + }), + ); + }); +}); diff --git a/packages/web/src/views/Forms/hooks/useOpenEventForm.ts b/packages/web/src/views/Forms/hooks/useOpenEventForm.ts index bf9238ce7..7e8c39457 100644 --- a/packages/web/src/views/Forms/hooks/useOpenEventForm.ts +++ b/packages/web/src/views/Forms/hooks/useOpenEventForm.ts @@ -48,74 +48,77 @@ export function useOpenEventForm({ } }, [isOverAllDayRow, isOverSomedayWeek, isOverSomedayMonth, isOverMainGrid]); - const openEventForm = useCallback(async () => { - const user = await getUserId(); + const openEventForm = useCallback( + async (create?: boolean) => { + const user = await getUserId(); - if (!user) return; + if (!user) return; - const event = element?.closest(`.${eventClass}`); - const existingEventId = event?.getAttribute(DATA_EVENT_ELEMENT_ID); + const event = element?.closest(`.${eventClass}`); + const existingEventId = event?.getAttribute(DATA_EVENT_ELEMENT_ID); - let draftEvent: Schema_Event; + let draftEvent: Schema_Event; - if (existingEventId) { - draftEvent = selectEventById(store.getState(), existingEventId); - setExisting(true); - } else { - let startTime: Dayjs = dayjs(); - let endTime: Dayjs = dayjs().add(15, "minutes"); + if (existingEventId && !create) { + draftEvent = selectEventById(store.getState(), existingEventId); + setExisting(true); + } else { + let startTime: Dayjs = dayjs(); + let endTime: Dayjs = dayjs().add(15, "minutes"); - if (isOverAllDayRow) { - const date = dateInView.startOf("day"); - startTime = date; - endTime = date.add(1, "day"); - } else if (isOverSomedayWeek || isOverSomedayMonth) { - const now = dayjs(); - const date = dateInView.hour(now.hour()).minute(now.minute()); - startTime = date; - endTime = date.add(15, "minutes"); - } else if (isOverMainGrid) { - const boundingRect = mousePointRef?.getBoundingClientRect(); - const startTimeY = boundingRect?.top ?? 0; - const endTimeY = startTimeY + SLOT_HEIGHT; - startTime = getEventTimeFromPosition(startTimeY, dateInView); - endTime = getEventTimeFromPosition(endTimeY, dateInView); - } + if (isOverAllDayRow) { + const date = dateInView.startOf("day"); + startTime = date; + endTime = date.add(1, "day"); + } else if (isOverSomedayWeek || isOverSomedayMonth) { + const now = dayjs(); + const date = dateInView.hour(now.hour()).minute(now.minute()); + startTime = date; + endTime = date.add(15, "minutes"); + } else if (isOverMainGrid) { + const boundingRect = mousePointRef?.getBoundingClientRect(); + const startTimeY = boundingRect?.top ?? 0; + const endTimeY = startTimeY + SLOT_HEIGHT; + startTime = getEventTimeFromPosition(startTimeY, dateInView); + endTime = getEventTimeFromPosition(endTimeY, dateInView); + } - draftEvent = { - title: "", - description: "", - startDate: startTime.toISOString(), - endDate: endTime.toISOString(), - isAllDay: isOverAllDayRow, - isSomeday: isOverSidebar, - user, - priority: Priorities.UNASSIGNED, - origin: Origin.COMPASS, - }; + draftEvent = { + title: "", + description: "", + startDate: startTime.toISOString(), + endDate: endTime.toISOString(), + isAllDay: isOverAllDayRow, + isSomeday: isOverSidebar, + user, + priority: Priorities.UNASSIGNED, + origin: Origin.COMPASS, + }; - setExisting(false); - } + setExisting(false); + } - setReference?.(mousePointRef); + setReference?.(mousePointRef); - setDraft(draftEvent); - setOpenAtMousePosition(true); - }, [ - element, - eventClass, - setReference, - mousePointRef, - setDraft, - setExisting, - setOpenAtMousePosition, - isOverAllDayRow, - isOverSomedayWeek, - isOverSomedayMonth, - isOverMainGrid, - isOverSidebar, - dateInView, - ]); + setDraft(draftEvent); + setOpenAtMousePosition(true); + }, + [ + element, + eventClass, + setReference, + mousePointRef, + setDraft, + setExisting, + setOpenAtMousePosition, + isOverAllDayRow, + isOverSomedayWeek, + isOverSomedayMonth, + isOverMainGrid, + isOverSidebar, + dateInView, + ], + ); return openEventForm; } From 1ec905fb2b31e48e66dfd279cfb95b7bba006964 Mon Sep 17 00:00:00 2001 From: victor-enogwe <23452630+victor-enogwe@users.noreply.github.com> Date: Mon, 8 Dec 2025 14:57:39 +0100 Subject: [PATCH 6/6] fix(tests): update import statement for renderHook in useOpenEventForm tests --- .../src/views/Forms/hooks/__tests__/useOpenEventForm.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/web/src/views/Forms/hooks/__tests__/useOpenEventForm.test.ts b/packages/web/src/views/Forms/hooks/__tests__/useOpenEventForm.test.ts index bb9ca9ac2..94a68f5d1 100644 --- a/packages/web/src/views/Forms/hooks/__tests__/useOpenEventForm.test.ts +++ b/packages/web/src/views/Forms/hooks/__tests__/useOpenEventForm.test.ts @@ -1,4 +1,5 @@ -import { act, renderHook } from "@testing-library/react"; +import { act } from "react"; +import { renderHook } from "@testing-library/react"; import { Origin, Priorities } from "@core/constants/core.constants"; import dayjs from "@core/util/date/dayjs"; import { getUserId } from "@web/auth/auth.util";