Add ability to reorder apps on the home screen
Implements drag-and-drop functionality for reordering apps using @dnd-kit, adds a new `userAppOrdersTable` to the database schema to store user-specific app order preferences, and introduces a new API endpoint `/api/me/app-order` for updating these preferences. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: e782e35b-00f5-4b9b-8931-63051a25df80 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PrQkd7G Replit-Helium-Checkpoint-Created: true
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Router, type IRouter } from "express";
|
||||
import { eq, asc, inArray } from "drizzle-orm";
|
||||
import { eq, asc, inArray, sql } from "drizzle-orm";
|
||||
import { db } from "@workspace/db";
|
||||
import {
|
||||
appsTable,
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
userRolesTable,
|
||||
rolePermissionsTable,
|
||||
rolesTable,
|
||||
userAppOrdersTable,
|
||||
} from "@workspace/db";
|
||||
import { requireAuth, requireAdmin } from "../middlewares/auth";
|
||||
import {
|
||||
@@ -15,13 +16,12 @@ import {
|
||||
GetAppParams,
|
||||
UpdateAppParams,
|
||||
DeleteAppParams,
|
||||
UpdateMyAppOrderBody,
|
||||
} from "@workspace/api-zod";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
router.get("/apps", requireAuth, async (req, res): Promise<void> => {
|
||||
const userId = req.session.userId!;
|
||||
|
||||
async function getVisibleAppsForUser(userId: number): Promise<typeof appsTable.$inferSelect[]> {
|
||||
const userRoleRows = await db
|
||||
.select({ roleName: rolesTable.name, roleId: userRolesTable.roleId })
|
||||
.from(userRolesTable)
|
||||
@@ -30,18 +30,30 @@ router.get("/apps", requireAuth, async (req, res): Promise<void> => {
|
||||
|
||||
const isAdmin = userRoleRows.some((r) => r.roleName === "admin");
|
||||
|
||||
if (isAdmin) {
|
||||
const apps = await db
|
||||
.select()
|
||||
.from(appsTable)
|
||||
.where(eq(appsTable.isActive, true))
|
||||
.orderBy(asc(appsTable.sortOrder), asc(appsTable.nameEn));
|
||||
res.json(apps);
|
||||
return;
|
||||
}
|
||||
// Pull the user's preferred order, then order by COALESCE(user_order, app.sort_order, name).
|
||||
const orderedRows = await db
|
||||
.select({
|
||||
app: appsTable,
|
||||
userSort: userAppOrdersTable.sortOrder,
|
||||
})
|
||||
.from(appsTable)
|
||||
.leftJoin(
|
||||
userAppOrdersTable,
|
||||
sql`${userAppOrdersTable.appId} = ${appsTable.id} and ${userAppOrdersTable.userId} = ${userId}`,
|
||||
)
|
||||
.where(eq(appsTable.isActive, true))
|
||||
.orderBy(
|
||||
sql`CASE WHEN ${userAppOrdersTable.sortOrder} IS NULL THEN 1 ELSE 0 END`,
|
||||
asc(userAppOrdersTable.sortOrder),
|
||||
asc(appsTable.sortOrder),
|
||||
asc(appsTable.nameEn),
|
||||
);
|
||||
|
||||
const apps = orderedRows.map((r) => r.app);
|
||||
|
||||
if (isAdmin) return apps;
|
||||
|
||||
const userRoleIds = userRoleRows.map((r) => r.roleId);
|
||||
|
||||
const userPermissionIds = userRoleIds.length > 0
|
||||
? (await db
|
||||
.select({ permissionId: rolePermissionsTable.permissionId })
|
||||
@@ -50,29 +62,12 @@ router.get("/apps", requireAuth, async (req, res): Promise<void> => {
|
||||
).map((r) => r.permissionId)
|
||||
: [];
|
||||
|
||||
const apps = await db
|
||||
.select()
|
||||
.from(appsTable)
|
||||
.where(
|
||||
eq(appsTable.isActive, true),
|
||||
)
|
||||
.orderBy(asc(appsTable.sortOrder), asc(appsTable.nameEn));
|
||||
|
||||
const appsWithPermissions = await db
|
||||
.select({ appId: appPermissionsTable.appId })
|
||||
.from(appPermissionsTable);
|
||||
|
||||
const restrictedAppIds = new Set(appsWithPermissions.map((r) => r.appId));
|
||||
|
||||
if (restrictedAppIds.size === 0) {
|
||||
const unrestricted = await db
|
||||
.select()
|
||||
.from(appsTable)
|
||||
.where(eq(appsTable.isActive, true))
|
||||
.orderBy(asc(appsTable.sortOrder), asc(appsTable.nameEn));
|
||||
res.json(unrestricted);
|
||||
return;
|
||||
}
|
||||
if (restrictedAppIds.size === 0) return apps;
|
||||
|
||||
const allowedRestrictedAppIds = userPermissionIds.length > 0
|
||||
? (await db
|
||||
@@ -83,12 +78,48 @@ router.get("/apps", requireAuth, async (req, res): Promise<void> => {
|
||||
: [];
|
||||
|
||||
const allowedAppIdSet = new Set(allowedRestrictedAppIds);
|
||||
|
||||
const visibleApps = apps.filter(
|
||||
return apps.filter(
|
||||
(app) => !restrictedAppIds.has(app.id) || allowedAppIdSet.has(app.id),
|
||||
);
|
||||
}
|
||||
|
||||
res.json(visibleApps);
|
||||
router.get("/apps", requireAuth, async (req, res): Promise<void> => {
|
||||
const userId = req.session.userId!;
|
||||
const visible = await getVisibleAppsForUser(userId);
|
||||
res.json(visible);
|
||||
});
|
||||
|
||||
router.put("/me/app-order", requireAuth, async (req, res): Promise<void> => {
|
||||
const userId = req.session.userId!;
|
||||
const parsed = UpdateMyAppOrderBody.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ error: parsed.error.message });
|
||||
return;
|
||||
}
|
||||
|
||||
const { order } = parsed.data;
|
||||
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;
|
||||
});
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.delete(userAppOrdersTable).where(eq(userAppOrdersTable.userId, userId));
|
||||
if (dedup.length > 0) {
|
||||
await tx.insert(userAppOrdersTable).values(
|
||||
dedup.map((appId, idx) => ({ userId, appId, sortOrder: idx })),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const updated = await getVisibleAppsForUser(userId);
|
||||
res.json(updated);
|
||||
});
|
||||
|
||||
router.post("/apps", requireAdmin, async (req, res): Promise<void> => {
|
||||
|
||||
Reference in New Issue
Block a user