Skip to content

Commit ad64c52

Browse files
Copilotvictor-enogweCopilot
authored
:sparkles feat(agenda-event resize): support resizable agenda event preview
* Initial plan * Implement resizable event preview with top and bottom handles Co-authored-by: victor-enogwe <23452630+victor-enogwe@users.noreply.github.com> * Address code review feedback: improve resize logic and reduce duplication Co-authored-by: victor-enogwe <23452630+victor-enogwe@users.noreply.github.com> * :sparkles feat(agenda-event resize): support resizable agenda event preview :package deps(rxjs-state-management): introduce rxjs state @ngneat/elf library - Updated EventContextMenu and related components to utilize activeEvent$ observable for managing active events. - Replaced setDraft with resetDraft and resetActiveEvent in event form hooks to improve state management. - Refactored tests to mock new event store structure and ensure proper event handling. - Introduced utility functions for calculating event height and rounding minutes to nearest fifteen. - Updated yarn.lock to include new dependencies for @ngneat/elf-entities and @ngneat/use-observable. * :bugfix fix(resize-agenda-event): improve event handling and add tests * Update packages/web/src/common/hooks/useEventResizeActions.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * feat(resize-agenda-event): add bounds parameter to useEventResizeActions for improved resizing logic * refactor(useEventResizeActions): remove debug log from resizing function * feat(useEventResizeActions): enhance resizing logic with bounds clamping and update tests --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: victor-enogwe <23452630+victor-enogwe@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 7636dfa commit ad64c52

67 files changed

Lines changed: 2310 additions & 473 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/web/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
"@dnd-kit/utilities": "^3.2.2",
1414
"@floating-ui/react": "^0.27.3",
1515
"@hello-pangea/dnd": "^16.2.0",
16+
"@ngneat/elf": "^2.5.1",
17+
"@ngneat/elf-entities": "^5.0.2",
18+
"@ngneat/use-observable": "^1.0.0",
1619
"@phosphor-icons/react": "^2.1.7",
1720
"@react-oauth/google": "^0.7.0",
1821
"@reduxjs/toolkit": "^1.6.1",

packages/web/src/__tests__/utils/state/store.test.util.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { PreloadedState, configureStore } from "@reduxjs/toolkit";
22
import { Schema_Event } from "@core/types/event.types";
3+
import { sagaMiddleware } from "@web/common/store/middlewares";
34
import { RootState } from "@web/store";
45
import { reducers } from "@web/store/reducers";
56

@@ -157,7 +158,7 @@ export const createStoreWithEvents = (
157158
return acc;
158159
}, {});
159160

160-
preloadedState.events.entities.value = entities;
161+
preloadedState.events.entities!.value = entities;
161162
preloadedState.events.getDayEvents = {
162163
value: {
163164
data: events
@@ -182,7 +183,7 @@ export const createStoreWithEvents = (
182183
thunk: false,
183184
serializableCheck: false,
184185
immutableCheck: false,
185-
}),
186+
}).concat(sagaMiddleware),
186187
});
187188
};
188189

packages/web/src/common/constants/web.constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export const DATA_EVENT_ELEMENT_ID = "data-event-id";
2424
export const DATA_DRAFT_EVENT = "data-draft-event";
2525
export const DATA_NEW_DRAFT_EVENT = "data-new-draft-event";
2626
export const DATA_TASK_ELEMENT_ID = "data-task-id";
27+
export const DATA_OVERLAPPING = "data-overlapping";
28+
export const DATA_FULL_WIDTH = "data-full-width";
2729
export const ID_CONTEXT_MENU_ITEMS = "context-menu-items";
2830
export const ID_ADD_TASK_BUTTON = "add-task-button";
2931
export const CLASS_ALL_DAY_CALENDAR_EVENT = "all-day-calendar-event";
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { act } from "react";
2+
import { renderHook } from "@testing-library/react";
3+
import { cursor$ } from "@web/common/context/mouse-position";
4+
import { useCursorCoordinates } from "./useCursorCoordinates";
5+
6+
describe("useCursorCoordinates", () => {
7+
beforeEach(() => {
8+
// Reset to a known state
9+
act(() => {
10+
cursor$.next({ x: 0, y: 0 });
11+
});
12+
});
13+
14+
it("should return the initial cursor coordinates", () => {
15+
const { result } = renderHook(() => useCursorCoordinates());
16+
expect(result.current).toEqual({ x: 0, y: 0 });
17+
});
18+
19+
it("should update coordinates when cursor$ emits new values", () => {
20+
const { result } = renderHook(() => useCursorCoordinates());
21+
22+
act(() => {
23+
cursor$.next({ x: 100, y: 200 });
24+
});
25+
26+
expect(result.current).toEqual({ x: 100, y: 200 });
27+
28+
act(() => {
29+
cursor$.next({ x: 50, y: 50 });
30+
});
31+
32+
expect(result.current).toEqual({ x: 50, y: 50 });
33+
});
34+
});

