From 5a0c8a9ed1f772adb3fd4183094482e8a5f60393 Mon Sep 17 00:00:00 2001 From: riyadhafraa <49757212-riyadhafraa@users.noreply.replit.com> Date: Mon, 11 May 2026 12:36:04 +0000 Subject: [PATCH] Task #489 fix: decouple row drag from edit mode + fix optimistic order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- artifacts/tx-os/src/pages/executive-meetings.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/artifacts/tx-os/src/pages/executive-meetings.tsx b/artifacts/tx-os/src/pages/executive-meetings.tsx index 93456a9d..5304b093 100644 --- a/artifacts/tx-os/src/pages/executive-meetings.tsx +++ b/artifacts/tx-os/src/pages/executive-meetings.tsx @@ -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(queryKey); + const previousOptimisticOrder = optimisticOrder; const slotById = new Map(); 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 = {}; for (const m of orderedMeetings) { expectedUpdatedAt[String(m.id)] = m.updatedAt; @@ -2193,6 +2200,9 @@ function ScheduleSection({ if (previous !== undefined) { qc.setQueryData(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