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:
riyadhafraa
2026-05-19 11:06:21 +00:00
parent 7609fa95e7
commit c31156be9f
11 changed files with 637 additions and 3 deletions
+1
View File
@@ -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";
+21
View File
@@ -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;