Task #613: replace per-app hide toggles with single Dock visibility switch
The per-app hidden feature shipped in #609 was rejected — user wanted one toggle that hides/shows the entire bottom AppDock bar, leaving Home apps untouched. Removed: - lib/db/src/schema/user-hidden-apps.ts (and index.ts export); table dropped via `pnpm --filter @workspace/db run push` - GET /me/apps and PUT /me/apps/:appId/hidden routes - getHiddenAppIdsForUser + getVisibleNonHiddenAppsForUser helpers - MyApp + UpdateMyAppHiddenBody OpenAPI schemas - MyAppsBody settings UI + related i18n keys - Regenerated api-client-react + api-zod from the trimmed spec - Reverted GET /apps back to getVisibleAppsForUser Added: - artifacts/tx-os/src/hooks/use-dock-visible.ts — localStorage-backed preference with a custom window event for in-tab sync and the native `storage` event for cross-tab sync. Default = true. - DockBody in settings-panel: single ToggleRow under a new "App dock" section ("settingsPanel.section.dock" / "settingsPanel.dock.show") - AppDock now returns null when the preference is off and clears `--app-dock-height` so page padding doesn't stay reserved. Code review: PASS (architect). No remaining references to the removed infra. Typecheck shows only pre-existing errors in executive-meetings.ts and push.ts unrelated to this change. Follow-up: publish to Gitea + redeploy on Mac (proposed as a follow-up task).
This commit is contained in:
@@ -7,7 +7,6 @@ import {
|
||||
userAppOrdersTable,
|
||||
userGroupsTable,
|
||||
groupAppsTable,
|
||||
userHiddenAppsTable,
|
||||
} from "@workspace/db";
|
||||
import { and, asc, eq, inArray, sql } from "drizzle-orm";
|
||||
import { getEffectiveRoleIds } from "../middlewares/auth";
|
||||
@@ -107,30 +106,3 @@ export async function getVisibleAppsForUser(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of app IDs the given user has personally hidden from
|
||||
* their Home/Dock via PUT /me/apps/{appId}/hidden.
|
||||
*/
|
||||
export async function getHiddenAppIdsForUser(
|
||||
userId: number,
|
||||
): Promise<Set<number>> {
|
||||
const rows = await db
|
||||
.select({ appId: userHiddenAppsTable.appId })
|
||||
.from(userHiddenAppsTable)
|
||||
.where(eq(userHiddenAppsTable.userId, userId));
|
||||
return new Set(rows.map((r) => r.appId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Like getVisibleAppsForUser but additionally strips apps the user has
|
||||
* personally hidden. Used by GET /apps which backs Home + Dock so the
|
||||
* hidden toggle in Settings takes effect immediately.
|
||||
*/
|
||||
export async function getVisibleNonHiddenAppsForUser(
|
||||
userId: number,
|
||||
): Promise<typeof appsTable.$inferSelect[]> {
|
||||
const visible = await getVisibleAppsForUser(userId);
|
||||
const hidden = await getHiddenAppIdsForUser(userId);
|
||||
if (hidden.size === 0) return visible;
|
||||
return visible.filter((a) => !hidden.has(a.id));
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
auditLogsTable,
|
||||
permissionsTable,
|
||||
usersTable,
|
||||
userHiddenAppsTable,
|
||||
} from "@workspace/db";
|
||||
import { requireAuth, requireAdmin, getEffectiveRoleIds } from "../middlewares/auth";
|
||||
import {
|
||||
@@ -28,11 +27,7 @@ import { emitAppsChangedToPermissionHolders } from "../lib/realtime";
|
||||
// Re-exported from lib/appsVisibility so storage authorization
|
||||
// (lib/objectAuthz.ts) can use the exact same RBAC without taking a
|
||||
// dependency on this routes module.
|
||||
import {
|
||||
getVisibleAppsForUser,
|
||||
getVisibleNonHiddenAppsForUser,
|
||||
getHiddenAppIdsForUser,
|
||||
} from "../lib/appsVisibility";
|
||||
import { getVisibleAppsForUser } from "../lib/appsVisibility";
|
||||
export { getVisibleAppsForUser };
|
||||
import {
|
||||
CreateAppBody,
|
||||
@@ -47,71 +42,10 @@ const router: IRouter = Router();
|
||||
|
||||
router.get("/apps", requireAuth, async (req, res): Promise<void> => {
|
||||
const userId = req.session.userId!;
|
||||
// Home + Dock want the user's effective list, so apply the per-user
|
||||
// hidden filter here. The Settings panel calls GET /me/apps below to
|
||||
// see the full unfiltered list with the hidden flag for toggling.
|
||||
const visible = await getVisibleNonHiddenAppsForUser(userId);
|
||||
const visible = await getVisibleAppsForUser(userId);
|
||||
res.json(visible);
|
||||
});
|
||||
|
||||
router.get("/me/apps", requireAuth, async (req, res): Promise<void> => {
|
||||
const userId = req.session.userId!;
|
||||
// Exclude globally-inactive apps even for admins — the per-user
|
||||
// Settings list is "which of my apps do I want visible", and the
|
||||
// user can't reach inactive apps from Home/Dock anyway.
|
||||
const visible = (await getVisibleAppsForUser(userId)).filter(
|
||||
(a) => a.isActive,
|
||||
);
|
||||
const hidden = await getHiddenAppIdsForUser(userId);
|
||||
res.json(visible.map((a) => ({ ...a, hidden: hidden.has(a.id) })));
|
||||
});
|
||||
|
||||
router.put(
|
||||
"/me/apps/:appId/hidden",
|
||||
requireAuth,
|
||||
async (req, res): Promise<void> => {
|
||||
const userId = req.session.userId!;
|
||||
const appId = Number(req.params.appId);
|
||||
if (!Number.isInteger(appId) || appId <= 0) {
|
||||
res.status(400).json({ error: "Invalid appId" });
|
||||
return;
|
||||
}
|
||||
const body = req.body as { hidden?: unknown } | undefined;
|
||||
if (typeof body?.hidden !== "boolean") {
|
||||
res.status(400).json({ error: "hidden must be a boolean" });
|
||||
return;
|
||||
}
|
||||
// Gate on the user's actual visibility so a non-admin can't toggle
|
||||
// a hidden flag for an app they shouldn't even know exists.
|
||||
// Also require the app to be globally active so we don't persist
|
||||
// user_hidden_apps rows for apps the user can't reach anyway.
|
||||
const visible = (await getVisibleAppsForUser(userId)).filter(
|
||||
(a) => a.isActive,
|
||||
);
|
||||
const app = visible.find((a) => a.id === appId);
|
||||
if (!app) {
|
||||
res.status(404).json({ error: "App not found" });
|
||||
return;
|
||||
}
|
||||
if (body.hidden) {
|
||||
await db
|
||||
.insert(userHiddenAppsTable)
|
||||
.values({ userId, appId })
|
||||
.onConflictDoNothing();
|
||||
} else {
|
||||
await db
|
||||
.delete(userHiddenAppsTable)
|
||||
.where(
|
||||
and(
|
||||
eq(userHiddenAppsTable.userId, userId),
|
||||
eq(userHiddenAppsTable.appId, appId),
|
||||
),
|
||||
);
|
||||
}
|
||||
res.json({ ...app, hidden: body.hidden });
|
||||
},
|
||||
);
|
||||
|
||||
router.get("/admin/apps", requireAdmin, async (_req, res): Promise<void> => {
|
||||
const allApps = await db
|
||||
.select()
|
||||
|
||||
Reference in New Issue
Block a user