Skip to content

Conversation

@tyler-dane
Copy link
Contributor

Follow-up to #1394

- 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.
- 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.
… 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.
Copilot AI review requested due to automatic review settings December 30, 2025 19:47
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors optimistic rendering by replacing the optimistic ID prefix approach with a pending events tracking system. Events now receive their final MongoDB IDs immediately upon creation instead of temporary prefixed IDs that are later replaced. The key architectural change is the introduction of a pendingEventsSlice that tracks event IDs during creation/conversion, allowing the UI to disable interactions with events still being saved to the database.

  • Replaces optimistic ID prefix pattern (optimistic-{id}) with pending event tracking via Redux state
  • Events now maintain the same ID throughout their lifecycle (no more ID replacement)
  • UI components check pending state instead of ID patterns to prevent premature interactions

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/web/src/ducks/events/slices/pending.slice.ts New Redux slice to track pending event IDs during creation/conversion
packages/web/src/ducks/events/selectors/pending.selectors.ts Selectors for accessing pending event state
packages/web/src/ducks/events/sagas/event.sagas.ts Updated create/delete sagas to track pending events; removed ID replacement logic
packages/web/src/ducks/events/sagas/someday.sagas.ts Removed ID replacement in convert saga
packages/web/src/ducks/events/sagas/saga.util.ts Removed replaceOptimisticId saga; updated _createOptimisticGridEvent to use new addId function
packages/web/src/common/utils/event/event.util.ts Replaced replaceIdWithOptimisticId and isOptimisticEvent with addId; updated cursor style functions to use isPending
packages/web/src/common/types/web.event.types.ts Removed optimisticIdSchema; added WithId utility type
packages/web/src/store/reducers.ts Integrated pendingEventsSlice into store
packages/web/src/views/Forms/hooks/useOpenEventForm.ts Checks pending state instead of ID pattern to prevent opening form for pending events
packages/web/src/views/Day/hooks/events/useOpenAgendaEventPreview.ts Checks pending state to prevent opening preview for pending events
packages/web/src/views/Day/components/Agenda/Events/*/Draggable*.tsx Components use pending state selector instead of isOptimisticEvent
packages/web/src/views/Calendar/components/Grid/MainGrid/MainGridEvents.tsx Prevents click handler from opening form for pending events
packages/web/src/views/Calendar/components/Grid/AllDayRow/AllDayEvent.tsx Uses pending state for cursor styling and resize prevention
packages/web/src/views/Calendar/components/Event/Grid/GridEvent/GridEvent.tsx Uses pending state for cursor styling and resize prevention
packages/web/src/views/Calendar/components/Event/styled.ts Renamed isOptimistic prop to isPending
packages/web/src/views/Calendar/components/Draft/hooks/actions/useDraftActions.ts Uses pending state to determine if event is existing vs new
packages/web/src/components/ContextMenu/GridContextMenuWrapper.tsx Prevents context menu from opening for pending events
packages/web/src/components/ContextMenu/ContextMenuItems.tsx Disables edit/duplicate actions for pending events with visual feedback
Test files Updated mocks and assertions to reflect new pending state approach and same-ID lifecycle

tyler-dane and others added 3 commits December 30, 2025 14:03
…gement

- 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.
- 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.
Copilot AI review requested due to automatic review settings December 30, 2025 20:18
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

packages/web/src/ducks/events/sagas/event.sagas.ts:143

  • The pending event ID should be removed from the pending state when an event is deleted. Without this cleanup, the pending event IDs array will grow indefinitely as events are deleted, causing a memory leak.

After line 127 (after deleting from entities), add a cleanup action to remove the event from pending state regardless of whether it was pending or not (the remove action is idempotent).

export function* deleteEvent({ payload }: Action_DeleteEvent) {
  try {
    yield put(getWeekEventsSlice.actions.delete(payload));
    yield put(getDayEventsSlice.actions.delete(payload));
    yield put(eventsEntitiesSlice.actions.delete(payload));

    const pendingEventIds = (yield select(
      (state: RootState) => state.events.pendingEvents.eventIds,
    )) as string[];
    const isPending = pendingEventIds.includes(payload._id);
    // Only call delete API if event is not pending (i.e., exists in DB)
    if (!isPending) {
      yield call(EventApi.delete, payload._id, payload.applyTo);
    }

    yield put(deleteEventSlice.actions.success());
  } catch (error) {
    yield put(deleteEventSlice.actions.error());
    handleError(error as Error);
  }
}

- 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.
- 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.
Copilot AI review requested due to automatic review settings December 30, 2025 21:18
- 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.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated 3 comments.

Comment on lines 40 to 44

// Mark event as pending when edit starts
yield put(pendingEventsSlice.actions.add(optimisticEvent._id));

yield* _editEvent(gridEvent);
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical bug: The optimistic event is created with a new ID on line 39, but then _editEvent is called with the original gridEvent which still has the old ID from the existing someday event. This means the backend will try to edit an event with the old ID, while the frontend has already inserted an optimistic event with a different new ID into state.

The fix should be to call _editEvent with optimisticEvent instead of gridEvent, after ensuring optimisticEvent has the correct data from gridEvent (minus the _id change).

Suggested change
// Mark event as pending when edit starts
yield put(pendingEventsSlice.actions.add(optimisticEvent._id));
yield* _editEvent(gridEvent);
// Ensure optimisticEvent has the latest gridEvent data, while preserving
// the optimistic-specific fields (like the new _id).
optimisticEvent = {
...gridEvent,
...optimisticEvent,
_id: optimisticEvent._id,
} as Schema_OptimisticEvent;
// Mark event as pending when edit starts
yield put(pendingEventsSlice.actions.add(optimisticEvent._id));
yield* _editEvent(optimisticEvent);

Copilot uses AI. Check for mistakes.
// Mark event as pending when edit starts
yield put(pendingEventsSlice.actions.add(optimisticEvent._id));

yield* _editEvent(gridEvent, { applyTo });
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical bug: The optimistic event is created with a new ID on line 62, but then _editEvent is called on line 67 with the original gridEvent which still has the old ID. This mismatch will cause the backend to attempt editing the event with the old ID while the frontend has already inserted an optimistic event with a different new ID into state.

The fix should be to call _editEvent with optimisticEvent instead of gridEvent.

Suggested change
yield* _editEvent(gridEvent, { applyTo });
yield* _editEvent(optimisticEvent, { applyTo });

Copilot uses AI. Check for mistakes.
Comment on lines +61 to 67
// optimistic event will have the same ID as that eventually saved
optimisticEvent = yield* _createOptimisticGridEvent(gridEvent, true);

// Mark event as pending when edit starts
yield put(pendingEventsSlice.actions.add(optimisticEvent._id));

yield* _editEvent(gridEvent, { applyTo });
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 61 is now misleading. It says "optimistic event will have the same ID as that eventually saved", but this is only true for newly created events (createEvent saga). For conversion operations like this one, the optimistic event gets a new ID while the original event that's being converted has its own ID. The backend edit call should use the new optimistic event's ID, not the old event's ID.

Suggested change
// optimistic event will have the same ID as that eventually saved
optimisticEvent = yield* _createOptimisticGridEvent(gridEvent, true);
// Mark event as pending when edit starts
yield put(pendingEventsSlice.actions.add(optimisticEvent._id));
yield* _editEvent(gridEvent, { applyTo });
// For conversions, the optimistic event represents the new event and has its own ID
optimisticEvent = yield* _createOptimisticGridEvent(gridEvent, true);
// Mark event as pending when edit starts
yield put(pendingEventsSlice.actions.add(optimisticEvent._id));
yield* _editEvent(optimisticEvent, { applyTo });

Copilot uses AI. Check for mistakes.
…g 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.
…lized 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.
…enda 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.
- 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.
…nts 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.
- 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.
- 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.
Copilot AI review requested due to automatic review settings December 31, 2025 00:28
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 43 out of 43 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (1)

packages/web/src/ducks/events/sagas/event.sagas.ts:143

  • The deleteEvent saga does not remove the event ID from the pendingEvents state when deleting a pending event. This can lead to memory leaks where pending event IDs remain in state even after the event has been deleted. The saga should call pendingEventsSlice.actions.remove(payload._id) before or after deleting the event from other slices.
export function* deleteEvent({ payload }: Action_DeleteEvent) {
  try {
    yield put(getWeekEventsSlice.actions.delete(payload));
    yield put(getDayEventsSlice.actions.delete(payload));
    yield put(eventsEntitiesSlice.actions.delete(payload));

    const pendingEventIds = (yield select(
      (state: RootState) => state.events.pendingEvents.eventIds,
    )) as string[];
    const isPending = pendingEventIds.includes(payload._id);
    // Only call delete API if event is not pending (i.e., exists in DB)
    if (!isPending) {
      yield call(EventApi.delete, payload._id, payload.applyTo);
    }

    yield put(deleteEventSlice.actions.success());
  } catch (error) {
    yield put(deleteEventSlice.actions.error());
    handleError(error as Error);
  }
}

interactions={interactions}
isDraftEvent={draft?._id === event._id}
isNewDraftEvent={!events.find((e) => e._id === event?._id)}
isDisabled={pendingEventIds.includes(event._id!)}
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code uses non-null assertion (event._id!) when checking if an event is pending, but Schema_GridEvent can have an optional _id field. If _id is undefined, this will cause undefined to be passed to includes(), which will always return false but is semantically incorrect. Add a null check before the includes call: event._id && pendingEventIds.includes(event._id) or use optional chaining.

Copilot uses AI. Check for mistakes.
interactions={interactions}
isDraftEvent={draft?._id === event._id}
isNewDraftEvent={!timedEvents.find((e) => e._id === event._id)}
isDisabled={pendingEventIds.includes(event._id!)}
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code uses non-null assertion (event._id!) when checking if an event is pending, but Schema_GridEvent can have an optional _id field. If _id is undefined, this will cause undefined to be passed to includes(), which will always return false but is semantically incorrect. Add a null check before the includes call: event._id && pendingEventIds.includes(event._id) or use optional chaining.

Copilot uses AI. Check for mistakes.

const sidebarContext = useSidebarContext(true);
const isPending = useAppSelector((state) =>
selectIsEventPending(state, event._id!),
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code uses non-null assertion (event._id!) when checking if an event is pending, but event._id can be undefined in the context menu. If _id is undefined, this will pass undefined to the selector which may cause issues. Add a null check: event._id && selectIsEventPending(state, event._id) or handle the undefined case in the selector.

Suggested change
selectIsEventPending(state, event._id!),
event._id ? selectIsEventPending(state, event._id) : false,

Copilot uses AI. Check for mistakes.
…enuItems

- 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.
@tyler-dane tyler-dane merged commit 7419f4c into main Dec 31, 2025
5 checks passed
@tyler-dane tyler-dane deleted the refactor/optimistic-ids branch December 31, 2025 01:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants