Task #609: Per-user app enable/disable in Settings
- New `user_hidden_apps` table (userId+appId composite PK, cascade) in
lib/db/src/schema/user-hidden-apps.ts; registered in schema index;
pushed to dev DB via drizzle-kit.
- Backend (artifacts/api-server/src/routes/apps.ts):
- GET /me/apps — returns every globally-active app visible to the
user with a `hidden` flag.
- PUT /me/apps/:appId/hidden — toggles a row in user_hidden_apps,
gated by getVisibleAppsForUser + isActive so a user can't toggle
apps they can't reach.
- GET /apps (Home/Dock) now uses getVisibleNonHiddenAppsForUser so
hidden apps disappear immediately.
- lib/appsVisibility.ts: added getHiddenAppIdsForUser and
getVisibleNonHiddenAppsForUser helpers.
- OpenAPI: added /me/apps + /me/apps/{appId}/hidden, MyApp and
UpdateMyAppHiddenBody schemas; regenerated api-zod + api-client-react.
- Frontend (settings-panel.tsx): added "My apps" accordion section as
first GroupItem with MyAppsBody — Switch per app, optimistic update,
invalidates getListMyAppsQueryKey + getListAppsQueryKey so Home/Dock
refresh without reload.
- Translations: added settingsPanel.section.myApps + settingsPanel.myApps
in ar.json + en.json.
- Code review fix: /me/apps and PUT gating filter isActive even for
admins, so inactive apps don't appear in the Settings list.
- Proposed follow-up #611 (Playwright test that hidden apps disappear
from Home and Dock).
This commit is contained in:
@@ -6,6 +6,7 @@ export * from "./service-orders";
|
||||
export * from "./notifications";
|
||||
export * from "./settings";
|
||||
export * from "./user-app-orders";
|
||||
export * from "./user-hidden-apps";
|
||||
export * from "./app-opens";
|
||||
export * from "./password-reset-tokens";
|
||||
export * from "./notes";
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { pgTable, integer, primaryKey, timestamp } from "drizzle-orm/pg-core";
|
||||
import { usersTable } from "./users";
|
||||
import { appsTable } from "./apps";
|
||||
|
||||
export const userHiddenAppsTable = pgTable(
|
||||
"user_hidden_apps",
|
||||
{
|
||||
userId: integer("user_id")
|
||||
.notNull()
|
||||
.references(() => usersTable.id, { onDelete: "cascade" }),
|
||||
appId: integer("app_id")
|
||||
.notNull()
|
||||
.references(() => appsTable.id, { onDelete: "cascade" }),
|
||||
hiddenAt: timestamp("hidden_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.userId, t.appId] })],
|
||||
);
|
||||
|
||||
export type UserHiddenApp = typeof userHiddenAppsTable.$inferSelect;
|
||||
Reference in New Issue
Block a user