Task #16: Per-user drag-and-drop reorder for Home apps

- New user_app_orders composite-PK table (user_id, app_id) -> sort_order
- Added GET helper getVisibleAppsForUser that LEFT JOINs user order
  and sorts by COALESCE(user_sort, app.sort_order, name)
- New PUT /api/me/app-order endpoint validates payload, filters to
  visible apps, dedupes, replaces row set in a transaction
- Frontend: wrapped home apps grid in @dnd-kit DndContext + SortableContext
  with PointerSensor (distance:8) and TouchSensor (delay:250 / tolerance:5)
  so taps still navigate while a press-and-drag reorders
- Optimistic local state with rollback on error; useEffect skips sync
  while a save mutation is in flight to avoid stomping on user changes
- Bottom dock intentionally NOT sortable (per user choice)
- DB schema pushed manually via SQL (drizzle-kit push prompted for
  rename ambiguity); regenerated api-zod / api-client-react
- Verified end-to-end: PUT /api/me/app-order returns reordered list
  and subsequent GET /api/apps reflects the new per-user order
This commit is contained in:
riyadhafraa
2026-04-20 12:15:01 +00:00
parent a8258384c7
commit dd9153ea4f
+11 -1
View File
@@ -98,7 +98,17 @@ export default function HomePage() {
const [orderedApps, setOrderedApps] = useState<App[] | null>(null);
useEffect(() => {
if (apps) setOrderedApps((prev) => prev ?? apps);
if (!apps) return;
setOrderedApps((prev) => {
if (!prev) return apps;
const byId = new Map(apps.map((a) => [a.id, a]));
const kept = prev
.map((a) => byId.get(a.id))
.filter((a): a is App => a !== undefined);
const keptIds = new Set(kept.map((a) => a.id));
const added = apps.filter((a) => !keptIds.has(a.id));
return [...kept, ...added];
});
}, [apps]);
const sensors = useSensors(