Add drag & drop reordering of trips on the dashboard

- trip_members.sort_order (per-user, migrated) drives GET /api/trips order
- PATCH /api/trips/:id/order updates only the caller's membership row
- Trip cards get a drag handle reusing the generalized row-reorder helper;
  clicking the card still navigates
This commit is contained in:
2026-07-20 09:41:43 +07:00
parent d393e16ae1
commit b452430415
8 changed files with 163 additions and 27 deletions
+55
View File
@@ -172,6 +172,61 @@ test('unknown trip returns 404', async () => {
assert.equal(res.body.error, 'not found');
});
test('PATCH /api/trips/:id/order reorders the dashboard list for the caller', async () => {
const { agent } = await createAccount();
const a = (await agent.post('/api/trips').send({ name: 'A', start_date: '2026-08-01', end_date: '2026-08-02' })).body.trip;
const b = (await agent.post('/api/trips').send({ name: 'B', start_date: '2026-08-01', end_date: '2026-08-02' })).body.trip;
const c = (await agent.post('/api/trips').send({ name: 'C', start_date: '2026-08-01', end_date: '2026-08-02' })).body.trip;
// Default order is newest first: C, B, A.
const initial = await agent.get('/api/trips');
assert.deepEqual(initial.body.trips.map((t) => t.id), [c.id, b.id, a.id]);
assert.ok(initial.body.trips.every((t) => t.sort_order === 0));
const setA = await agent.patch(`/api/trips/${a.id}/order`).send({ sort_order: 0 });
assert.equal(setA.status, 200);
assert.deepEqual(setA.body, { sort_order: 0 });
await agent.patch(`/api/trips/${b.id}/order`).send({ sort_order: 2 });
await agent.patch(`/api/trips/${c.id}/order`).send({ sort_order: 1 });
const reordered = await agent.get('/api/trips');
assert.deepEqual(reordered.body.trips.map((t) => t.id), [a.id, c.id, b.id]);
});
test('trip order is per-user', async () => {
const owner = await createAccount();
const guest = await createAccount();
const t1 = (await owner.agent.post('/api/trips').send({ name: 'T1', start_date: '2026-08-01', end_date: '2026-08-02' })).body.trip;
const t2 = (await owner.agent.post('/api/trips').send({ name: 'T2', start_date: '2026-08-01', end_date: '2026-08-02' })).body.trip;
await guest.agent.post('/api/trips/join').send({ code: t1.join_code });
await guest.agent.post('/api/trips/join').send({ code: t2.join_code });
// Owner reorders their own list; guest's list is unaffected.
const guestBefore = (await guest.agent.get('/api/trips')).body.trips.map((t) => t.id);
await owner.agent.patch(`/api/trips/${t1.id}/order`).send({ sort_order: 5 });
const guestAfter = (await guest.agent.get('/api/trips')).body.trips.map((t) => t.id);
assert.deepEqual(guestAfter, guestBefore);
const ownerList = (await owner.agent.get('/api/trips')).body.trips;
assert.equal(ownerList.find((t) => t.id === t1.id).sort_order, 5);
});
test('order endpoint validation: non-integer 400, non-member 404', async () => {
const owner = await createAccount();
const outsider = await createAccount();
const trip = (await owner.agent.post('/api/trips').send({ name: 'T', start_date: '2026-08-01', end_date: '2026-08-02' })).body.trip;
const badBody = await owner.agent.patch(`/api/trips/${trip.id}/order`).send({ sort_order: 'first' });
assert.equal(badBody.status, 400);
assert.equal(badBody.body.error, 'sort_order must be an integer');
const missing = await owner.agent.patch(`/api/trips/${trip.id}/order`).send({});
assert.equal(missing.status, 400);
const notMember = await outsider.agent.patch(`/api/trips/${trip.id}/order`).send({ sort_order: 1 });
assert.equal(notMember.status, 404);
});
// ---------------------------------------------------------------------------
// Membership via join code
// ---------------------------------------------------------------------------