feat(web): reorderable ServerCards — drag-and-drop + keyboard (#1369)#1447
Conversation
Let users control the order of servers on the Servers screen instead of hand-editing mcp.json. Each ServerCard gets a grip handle (in the header, left of the name) that drives an @dnd-kit sortable; pointer and keyboard sensors both work, with live-region announcements for screen readers. - Backend: PUT /api/servers/order rewrites mcp.json in the supplied order inside the write lock, reusing the atomic-write + watcher-notify path. Registered before /:id so "order" isn't captured as a server id. Rejects with 409 when the id set no longer matches disk and 400 on duplicate / malformed bodies, so a reorder racing an external edit can't drop or duplicate an entry. - Hook: useServers.reorderServers(orderedIds) — optimistic local reorder, PUT, refresh-to-disk-truth on failure. - UI: SortableServerCard wrapper owns the dnd-kit concerns; ServerListScreen wraps the grid in DndContext + SortableContext (rect strategy) with pointer + keyboard sensors and announcements. Reorder math and a11y copy extracted to reorderIds.ts / serverReorder.ts for direct unit testing. Renders plain cards (no grip) when no onReorder is wired. - Wired onServerReorder through InspectorView -> App.tsx. Tests: pure-helper units, hook (persist / optimistic / 409 revert), 9 backend integration tests (incl. id-set-mismatch rejection and the route-registration guard), ServerCard/ServerListScreen units, and a real-browser Storybook keyboard-reorder play (the dnd-kit keyboard sensor needs layout rects that happy-dom doesn't provide). Adds @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@claude review |
|
Claude finished @cliffhall's task in 2m 39s —— View job Code Review — reorderable ServerCards (#1447)
Overall this is a clean, well-factored PR. The dumb- Findings1. Reorder failure is an unhandled promise rejection (minor, actionable) — onServerReorder={(orderedIds) => {
void reorderServers(orderedIds);
}}
onServerReorder={(orderedIds) => {
reorderServers(orderedIds).catch((err: unknown) => {
notifications.show({
color: "red",
title: "Couldn't reorder servers",
message: err instanceof Error ? err.message : String(err),
});
});
}}2. Reorder glue is computed unconditionally each render (nit) — const ids = servers.map((s) => s.id);
const handleDragEnd = makeServerDragEndHandler(servers, onReorder);
const announcements = buildReorderAnnouncements(servers);These run (and 3. Things I checked that are correct ✅
VerdictNo blockers. Recommend addressing #1 (silent reorder failure) before merge for UX consistency; #2/#3 are optional polish. |
…erable Address PR #1447 review: - App.tsx: catch reorderServers rejection (409 from a racing external edit, or a network error) and show a red toast, matching the error-handling pattern used by every other mutation — previously the rejection was discarded with `void` and the drag silently bounced back. - ServerListScreen: only build handleDragEnd / announcements when `reorderable`, so the four-closure announcements object isn't allocated every render when no onReorder is wired. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the review! Addressed in 855701c. #1 — Silent reorder failure (fixed). Agreed, this was the one real gap. reorderServers(orderedIds).catch((err: unknown) => {
notifications.show({
title: "Failed to reorder servers",
message: err instanceof Error ? err.message : String(err),
color: "red",
});
});#2 — Reorder glue computed unconditionally (fixed). #3 —
|
|
@claude review |
|
Claude finished @cliffhall's task in 42s —— View job Re-review — fixes in
|
Closes #1369.
Users can now control the order of
ServerCards on the Servers screen instead of hand-editingmcp.json. Each card has a grip handle (in the header, left of the name) that drives an@dnd-kitsortable; pointer and keyboard reorder both work, with live-region announcements for screen readers. The new order persists tomcp.jsonso it survives reloads and is visible to other Inspector incarnations.Screen capture
drag-cards.mov
Changes
Backend (
core/mcp/remote/node/server.ts)PUT /api/servers/orderaccepting{ order: string[] }. Rewritesmcp.jsonin the supplied order inside the write lock, reusing the atomic-write + watcher-notify path so peers see exactly one change.PUT /api/servers/:idso the literal/ordersegment isn't captured as an:id.Hook (
core/react/useServers.ts)reorderServers(orderedIds): optimistic local reorder so the grid reflows instantly; re-fetches disk truth on failure.UI
SortableServerCardwrapper owns all dnd-kit concerns (sortable node, per-frame transform, grip activator) soServerCardstays a dumb display component that just renders thedragHandleslot.ServerListScreenwraps the grid inDndContext+SortableContext(rect strategy) with pointer + keyboard sensors andannouncements. Reorder math and a11y copy are extracted toreorderIds.ts/serverReorder.tsfor direct unit testing. Renders plain cards (no grip) when noonReorderis wired.onServerReorderthroughInspectorView→App.tsx.ListToggle(subtle/gray/size=md), grab cursor via.server-drag-handle.Acceptance criteria
mcp.jsonand survives reload.ServerListScreenunits, a Storybook keyboard-reorder play, and backend integration tests incl. the conflicting-id-set rejection.Testing notes
The
@dnd-kitkeyboard sensor needs measurable layout rects that happy-dom doesn't provide, so the full keyboard gesture is exercised in a real-browser Storybook play (test:storybook); the ordering math + announcement copy are unit-tested directly via the extracted pure modules.npm run validate,npm run test:integration, andnpm run test:storybookall pass.Adds
@dnd-kit/core,@dnd-kit/sortable,@dnd-kit/utilities.🤖 Generated with Claude Code