packages/web/src/common/hooks/__tests__/useEventDNDActions.test.ts renamed to packages/web/src/common/hooks/useEventDNDActions.test.ts

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,19 @@ import {
66
ID_GRID_ALLDAY_ROW,
77
ID_GRID_MAIN,
88
} from "@web/common/constants/web.constants";
9-
import { editEventSlice } from "@web/ducks/events/slices/event.slice";
9+
import { useEventDNDActions } from "@web/common/hooks/useEventDNDActions";
10+
import { useUpdateEvent } from "@web/common/hooks/useUpdateEvent";
1011
import { useAppDispatch } from "@web/store/store.hooks";
1112
import { getSnappedMinutes } from "@web/views/Day/util/agenda/agenda.util";
12-
import { useEventDNDActions } from "../useEventDNDActions";
1313

1414
jest.mock("@dnd-kit/core", () => ({
1515
useDndMonitor: jest.fn(),
1616
}));
1717

18+
jest.mock("@web/common/hooks/useUpdateEvent", () => ({
19+
useUpdateEvent: jest.fn(),
20+
}));
21+
1822
jest.mock("@web/store/store.hooks", () => ({
1923
useAppDispatch: jest.fn(),
2024
}));
@@ -25,6 +29,7 @@ jest.mock("@web/views/Day/util/agenda/agenda.util", () => ({
2529

2630
describe("useEventDNDActions", () => {
2731
const mockDispatch = jest.fn();
32+
const mockUpdateEvent = jest.fn();
2833
const mockEvent = {
2934
_id: "event-1",
3035
startDate: "2023-01-01T10:00:00.000Z",
@@ -35,6 +40,7 @@ describe("useEventDNDActions", () => {
3540
beforeEach(() => {
3641
jest.clearAllMocks();
3742
(useAppDispatch as jest.Mock).mockReturnValue(mockDispatch);
43+
(useUpdateEvent as jest.Mock).mockReturnValue(mockUpdateEvent);
3844
});
3945

4046
it("should register dnd monitor", () => {
@@ -79,14 +85,15 @@ describe("useEventDNDActions", () => {
7985
.add(60, "minute")
8086
.toISOString();
8187

82-
expect(mockDispatch).toHaveBeenCalledWith(
83-
editEventSlice.actions.request({
84-
_id: mockEvent._id,
88+
expect(mockUpdateEvent).toHaveBeenCalledWith(
89+
{
8590
event: expect.objectContaining({
91+
...mockEvent,
8692
startDate: expectedStartDate,
8793
endDate: expectedEndDate,
8894
}),
89-
}),
95+
},
96+
true,
9097
);
9198
});
9299

@@ -115,15 +122,16 @@ describe("useEventDNDActions", () => {
115122
.add(15, "minute")
116123
.toISOString();
117124

118-
expect(mockDispatch).toHaveBeenCalledWith(
119-
editEventSlice.actions.request({
120-
_id: allDayEvent._id,
125+
expect(mockUpdateEvent).toHaveBeenCalledWith(
126+
{
121127
event: expect.objectContaining({
128+
...allDayEvent,
122129
isAllDay: false,
123130
startDate: expectedStartDate,
124131
endDate: expectedEndDate,
125132
}),
126-
}),
133+
},
134+
true,
127135
);
128136
});
129137

@@ -149,15 +157,16 @@ describe("useEventDNDActions", () => {
149157
.add(1, "day")
150158
.format(dayjs.DateFormat.YEAR_MONTH_DAY_FORMAT);
151159

152-
expect(mockDispatch).toHaveBeenCalledWith(
153-
editEventSlice.actions.request({
154-
_id: mockEvent._id,
160+
expect(mockUpdateEvent).toHaveBeenCalledWith(
161+
{
155162
event: expect.objectContaining({
163+
...mockEvent,
156164
isAllDay: true,
157165
startDate: expectedStartDate,
158166
endDate: expectedEndDate,
159167
}),
160-
}),
168+
},
169+
true,
161170
);
162171
});
163172

@@ -167,7 +176,7 @@ describe("useEventDNDActions", () => {
167176

168177
onDragEnd({ active, over });
169178

170-
expect(mockDispatch).not.toHaveBeenCalled();
179+
expect(mockUpdateEvent).not.toHaveBeenCalled();
171180
});
172181
});
173182
});

packages/web/src/common/hooks/useEventDNDActions.ts

Lines changed: 65 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,30 @@ import {
66
ID_GRID_ALLDAY_ROW,
77
ID_GRID_MAIN,
88
} from "@web/common/constants/web.constants";
9+
import {
10+
CursorItem,
11+
isOpenAtCursor,
12+
setFloatingReferenceAtCursor,
13+
} from "@web/common/hooks/useOpenAtCursor";
14+
import { useUpdateEvent } from "@web/common/hooks/useUpdateEvent";
915
import { Schema_GridEvent } from "@web/common/types/web.event.types";
10-
import { editEventSlice } from "@web/ducks/events/slices/event.slice";
11-
import { useAppDispatch } from "@web/store/store.hooks";
16+
import { reorderGrid } from "@web/common/utils/dom/grid-organization.util";
17+
import { getCalendarEventElementFromGrid } from "@web/common/utils/event/event.util";
1218
import { getSnappedMinutes } from "@web/views/Day/util/agenda/agenda.util";
1319

14-
export function useEventDNDActions() {
15-
const dispatch = useAppDispatch();
20+
const shouldSaveImmediately = () => !isOpenAtCursor(CursorItem.EventForm);
1621

17-
const updateEvent = useCallback(
18-
(event: Schema_GridEvent) => {
19-
if (!event._id) return;
22+
const resetFloatingReference = (eventId: string) => {
23+
queueMicrotask(() => {
24+
const reference = getCalendarEventElementFromGrid(eventId);
2025

21-
dispatch(editEventSlice.actions.request({ _id: event._id, event }));
22-
},
23-
[dispatch],
24-
);
26+
setFloatingReferenceAtCursor(reference);
27+
reorderGrid();
28+
});
29+
};
30+
31+
export function useEventDNDActions() {
32+
const updateEvent = useUpdateEvent();
2533

2634
const moveTimedAroundMainGridDayView = useCallback(
2735
(event: Schema_GridEvent, active: Active, over: Over) => {
@@ -32,14 +40,26 @@ export function useEventDNDActions() {
3240
const start = dayjs(event.startDate);
3341
const end = dayjs(event.endDate);
3442
const durationMinutes = end.diff(start, "minute");
35-
const startDate = start.startOf("day").add(snappedMinutes, "minute");
36-
const newEndDate = startDate.add(durationMinutes, "minute");
37-
38-
updateEvent({
39-
...event,
40-
startDate: startDate.toISOString(),
41-
endDate: newEndDate.toISOString(),
42-
});
43+
const newStartDate = start.startOf("day").add(snappedMinutes, "minute");
44+
const newEndDate = newStartDate.add(durationMinutes, "minute");
45+
46+
const startChanged = !newStartDate.isSame(start);
47+
const endChanged = !newEndDate.isSame(end);
48+
49+
if (!startChanged && !endChanged) return;
50+
51+
updateEvent(
52+
{
53+
event: {
54+
...event,
55+
startDate: newStartDate.toISOString(),
56+
endDate: newEndDate.toISOString(),
57+
},
58+
},
59+
shouldSaveImmediately(),
60+
);
61+
62+
resetFloatingReference(event._id);
4363
},
4464
[updateEvent],
4565
);
@@ -54,12 +74,19 @@ export function useEventDNDActions() {
5474
const startDate = start.add(snappedMinutes, "minute");
5575
const endDate = startDate.add(15, "minutes"); // Default 15 mins
5676

57-
updateEvent({
58-
...event,
59-
isAllDay: false,
60-
startDate: startDate.toISOString(),
61-
endDate: endDate.toISOString(),
62-
});
77+
updateEvent(
78+
{
79+
event: {
80+
...event,
81+
isAllDay: false,
82+
startDate: startDate.toISOString(),
83+
endDate: endDate.toISOString(),
84+
},
85+
},
86+
shouldSaveImmediately(),
87+
);
88+
89+
resetFloatingReference(event._id);
6390
},
6491
[updateEvent],
6592
);
@@ -70,12 +97,19 @@ export function useEventDNDActions() {
7097
const startDate = dayjs(event.startDate).startOf("day");
7198
const endDate = dayjs(event.endDate).startOf("day").add(1, "day");
7299

73-
updateEvent({
74-
...event,
75-
isAllDay: true,
76-
startDate: startDate.format(dayjs.DateFormat.YEAR_MONTH_DAY_FORMAT),
77-
endDate: endDate.format(dayjs.DateFormat.YEAR_MONTH_DAY_FORMAT),
78-
});
100+
updateEvent(
101+
{
102+
event: {
103+
...event,
104+
isAllDay: true,
105+
startDate: startDate.format(dayjs.DateFormat.YEAR_MONTH_DAY_FORMAT),
106+
endDate: endDate.format(dayjs.DateFormat.YEAR_MONTH_DAY_FORMAT),
107+
},
108+
},
109+
shouldSaveImmediately(),
110+
);
111+
112+
resetFloatingReference(event._id);
79113
},
80114
[updateEvent],
81115
);

0 commit comments

Comments
 (0)