Task #489 fix: decouple row drag from edit mode + fix optimistic order

Previous review rejected because row drag was gated on
`effectiveCanMutate = canMutate && editMode`, forcing users to
enter Edit mode to drag. Spec requires drag to work in view mode
for any user with mutate permission.

Second rejection: optimistic UI patched per-meeting (start/end/
dailyNumber) but did not reorder the rendered rows, so post-drop
slots could display rotated times against the wrong content
(e.g. 11:00, 09:00, 10:00) until refetch.

Changes:
- MeetingRow: new `dragEnabled` prop separate from `canMutate`.
  Drives useSortable.disabled, safeRowDragListeners gate, attributes
  spread, and cursor-grab class.
- Parent passes `dragEnabled={canMutate}` (raw, not gated by
  editMode); `canMutate={effectiveCanMutate}` retained for inline
  cell editing.
- handleRowClick: scope interactive `closest()` to descendants
  (`interactive !== e.currentTarget`). Required because the row now
  carries dnd-kit's role="button" in view mode and an unscoped
  closest() matched the row itself, swallowing every popover click.
- rotateContent: also call `setOptimisticOrder(newOrder)` so slot[i]
  visually shows newOrder[i] in the same React commit; rollback
  restores the prior optimistic order on rotate failure.
- e2e: removed edit-toggle click from row-drag spec — drag now
  passes in view mode.

Tests: row-drag spec (view mode), popover spec (4/4), schedule-
features drag tests (290, 834), backend rotate-content (6/6) all
pass. 3 unrelated schedule-features failures reference a missing
em-nav-settings testid that does not exist in source — pre-existing
broken tests, not caused by this task.
This commit is contained in:
Riyadh
2026-05-11 12:36:04 +00:00
parent 10a469b8dc
commit 5ddb1c2e52
@@ -2146,6 +2146,7 @@ function ScheduleSection({
// never in `orderedMeetings`, so they keep their existing tuple.
const queryKey = ["/api/executive-meetings", date] as const;
const previous = qc.getQueryData<DayResponse>(queryKey);
const previousOptimisticOrder = optimisticOrder;
const slotById = new Map<number, { startTime: string | null; endTime: string | null; dailyNumber: number }>();
newOrder.forEach((id, i) => {
const slotMeeting = orderedMeetings[i];
@@ -2173,6 +2174,12 @@ function ScheduleSection({
}
: prev,
);
// #489: also drive the rendered row order so slot[i] visually
// shows newOrder[i]. Without this, the array order in cache
// remains the pre-drag order and times appear rotated against
// the wrong row content (e.g. 11:00, 09:00, 10:00). Cleared on
// refetch by the useEffect on `meetings`.
setOptimisticOrder(newOrder);
const expectedUpdatedAt: Record<string, string> = {};
for (const m of orderedMeetings) {
expectedUpdatedAt[String(m.id)] = m.updatedAt;
@@ -2193,6 +2200,9 @@ function ScheduleSection({
if (previous !== undefined) {
qc.setQueryData<DayResponse>(queryKey, previous);
}
// #489: restore the prior optimistic order on rollback so a
// failed rotate doesn't leave the UI in the post-drop order.
setOptimisticOrder(previousOptimisticOrder);
const msg = err instanceof Error ? err.message : String(err);
toast({
title: t("common.error"),
@@ -2204,7 +2214,7 @@ function ScheduleSection({
setReordering(false);
}
},
[orderedMeetings, date, qc, refreshDay, toast, t],
[orderedMeetings, optimisticOrder, date, qc, refreshDay, toast, t],
);
// #486 → #489: Move up / Move down were retired in favour of