Task #489: row-wide drag rotates meeting content; time + daily numbers anchored
Backend - New POST /api/executive-meetings/rotate-content (zod-validated) rotates ONLY meeting content through fixed (start_time, end_time, daily_number) slots. Same-date enforced; per-meeting expectedUpdatedAt → 409 stale; incomplete day (missing visible row) → 400. - ExecutiveMeetingsRotateContentBody added in lib/api-zod (manual.ts). - 6 backend tests cover happy path, stale, different_dates, 401, 403, incomplete_day. Existing /swap-times tests still pass. Frontend (artifacts/tx-os) - Whole <tr> is now the drag handle (the dedicated GripVertical button is retired). useSortable is gated on canMutate; safeRowDragListeners filters drags whose target is an interactive descendant (button, input, edit/time cells, row-actions, bulk-select). useSortable `attributes` are spread only when canMutate so view-mode rows stay clickable (otherwise aria-disabled blocked the popover trigger). - onRowDragEnd → rotateContent(fromId, toId): optimistic patch reassigns each chronological slot's tuple to the new occupant; rolls back + toast on failure. - Quick-actions popover now contains only Postpone (#486 Move up/down buttons removed). Tests (artifacts/tx-os) - New tests/executive-meetings-row-drag.spec.mjs: drags Alpha → Charlie position by # cell, asserts rotate-content fires and slots stay anchored. - tests/executive-meetings-row-quick-actions.spec.mjs: drops up/down cases, keeps Postpone + skip-surfaces + viewer. - tests/executive-meetings-schedule-features.spec.mjs: two legacy grip drag tests rewritten to drag the row body and target /rotate-content (the legacy /reorder route + tests are intentionally untouched). Drift / notes - Architect flagged a medium-severity hardening note: rotate-content FOR UPDATE locks orderedIds but not the day-scope completeness query. Out of #489 scope; no follow-up created (proposeFollowUpTasks was already consumed on #486).
This commit is contained in:
@@ -28,3 +28,26 @@ export const ExecutiveMeetingsSwapTimesBody = z.object({
|
||||
export type ExecutiveMeetingsSwapTimesBodyT = z.infer<
|
||||
typeof ExecutiveMeetingsSwapTimesBody
|
||||
>;
|
||||
|
||||
// #489: Body for POST /executive-meetings/rotate-content. Used when the
|
||||
// user drags a meeting from anywhere on its row to a new position. The
|
||||
// server keeps each meeting's slot (startTime, endTime, dailyNumber)
|
||||
// anchored to its physical row in chronological order and rotates the
|
||||
// meeting CONTENT (title, attendees, notes, color, status, merge) so
|
||||
// that visible position i ends up holding the i-th tuple from the
|
||||
// original chronological order.
|
||||
//
|
||||
// `orderedIds` is the new visible content order (same set as the day's
|
||||
// non-cancelled meetings — every visible row must appear exactly once).
|
||||
// `expectedUpdatedAt` carries each visible meeting's last-known
|
||||
// `updatedAt` ISO string so the server can detect concurrent edits and
|
||||
// return 409 `stale_meeting` like the other optimistic-locked routes.
|
||||
export const ExecutiveMeetingsRotateContentBody = z.object({
|
||||
meetingDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||
orderedIds: z.array(z.number().int().positive()).min(2).max(1000),
|
||||
expectedUpdatedAt: z.record(z.string().regex(/^\d+$/), z.string().datetime()),
|
||||
});
|
||||
|
||||
export type ExecutiveMeetingsRotateContentBodyT = z.infer<
|
||||
typeof ExecutiveMeetingsRotateContentBody
|
||||
>;
|
||||
|
||||
Reference in New Issue
Block a user