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:
@@ -43,9 +43,7 @@ async function getVisibleAppsForUser(userId: number): Promise<typeof appsTable.$
|
||||
)
|
||||
.where(eq(appsTable.isActive, true))
|
||||
.orderBy(
|
||||
sql`CASE WHEN ${userAppOrdersTable.sortOrder} IS NULL THEN 1 ELSE 0 END`,
|
||||
asc(userAppOrdersTable.sortOrder),
|
||||
asc(appsTable.sortOrder),
|
||||
sql`COALESCE(${userAppOrdersTable.sortOrder}, ${appsTable.sortOrder})`,
|
||||
asc(appsTable.nameEn),
|
||||
);
|
||||
|
||||
@@ -98,22 +96,31 @@ router.put("/me/app-order", requireAuth, async (req, res): Promise<void> => {
|
||||
}
|
||||
|
||||
const { order } = parsed.data;
|
||||
|
||||
const seen = new Set<number>();
|
||||
for (const id of order) {
|
||||
if (seen.has(id)) {
|
||||
res.status(400).json({ error: `Duplicate app id in order: ${id}` });
|
||||
return;
|
||||
}
|
||||
seen.add(id);
|
||||
}
|
||||
|
||||
const visible = await getVisibleAppsForUser(userId);
|
||||
const visibleIds = new Set(visible.map((a) => a.id));
|
||||
|
||||
const filtered = order.filter((id) => visibleIds.has(id));
|
||||
const seen = new Set<number>();
|
||||
const dedup = filtered.filter((id) => {
|
||||
if (seen.has(id)) return false;
|
||||
seen.add(id);
|
||||
return true;
|
||||
});
|
||||
const invalid = order.filter((id) => !visibleIds.has(id));
|
||||
if (invalid.length > 0) {
|
||||
res.status(400).json({
|
||||
error: `Unknown or unauthorized app id(s): ${invalid.join(", ")}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.delete(userAppOrdersTable).where(eq(userAppOrdersTable.userId, userId));
|
||||
if (dedup.length > 0) {
|
||||
if (order.length > 0) {
|
||||
await tx.insert(userAppOrdersTable).values(
|
||||
dedup.map((appId, idx) => ({ userId, appId, sortOrder: idx })),
|
||||
order.map((appId, idx) => ({ userId, appId, sortOrder: idx })),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user