Skip to content

Commit ba0314d

Browse files
committed
: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.
1 parent a4ac404 commit ba0314d

File tree

58 files changed

+1187
-486
lines changed

Some content is hidden

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

58 files changed

+1187
-486
lines changed

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";

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

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,20 @@ 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 { useUpdateEvent } from "@web/common/hooks/useUpdateEvent";
10+
import { getEventFromStore } from "@web/common/utils/event/event.util";
1011
import { useAppDispatch } from "@web/store/store.hooks";
1112
import { getSnappedMinutes } from "@web/views/Day/util/agenda/agenda.util";
12-
import { useEventDNDActions } from "../useEventDNDActions";
13+
import { useEventDNDActions } from "./useEventDNDActions";
1314

1415
jest.mock("@dnd-kit/core", () => ({
1516
useDndMonitor: jest.fn(),
1617
}));
1718

19+
jest.mock("@web/common/hooks/useUpdateEvent", () => ({
20+
useUpdateEvent: jest.fn(),
21+
}));
22+
1823
jest.mock("@web/store/store.hooks", () => ({
1924
useAppDispatch: jest.fn(),
2025
}));
@@ -23,8 +28,13 @@ jest.mock("@web/views/Day/util/agenda/agenda.util", () => ({
2328
getSnappedMinutes: jest.fn(),
2429
}));
2530

