Skip to content

Commit 7419f4c

Browse files
tyler-daneCopilot
andauthored
refactor(web): simplify optimistic rendering (#1399)
* feat(event): introduce WithId type and addId utility function - Added a new type `WithId` to enhance event objects with a unique `_id` property. - Implemented the `addId` function to generate and attach a MongoDB ObjectId to events, marking them as optimistic. - Updated relevant tests to validate the functionality of the new `addId` method and ensure correct event handling during optimistic rendering. * feat(event): implement pending event handling and cursor updates - Introduced a new `pendingEventsSlice` to manage events in a pending state, allowing for better user experience during event creation and updates. - Updated cursor styles and classes in various components to reflect the pending state, enhancing visual feedback for users. - Refactored event utility functions to replace optimistic checks with pending state checks, ensuring consistent behavior across the application. - Enhanced context menu and event components to prevent interactions when events are pending, improving usability and preventing errors during event processing. * refactor(event): replace optimistic event handling with pending state checks - Removed optimistic ID handling from event types and utility functions, streamlining event management. - Updated event components and utilities to utilize a new `pendingEventsSlice`, enhancing user experience during event creation and updates. - Refactored tests to ensure correct behavior when handling pending events, improving overall reliability. - Adjusted context menus and event interactions to prevent actions on pending events, reducing potential errors. * refactor(event): update pending event handling and improve state management - Changed the data structure for pending event IDs from a Set to an array for better compatibility with existing methods. - Updated event handling logic across various components to utilize the new array-based pending event IDs, ensuring consistent behavior. - Enhanced the user experience by preventing interactions with events that are in a pending state, reducing potential errors during event processing. - Refactored related tests to align with the new pending event management approach, improving overall reliability. * test(event): add unit tests for pending event handling and selectors - Introduced comprehensive tests for the new pending event management, including sagas and selectors. - Verified that events are correctly added to and removed from the pending state during creation and editing processes. - Ensured selectors accurately reflect the pending state of events, improving overall reliability and maintainability of the event handling logic. * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * test(SomedayEventForm): enhance hotkey functionality tests - Updated tests for SomedayEventForm to include new hotkey functionalities. - Mocked API calls for create, edit, and delete operations to ensure isolated testing. - Added isDraft and isExistingEvent props to test scenarios, improving coverage for event states. - Refactored test cases to use 'it' instead of 'test' for consistency and clarity. * test(draft): enhance setupDraftState for improved store handling - Updated setupDraftState function to accept an optional store parameter, allowing for better test isolation and flexibility. - Introduced dispatchSpy option to monitor dispatch calls during tests. - Refactored related tests in SomedayEventForm to utilize the new setupDraftState functionality, improving test coverage and reliability. * test(draft): simplify setupDraftState and improve test integration - Refactored setupDraftState to remove the optional store parameter, streamlining the function for better clarity and usage. - Updated related tests in SomedayEventForm to align with the simplified setupDraftState, enhancing test reliability and maintainability. - Introduced dispatchSpy directly in the test setup for improved monitoring of dispatch calls during tests. * test(context-menu): add tests for ContextMenuItems and enhance pending event handling - Introduced unit tests for ContextMenuItems to verify rendering and interaction behaviors for both pending and non-pending events. - Updated ContextMenuItems component to disable actions (edit, duplicate, delete) for pending events, improving user experience and preventing errors. - Enhanced GridEvent and AllDayEvents components to utilize pending event checks, ensuring consistent behavior across the application. - Added new tests for GridEvent and AllDayEvents to validate rendering and interaction with pending events. * test(context-menu): refactor tests for ContextMenuItems to use centralized state creation - Introduced a new utility function, createStateWithPendingEvents, to streamline the setup of test states for pending events. - Updated existing tests for ContextMenuItems to utilize the new state creation function, improving code clarity and maintainability. - Removed redundant initial state definitions, enhancing the overall readability of the test cases. * test(agenda-events): enhance tests for draggable all-day and timed agenda events - Added tests to verify dragging behavior for events based on their pending state, ensuring that dragging is disabled for pending events and enabled for non-pending events. - Updated type assertions for interactions to improve type safety in tests. - Refactored initial state setup in tests to utilize a centralized state creation utility, enhancing clarity and maintainability. * test(agenda-events): add isDisabled prop to draggable agenda events - Introduced an `isDisabled` prop to the DraggableAllDayAgendaEvent and DraggableTimedAgendaEvent components to manage event interaction states. - Updated tests to verify the behavior of draggable events based on the new `isDisabled` prop, ensuring accurate rendering and interaction handling. - Removed redundant initial state setups in tests, enhancing clarity and maintainability. * refactor(agenda-events): update AllDayAgendaEvents and TimedAgendaEvents components to utilize pending event IDs - Refactored AllDayAgendaEvents and TimedAgendaEvents components to include the `isDisabled` prop for event interactions based on pending event IDs. - Integrated `selectPendingEventIds` selector to manage the disabled state of draggable events, enhancing user experience by preventing interactions with pending events. - Improved code clarity and maintainability by consolidating event handling logic across both components. * test(DND): add test for Draggable component's disabled prop behavior - Introduced a new test to verify the Draggable component's handling of the disabled prop, ensuring it updates correctly on rerender. - Mocked the useDraggable hook to check calls with both true and false values for the disabled state, enhancing test coverage for dynamic prop changes. - Removed unnecessary state management from the Draggable component, simplifying its implementation and improving clarity. * refactor(events): reorganize event slice utilities and update imports - Moved event-related utility functions (insert, cancel, convert) from someday.slice.ts to a new file event.slice.util.ts for better modularity and clarity. - Updated imports in day.slice.ts and week.slice.ts to reference the new utility file, ensuring consistent usage across the event slices. - Improved code organization by consolidating event handling logic into a dedicated utility module. * refactor(context-menu): streamline action disabling logic in ContextMenuItems - Introduced a helper function to determine if actions (edit, duplicate, delete) should be disabled based on the pending state. - Simplified the rendering logic for menu items by utilizing the new helper function, improving code clarity and maintainability. - Enhanced user experience by ensuring that disabled actions are visually indicated and non-interactive during pending states. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 86d6c53 commit 7419f4c

Some content is hidden

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

43 files changed

+2098
-454
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ export const createInitialState = (
119119
dateToResize: null,
120120
},
121121
},
122+
pendingEvents: {
123+
eventIds: [],
124+
},
122125
},
123126
view: {
124127
dates: {

packages/web/src/common/types/web.event.types.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import { ObjectId } from "bson";
21
import { z } from "zod";
3-
import { ID_OPTIMISTIC_PREFIX } from "@core/constants/core.constants";
42
import {
53
CompassCoreEventSchema,
64
CompassEventRecurrence,
@@ -9,22 +7,14 @@ import {
97
import { IDSchema } from "@core/types/type.utils";
108
import { SelectOption } from "@web/common/types/component.types";
119

12-
export const optimisticIdSchema = z
13-
.string()
14-
.refine(
15-
(id) =>
16-
id.startsWith(`${ID_OPTIMISTIC_PREFIX}-`) &&
17-
ObjectId.isValid(id.replace(`${ID_OPTIMISTIC_PREFIX}-`, "")),
18-
);
19-
2010
const WebEventRecurrence = z.union([
2111
z.undefined(),
2212
CompassEventRecurrence.omit({ rule: true }).extend({ rule: z.null() }),
2313
CompassEventRecurrence,
2414
]);
2515

2616
const WebCoreEventSchema = CompassCoreEventSchema.extend({
27-
_id: z.union([IDSchema, optimisticIdSchema]).optional(),
17+
_id: IDSchema.optional(),
2818
recurrence: WebEventRecurrence,
2919
order: z.number().optional(),
3020
});
@@ -79,3 +69,9 @@ export interface Schema_SomedayEventsColumn {
7969
[key: string]: Schema_Event;
8070
};
8171
}
72+
73+
/**
74+
* Adds an _id property to an object shape
75+
* @template TSchema - The base type to add _id to.
76+
*/
77+
export type WithId<TSchema> = TSchema & { _id: string };

packages/web/src/common/utils/event/event.util.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
Schema_GridEvent,
44
Schema_WebEvent,
55
} from "@web/common/types/web.event.types";
6-
import { isEventInRange } from "@web/common/utils/event/event.util";
6+
import { addId, isEventInRange } from "@web/common/utils/event/event.util";
77
import { _assembleGridEvent } from "@web/ducks/events/sagas/saga.util";
88

99
describe("isEventInRange", () => {
@@ -53,3 +53,14 @@ describe("_assembleGridEvent", () => {
5353
expect(result.position.horizontalOrder).toBe(1);
5454
});
5555
});
56+
57+
describe("addId", () => {
58+
it("should add a raw MongoID and set isOptimistic flag", () => {
59+
const event = createMockStandaloneEvent() as Schema_GridEvent;
60+
const result = addId(event);
61+
62+
expect(result._id).toBeDefined();
63+
expect(result._id).not.toMatch(/^optimistic-/);
64+
expect((result as any).isOptimistic).toBe(true);
65+
});
66+
});

packages/web/src/common/utils/event/event.util.ts

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import { ObjectId } from "bson";
22
import { DropResult } from "@hello-pangea/dnd";
3-
import {
4-
ID_OPTIMISTIC_PREFIX,
5-
Origin,
6-
Priorities,
7-
} from "@core/constants/core.constants";
3+
import { Origin, Priorities } from "@core/constants/core.constants";
84
import { YEAR_MONTH_DAY_COMPACT_FORMAT } from "@core/constants/date.constants";
95
import { Status } from "@core/errors/status.codes";
106
import {
@@ -25,8 +21,8 @@ import { isElementInViewport } from "@web/common/context/pointer-position";
2521
import { PartialMouseEvent } from "@web/common/types/util.types";
2622
import {
2723
Schema_GridEvent,
28-
Schema_OptimisticEvent,
2924
Schema_WebEvent,
25+
WithId,
3026
} from "@web/common/types/web.event.types";
3127
import {
3228
focusElement,
@@ -43,6 +39,21 @@ export const gridEventDefaultPosition: Schema_GridEvent["position"] = {
4339
dragOffset: { x: 0, y: 0 },
4440
};
4541

42+
export const addId = (event: Schema_GridEvent): WithId<Schema_GridEvent> => {
43+
const _event = {
44+
...event,
45+
_id: new ObjectId().toString(),
46+
} as WithId<Schema_GridEvent>;
47+
48+
Object.defineProperty(_event, "isOptimistic", {
49+
value: true,
50+
writable: true,
51+
configurable: true,
52+
enumerable: false,
53+
});
54+
return _event;
55+
};
56+
4657
export const assembleDefaultEvent = async (
4758
draftType?: Categories_Event | null,
4859
startDate?: string,
@@ -248,26 +259,21 @@ export const isEventInRange = (
248259
return isStartDateInRange || isEndDateInRange;
249260
};
250261

251-
export const isOptimisticEvent = (event: Schema_Event) => {
252-
const isOptimistic = event._id?.startsWith(ID_OPTIMISTIC_PREFIX) || false;
253-
return isOptimistic;
254-
};
255-
256262
export const getEventCursorStyle = (
257263
isDragging: boolean,
258-
isOptimistic: boolean,
264+
isPending = false,
259265
): string => {
260266
if (isDragging) return "move";
261-
if (isOptimistic) return "wait";
267+
if (isPending) return "wait";
262268
return "pointer";
263269
};
264270

265271
export const getEventCursorClass = (
266272
isDragging: boolean,
267-
isOptimistic: boolean,
273+
isPending = false,
268274
): string => {
269275
if (isDragging) return "cursor-move";
270-
if (isOptimistic) return "cursor-wait";
276+
if (isPending) return "cursor-wait";
271277
return "cursor-pointer";
272278
};
273279

@@ -291,17 +297,6 @@ export const prepEvtAfterDraftDrop = (
291297
return event;
292298
};
293299

294-
export const replaceIdWithOptimisticId = (
295-
event: Schema_GridEvent,
296-
): Schema_OptimisticEvent => {
297-
const _event = {
298-
...event,
299-
_id: `${ID_OPTIMISTIC_PREFIX}-${new ObjectId().toString()}`,
300-
};
301-
302-
return _event;
303-
};
304-
305300
const _assembleBaseEvent = (
306301
userId: string,
307302
event: Partial<Schema_Event>,

packages/web/src/common/utils/event/someday.event.util.test.ts

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { ObjectId } from "bson";
22
import { faker } from "@faker-js/faker";
33
import {
4-
ID_OPTIMISTIC_PREFIX,
54
Origin,
65
Priorities,
76
RRULE,
@@ -333,14 +332,14 @@ describe("computeRelativeEventDateRange", () => {
333332
jest.useRealTimers();
334333
});
335334

336-
describe("Optimistic Event IDs", () => {
337-
it("should handle events with optimistic IDs", () => {
338-
const optimisticId = `${ID_OPTIMISTIC_PREFIX}-${new ObjectId().toString()}`;
335+
describe("Event IDs", () => {
336+
it("should handle events with IDs", () => {
337+
const eventId = new ObjectId().toString();
339338
// Add all required properties for Schema_SomedayEvent
340339
const events = {
341-
[optimisticId]: {
340+
[eventId]: {
342341
...baseEvent,
343-
_id: optimisticId,
342+
_id: eventId,
344343
startDate: "2024-03-19",
345344
endDate: "2024-03-20",
346345
isSomeday: true,
@@ -358,20 +357,20 @@ describe("computeRelativeEventDateRange", () => {
358357

359358
const result = categorizeSomedayEvents(events, weekDates);
360359

361-
expect(result.columns[COLUMN_WEEK].eventIds).toContain(optimisticId);
362-
expect(result.columns[COLUMN_MONTH].eventIds).not.toContain(optimisticId);
360+
expect(result.columns[COLUMN_WEEK].eventIds).toContain(eventId);
361+
expect(result.columns[COLUMN_MONTH].eventIds).not.toContain(eventId);
363362
expect(result.columnOrder).toEqual([COLUMN_WEEK, COLUMN_MONTH]);
364363
});
365364

366-
it("should maintain order for events with optimistic IDs", () => {
367-
const optimisticIdA = `${ID_OPTIMISTIC_PREFIX}-${new ObjectId().toString()}`;
368-
const optimisticIdB = `${ID_OPTIMISTIC_PREFIX}-${new ObjectId().toString()}`;
365+
it("should maintain order for events with IDs", () => {
366+
const eventIdA = new ObjectId().toString();
367+
const eventIdB = new ObjectId().toString();
369368

370369
// Add all required properties for Schema_SomedayEvent
371370
const events = {
372-
[optimisticIdA]: {
371+
[eventIdA]: {
373372
...baseEvent,
374-
_id: optimisticIdA,
373+
_id: eventIdA,
375374
startDate: "2024-03-19",
376375
endDate: "2024-03-20",
377376
order: 2,
@@ -380,9 +379,9 @@ describe("computeRelativeEventDateRange", () => {
380379
priority: Priorities.UNASSIGNED,
381380
user: "test-user",
382381
},
383-
[optimisticIdB]: {
382+
[eventIdB]: {
384383
...baseEvent,
385-
_id: optimisticIdB,
384+
_id: eventIdB,
386385
startDate: "2024-03-19",
387386
endDate: "2024-03-20",
388387
order: 1,
@@ -401,27 +400,27 @@ describe("computeRelativeEventDateRange", () => {
401400
const result = categorizeSomedayEvents(events, weekDates);
402401

403402
expect(result.columns[COLUMN_WEEK].eventIds).toEqual([
404-
optimisticIdB,
405-
optimisticIdA,
403+
eventIdB,
404+
eventIdA,
406405
]);
407406
});
408407

409-
it("should assign sequential orders for optimistic events without order", () => {
410-
const optimisticIdA = `${ID_OPTIMISTIC_PREFIX}-${new ObjectId().toString()}`;
411-
const optimisticIdB = `${ID_OPTIMISTIC_PREFIX}-${new ObjectId().toString()}`;
408+
it("should assign sequential orders for events without order", () => {
409+
const eventIdA = new ObjectId().toString();
410+
const eventIdB = new ObjectId().toString();
412411
// Add all required properties for Schema_Event
413412
const events = [
414413
{
415414
...baseEvent,
416-
_id: optimisticIdA,
415+
_id: eventIdA,
417416
isSomeday: true,
418417
origin: Origin.COMPASS,
419418
priority: Priorities.UNASSIGNED,
420419
user: "test-user",
421420
},
422421
{
423422
...baseEvent,
424-
_id: optimisticIdB,
423+
_id: eventIdB,
425424
isSomeday: true,
426425
origin: Origin.COMPASS,
427426
priority: Priorities.UNASSIGNED,
@@ -437,15 +436,15 @@ describe("computeRelativeEventDateRange", () => {
437436
]);
438437
});
439438

440-
it("should fill gaps in order sequence for optimistic events", () => {
441-
const optimisticIdA = `${ID_OPTIMISTIC_PREFIX}-${new ObjectId().toString()}`;
442-
const optimisticIdB = `${ID_OPTIMISTIC_PREFIX}-${new ObjectId().toString()}`;
443-
const optimisticIdC = `${ID_OPTIMISTIC_PREFIX}-${new ObjectId().toString()}`;
439+
it("should fill gaps in order sequence for events", () => {
440+
const eventIdA = new ObjectId().toString();
441+
const eventIdB = new ObjectId().toString();
442+
const eventIdC = new ObjectId().toString();
444443
// Add all required properties for Schema_Event
445444
const events = [
446445
{
447446
...baseEvent,
448-
_id: optimisticIdA,
447+
_id: eventIdA,
449448
order: 0,
450449
isSomeday: true,
451450
origin: Origin.COMPASS,
@@ -454,15 +453,15 @@ describe("computeRelativeEventDateRange", () => {
454453
},
455454
{
456455
...baseEvent,
457-
_id: optimisticIdB,
456+
_id: eventIdB,
458457
isSomeday: true,
459458
origin: Origin.COMPASS,
460459
priority: Priorities.UNASSIGNED,
461460
user: "test-user",
462461
}, // Should get order 1
463462
{
464463
...baseEvent,
465-
_id: optimisticIdC,
464+
_id: eventIdC,
466465
order: 3,
467466
isSomeday: true,
468467
origin: Origin.COMPASS,

0 commit comments

Comments
 (0)