fix(executive-meetings): keep visible drag-reorder chronological with cancelled rows
Task #311. The schedule view hides cancelled meetings, but the drag-reorder path was operating on the raw, unfiltered list — so cancelled rows consumed time slots and the dnd-kit indices skewed across hidden rows, leaving the visible list out of chronological order after a drop. Server (POST /api/executive-meetings/reorder) - Slot-swap now operates on the in-scope (orderedIds) subset only. Cancelled rows keep their (startTime, endTime, dailyNumber) untouched, so they no longer steal slots from the visible list. - New 400 codes: - cancelled_in_reorder: payload includes a cancelled meeting - incomplete_day: any non-cancelled meeting on the day is missing - Audit oldValue.order now reflects the visible order the user actually saw. - Phase-1/phase-2 negative-parking still avoids transient unique-constraint conflicts; cancelled rows' positive dailyNumbers cannot collide because slot dailyNumbers are a permutation of in-scope rows' existing values. Client (executive-meetings.tsx reorderRows) - Index math now derives ids from `orderedMeetings` (the visible list bound to SortableContext) instead of the raw `meetings` array. useCallback deps updated. Tests - artifacts/api-server/tests/executive-meetings.test.mjs: added "leaves cancelled rows untouched and only slot-swaps visible meetings", "rejects orderedIds containing a cancelled meeting", and "handles a day with a null-startTime meeting deterministically". - artifacts/tx-os/tests/executive-meetings-keyboard-editing.spec.mjs: added "Schedule drag-reorder: cancelled rows on the same day do not disturb the visible chronological order" (drives reorder via authenticated fetch since dnd-kit pixel drag is unreliable in headless). Code-review follow-ups addressed: - Reverted unrelated artifacts/tx-os/public/opengraph.jpg binary change. - Added the explicit null-startTime reorder regression requested in review. - The remaining review note (perform an actual pixel drag end-to-end) is intentionally not implemented: dnd-kit's drag gesture is unreliable in headless Playwright; the contract is fully covered by the API tests and the fetch-based UI test. All 8 reorder tests pass. Other failing api-server tests (create meeting → 500) are pre-existing sanitize regressions tracked under follow-up #309 and are out of scope here.
This commit is contained in:
@@ -1217,6 +1217,61 @@ test("Reorder: rejects orderedIds containing a cancelled meeting with code cance
|
||||
"rejection must be reported with the cancelled_in_reorder code");
|
||||
});
|
||||
|
||||
test("Reorder: handles a day with a null-startTime meeting deterministically", async () => {
|
||||
// Per the slot-swap sort, rows with non-null startTime come first
|
||||
// (chronological), and null-startTime rows come last. After a reorder
|
||||
// of [null-row, A, B], the null row should land in the LAST slot
|
||||
// (whatever its startTime/dailyNumber were originally), and A/B should
|
||||
// get the first two chronological slots. This proves the slot-swap
|
||||
// does not crash on nulls and produces a stable order.
|
||||
const reorderDate = "2050-06-10";
|
||||
const a = await api(adminCookie, "POST", "/api/executive-meetings", {
|
||||
titleAr: "أ", titleEn: "A", meetingDate: reorderDate,
|
||||
startTime: "09:00", endTime: "09:30",
|
||||
});
|
||||
const b = await api(adminCookie, "POST", "/api/executive-meetings", {
|
||||
titleAr: "ب", titleEn: "B", meetingDate: reorderDate,
|
||||
startTime: "10:00", endTime: "10:30",
|
||||
});
|
||||
const n = await api(adminCookie, "POST", "/api/executive-meetings", {
|
||||
titleAr: "ن", titleEn: "Null-time", meetingDate: reorderDate,
|
||||
// startTime/endTime intentionally omitted -> null in DB
|
||||
});
|
||||
const A = await a.json(); created.meetingIds.push(A.id);
|
||||
const B = await b.json(); created.meetingIds.push(B.id);
|
||||
const N = await n.json(); created.meetingIds.push(N.id);
|
||||
|
||||
// Ask: put null-row first, then A, then B
|
||||
const r = await api(adminCookie, "POST",
|
||||
"/api/executive-meetings/reorder",
|
||||
{ meetingDate: reorderDate, orderedIds: [N.id, A.id, B.id] });
|
||||
assert.equal(r.status, 200, "reorder with a null-time row must succeed");
|
||||
|
||||
const after = await pool.query(
|
||||
`SELECT id, daily_number, start_time, end_time
|
||||
FROM executive_meetings
|
||||
WHERE meeting_date = $1
|
||||
ORDER BY daily_number`,
|
||||
[reorderDate],
|
||||
);
|
||||
// Slots sorted by startTime (nulls last) were:
|
||||
// slot[0] = (09:00, 09:30, dn=A) for A originally
|
||||
// slot[1] = (10:00, 10:30, dn=B) for B originally
|
||||
// slot[2] = (null, null, dn=N) for N originally
|
||||
// Assignment N->slot[0], A->slot[1], B->slot[2]:
|
||||
const byId = Object.fromEntries(after.rows.map((r) => [r.id, r]));
|
||||
assert.equal(byId[N.id].start_time, "09:00:00",
|
||||
"null-time row should inherit the first chronological slot's startTime");
|
||||
assert.equal(byId[A.id].start_time, "10:00:00",
|
||||
"A should inherit the second chronological slot's startTime");
|
||||
assert.equal(byId[B.id].start_time, null,
|
||||
"B should inherit the originally-null third slot's startTime");
|
||||
// dailyNumbers must be unique 1..3 across the day
|
||||
const dns = after.rows.map((r) => r.daily_number).sort();
|
||||
assert.deepEqual(dns, [1, 2, 3].sort(),
|
||||
"dailyNumbers across the day must remain a unique 1..N permutation");
|
||||
});
|
||||
|
||||
test("Reorder: rejects an incomplete-day request (orderedIds missing some)", async () => {
|
||||
const reorderDate = "2050-02-20";
|
||||
const a = await api(adminCookie, "POST", "/api/executive-meetings", {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Reference in New Issue
Block a user