31+
jest.mock("@web/common/utils/event/event.util", () => ({
32+
getEventFromStore: jest.fn(),
33+
}));
34+
2635
describe("useEventDNDActions", () => {
2736
const mockDispatch = jest.fn();
37+
const mockUpdateEvent = jest.fn();
2838
const mockEvent = {
2939
_id: "event-1",
3040
startDate: "2023-01-01T10:00:00.000Z",
@@ -35,6 +45,8 @@ describe("useEventDNDActions", () => {
3545
beforeEach(() => {
3646
jest.clearAllMocks();
3747
(useAppDispatch as jest.Mock).mockReturnValue(mockDispatch);
48+
(getEventFromStore as jest.Mock).mockReturnValue(mockEvent);
49+
(useUpdateEvent as jest.Mock).mockReturnValue(mockUpdateEvent);
3850
});
3951

4052
it("should register dnd monitor", () => {
@@ -79,14 +91,15 @@ describe("useEventDNDActions", () => {
7991
.add(60, "minute")
8092
.toISOString();
8193

82-
expect(mockDispatch).toHaveBeenCalledWith(
83-
editEventSlice.actions.request({
84-
_id: mockEvent._id,
94+
expect(mockUpdateEvent).toHaveBeenCalledWith(
95+
{
8596
event: expect.objectContaining({
97+
...mockEvent,
8698
startDate: expectedStartDate,
8799
endDate: expectedEndDate,
88100
}),
89-
}),
101+
},
102+
true,
90103
);
91104
});
92105

@@ -115,15 +128,16 @@ describe("useEventDNDActions", () => {
115128
.add(15, "minute")
116129
.toISOString();
117130

118-
expect(mockDispatch).toHaveBeenCalledWith(
119-
editEventSlice.actions.request({
120-
_id: allDayEvent._id,
131+
expect(mockUpdateEvent).toHaveBeenCalledWith(
132+
{
121133
event: expect.objectContaining({
134+
...allDayEvent,
122135
isAllDay: false,
123136
startDate: expectedStartDate,
124137
endDate: expectedEndDate,
125138
}),
126-
}),
139+
},
140+
true,
127141
);
128142
});
129143

@@ -149,15 +163,16 @@ describe("useEventDNDActions", () => {
149163
.add(1, "day")
150164
.format(dayjs.DateFormat.YEAR_MONTH_DAY_FORMAT);
151165

152-
expect(mockDispatch).toHaveBeenCalledWith(
153-
editEventSlice.actions.request({
154-
_id: mockEvent._id,
166+
expect(mockUpdateEvent).toHaveBeenCalledWith(
167+
{
155168
event: expect.objectContaining({
169+
...mockEvent,
156170
isAllDay: true,
157171
startDate: expectedStartDate,
158172
endDate: expectedEndDate,
159173
}),
160-
}),
174+
},
175+
true,
161176
);
162177
});
163178

@@ -167,7 +182,7 @@ describe("useEventDNDActions", () => {
167182

168183
onDragEnd({ active, over });
169184

170-
expect(mockDispatch).not.toHaveBeenCalled();
185+
expect(mockUpdateEvent).not.toHaveBeenCalled();
171186
});
172187
});
173188
});

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

Lines changed: 49 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,21 @@ import {
66
ID_GRID_ALLDAY_ROW,
77
ID_GRID_MAIN,
88
} from "@web/common/constants/web.constants";
9+
import { CursorItem, nodeId$, open$ } from "@web/common/hooks/useOpenAtCursor";
10+
import { useUpdateEvent } from "@web/common/hooks/useUpdateEvent";
911
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";
1212
import { getSnappedMinutes } from "@web/views/Day/util/agenda/agenda.util";
1313

1414
export function useEventDNDActions() {
15-
const dispatch = useAppDispatch();
15+
const updateEvent = useUpdateEvent();
1616

17-
const updateEvent = useCallback(
18-
(event: Schema_GridEvent) => {
19-
if (!event._id) return;
17+
const saveImmediate = () => {
18+
const eventFormOpen = nodeId$.getValue() === CursorItem.EventForm;
19+
const openAtCursor = open$.getValue();
20+
const saveDraftOnly = eventFormOpen && openAtCursor;
2021

21-
dispatch(editEventSlice.actions.request({ _id: event._id, event }));
22-
},
23-
[dispatch],
24-
);
22+
return !saveDraftOnly;
23+
};
2524

2625
const moveTimedAroundMainGridDayView = useCallback(
2726
(event: Schema_GridEvent, active: Active, over: Over) => {
@@ -32,14 +31,24 @@ export function useEventDNDActions() {
3231
const start = dayjs(event.startDate);
3332
const end = dayjs(event.endDate);
3433
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-
});
34+
const newStartDate = start.startOf("day").add(snappedMinutes, "minute");
35+
const newEndDate = newStartDate.add(durationMinutes, "minute");
36+
37+
const startChanged = !newStartDate.isSame(start);
38+
const endChanged = !newEndDate.isSame(end);
39+
40+
if (!startChanged && !endChanged) return;
41+
42+
updateEvent(
43+
{
44+
event: {
45+
...event,
46+
startDate: newStartDate.toISOString(),
47+
endDate: newEndDate.toISOString(),
48+
},
49+
},
50+
saveImmediate(),
51+
);
4352
},
4453
[updateEvent],
4554
);
@@ -54,12 +63,17 @@ export function useEventDNDActions() {
5463
const startDate = start.add(snappedMinutes, "minute");
5564
const endDate = startDate.add(15, "minutes"); // Default 15 mins
5665

57-
updateEvent({
58-
...event,
59-
isAllDay: false,
60-
startDate: startDate.toISOString(),
61-
endDate: endDate.toISOString(),
62-
});
66+
updateEvent(
67+
{
68+
event: {
69+
...event,
70+
isAllDay: false,
71+
startDate: startDate.toISOString(),
72+
endDate: endDate.toISOString(),
73+
},
74+
},
75+
saveImmediate(),
76+
);
6377
},
6478
[updateEvent],
6579
);
@@ -70,12 +84,17 @@ export function useEventDNDActions() {
7084
const startDate = dayjs(event.startDate).startOf("day");
7185
const endDate = dayjs(event.endDate).startOf("day").add(1, "day");
7286

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-
});
87+
updateEvent(
88+
{
89+
event: {
90+
...event,
91+
isAllDay: true,
92+
startDate: startDate.format(dayjs.DateFormat.YEAR_MONTH_DAY_FORMAT),
93+
endDate: endDate.format(dayjs.DateFormat.YEAR_MONTH_DAY_FORMAT),
94+
},
95+
},
96+
saveImmediate(),
97+
);
7998
},
8099
[updateEvent],
81100
);
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { ResizeCallback, ResizeStartCallback } from "re-resizable";
2+
import { useCallback, useRef, useState } from "react";
3+
import { Schema_Event, WithCompassId } from "@core/types/event.types";
4+
import dayjs from "@core/util/date/dayjs";
5+
import { CursorItem, nodeId$, open$ } from "@web/common/hooks/useOpenAtCursor";
6+
import { useUpdateEvent } from "@web/common/hooks/useUpdateEvent";
7+
import { Schema_GridEvent } from "@web/common/types/web.event.types";
8+
import { setDraft } from "@web/store/events";
9+
import {
10+
MINUTES_PER_SLOT,
11+
SLOT_HEIGHT,
12+
} from "@web/views/Day/constants/day.constants";
13+
import {
14+
roundMinutesToNearestFifteen,
15+
roundToNearestFifteenWithinHour,
16+
} from "@web/views/Day/util/agenda/agenda.util";
17+
18+
export function useEventResizeActions(event: WithCompassId<Schema_Event>) {
19+
const updateReduxEvent = useUpdateEvent();
20+
const [resizing, setResizing] = useState<boolean>(false);
21+
const originalEvent = useRef<WithCompassId<Schema_Event> | null>(event);
22+
const _id = event._id;
23+
24+
const onResizeStart: ResizeStartCallback = useCallback(() => {
25+
setResizing(true);
26+
setDraft(event);
27+
originalEvent.current = event;
28+
}, [setResizing, event]);
29+
30+
const onResize: ResizeCallback = useCallback(
31+
(_e, direction, _ref, delta) => {
32+
const slotMinute = MINUTES_PER_SLOT / SLOT_HEIGHT;
33+
const minutes = roundMinutesToNearestFifteen(delta.height * slotMinute);
34+
const start = dayjs(originalEvent.current?.startDate);
35+
const end = dayjs(originalEvent.current?.endDate);
36+
37+
if (direction === "top") {
38+
setDraft({
39+
_id,
40+
...originalEvent.current,
41+
startDate: start.subtract(minutes, "minutes").format(),
42+
});
43+
} else {
44+
setDraft({
45+
_id,
46+
...originalEvent.current,
47+
endDate: end.add(minutes, "minutes").format(),
48+
});
49+
}
50+
},
51+
[_id],
52+
);
53+
54+
const onResizeStop: ResizeCallback = useCallback(() => {
55+
setResizing(false);
56+
originalEvent.current = null;
57+
58+
const start = dayjs(event.startDate);
59+
const end = dayjs(event.endDate);
60+
const snappedStartMinute = roundToNearestFifteenWithinHour(start.minute());
61+
const snappedEndMinute = roundToNearestFifteenWithinHour(end.minute());
62+
63+
const snappedStart = start.minute(snappedStartMinute).second(0);
64+
const snappedEnd = end.minute(snappedEndMinute).second(0);
65+
66+
const eventFormOpen = nodeId$.getValue() === CursorItem.EventForm;
67+
const openAtCursor = open$.getValue();
68+
const saveDraftOnly = eventFormOpen && openAtCursor;
69+
70+
setDraft({
71+
...event,
72+
startDate: snappedStart.format(),
73+
endDate: snappedEnd.format(),
74+
});
75+
76+
if (saveDraftOnly) return;
77+
78+
updateReduxEvent({
79+
event: {
80+
...event,
81+
startDate: snappedStart.format(),
82+
endDate: snappedEnd.format(),
83+
} as Schema_GridEvent,
84+
});
85+
}, [event, updateReduxEvent]);
86+
87+
return { onResizeStart, onResize, onResizeStop, resizing };
88+
}

0 commit comments

Comments
 (0)