From bc0737aed262f2ca20062f65ae53cff282ed49ae Mon Sep 17 00:00:00 2001 From: riyadhafraa <49757212-riyadhafraa@users.noreply.replit.com> Date: Wed, 22 Apr 2026 10:13:17 +0000 Subject: [PATCH] Task #76: Show roles management inside the Groups editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a "Roles" tab to the GroupDetailEditor in the admin Groups screen so admins can manage which roles are auto-granted to members of a group. Changes: - OpenAPI: added `GET /roles` (operationId `listRoles`) and a `Role` schema in `lib/api-spec/openapi.yaml`. Ran `pnpm --filter @workspace/api-spec run codegen` to regenerate `lib/api-client-react` and `lib/api-zod`. - API server: added `GET /api/roles` (admin-only) in `artifacts/api-server/src/routes/groups.ts`, returning all roles ordered by name. - Frontend (`artifacts/teaboy-os/src/pages/admin.tsx`): - Imported `useListRoles` / `getListRolesQueryKey`. - Added `roleIds` state and a fourth `"roles"` tab to GroupDetailEditor with a checkbox list backed by the new endpoint, hydrated from `group.roleIds`. - Save now sends `roleIds: Array.from(roleIds)` via the existing `PATCH /api/groups/:id` mutation. - On save, also invalidate `getGetGroupQueryKey(editingGroupId)` so the detail cache (30s staleTime) reflects the new role list immediately when the editor is reopened. - Translations: added `admin.groups.tab.roles`, `admin.groups.rolesHint`, and `admin.groups.rolesEmpty` in both `en.json` and `ar.json`. Verification: - `pnpm -w run typecheck` passes. - `pnpm --filter @workspace/api-server test` — 42/42 pass. - e2e (testing skill): logged in as admin, opened TeaBoy group, toggled the `user` role on the new Roles tab, saved, reopened — toggle persisted. No deviations from the task description. Replit-Task-Id: 42808400-9209-469b-b526-37c776ffb25d --- artifacts/api-server/src/routes/groups.ts | 13 ++++ artifacts/teaboy-os/public/opengraph.jpg | Bin 24672 -> 25845 bytes artifacts/teaboy-os/src/locales/ar.json | 5 +- artifacts/teaboy-os/src/locales/en.json | 5 +- artifacts/teaboy-os/src/pages/admin.tsx | 46 +++++++++++- .../src/generated/api.schemas.ts | 10 +++ lib/api-client-react/src/generated/api.ts | 66 ++++++++++++++++++ lib/api-spec/openapi.yaml | 35 ++++++++++ lib/api-zod/src/generated/api.ts | 12 ++++ 9 files changed, 187 insertions(+), 5 deletions(-) diff --git a/artifacts/api-server/src/routes/groups.ts b/artifacts/api-server/src/routes/groups.ts index cff6991f..00b47eab 100644 --- a/artifacts/api-server/src/routes/groups.ts +++ b/artifacts/api-server/src/routes/groups.ts @@ -29,6 +29,19 @@ function serializeGroup(g: typeof groupsTable.$inferSelect) { }; } +router.get("/roles", requireAdmin, async (_req, res): Promise => { + const rows = await db.select().from(rolesTable).orderBy(rolesTable.name); + res.json( + rows.map((r) => ({ + id: r.id, + name: r.name, + descriptionAr: r.descriptionAr, + descriptionEn: r.descriptionEn, + createdAt: r.createdAt, + })), + ); +}); + router.get("/groups", requireAdmin, async (req, res): Promise => { const q = typeof req.query.q === "string" ? req.query.q.trim() : ""; const where = q ? ilike(groupsTable.name, `%${q}%`) : undefined; diff --git a/artifacts/teaboy-os/public/opengraph.jpg b/artifacts/teaboy-os/public/opengraph.jpg index 5e4be343a23951e31c74d2bf3f3a946e54fb6828..156fbf992b02b030d324759803c194ed0576b861 100644 GIT binary patch literal 25845 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 aVnvFesd>l3(~(h4%|W0=t%Abee*9m#9{cM6 literal 24672 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 K(|r2d&;JIh;lG{$ diff --git a/artifacts/teaboy-os/src/locales/ar.json b/artifacts/teaboy-os/src/locales/ar.json index 0124a409..3aa96719 100644 --- a/artifacts/teaboy-os/src/locales/ar.json +++ b/artifacts/teaboy-os/src/locales/ar.json @@ -393,10 +393,13 @@ "rolesCount": "{{count}} دور", "appsHint": "أعضاء هذه المجموعة سيرون هذه التطبيقات.", "usersHint": "أعضاء هذه المجموعة.", + "rolesHint": "يحصل أعضاء هذه المجموعة تلقائياً على هذه الأدوار.", + "rolesEmpty": "لا توجد أدوار متاحة.", "tab": { "info": "المعلومات", "apps": "التطبيقات", - "users": "المستخدمون" + "users": "المستخدمون", + "roles": "الأدوار" } }, "dashboardSoon": "إحصائيات اللوحة قريباً.", diff --git a/artifacts/teaboy-os/src/locales/en.json b/artifacts/teaboy-os/src/locales/en.json index 64f641fb..30472b68 100644 --- a/artifacts/teaboy-os/src/locales/en.json +++ b/artifacts/teaboy-os/src/locales/en.json @@ -390,10 +390,13 @@ "rolesCount": "{{count}} roles", "appsHint": "Members of this group will see these apps.", "usersHint": "Members of this group.", + "rolesHint": "Members of this group are automatically granted these roles.", + "rolesEmpty": "No roles available.", "tab": { "info": "Info", "apps": "Apps", - "users": "Users" + "users": "Users", + "roles": "Roles" } }, "dashboardSoon": "Dashboard widgets coming soon.", diff --git a/artifacts/teaboy-os/src/pages/admin.tsx b/artifacts/teaboy-os/src/pages/admin.tsx index 2d42d15e..a6a73dc9 100644 --- a/artifacts/teaboy-os/src/pages/admin.tsx +++ b/artifacts/teaboy-os/src/pages/admin.tsx @@ -38,6 +38,8 @@ import { useCreateGroup, useUpdateGroup, useDeleteGroup, + useListRoles, + getListRolesQueryKey, } from "@workspace/api-client-react"; import type { App, @@ -2547,7 +2549,15 @@ function GroupsPanel() { )} {editingGroupId != null && ( - setEditingGroupId(null)} onSaved={() => { invalidate(); setEditingGroupId(null); }} /> + setEditingGroupId(null)} + onSaved={() => { + invalidate(); + queryClient.invalidateQueries({ queryKey: getGetGroupQueryKey(editingGroupId) }); + setEditingGroupId(null); + }} + /> )} ); @@ -2560,6 +2570,7 @@ function GroupDetailEditor({ groupId, onClose, onSaved }: { groupId: number; onC const { data: group } = useGetGroup(groupId, { query: { queryKey: getGetGroupQueryKey(groupId) } }); const { data: apps } = useListApps({ query: { queryKey: getListAppsQueryKey() } }); const { data: users } = useListUsers(undefined, { query: { queryKey: getListUsersQueryKey() } }); + const { data: roles } = useListRoles({ query: { queryKey: getListRolesQueryKey() } }); const updateGroup = useUpdateGroup(); const [name, setName] = useState(""); @@ -2567,7 +2578,8 @@ function GroupDetailEditor({ groupId, onClose, onSaved }: { groupId: number; onC const [descriptionEn, setDescriptionEn] = useState(""); const [appIds, setAppIds] = useState>(new Set()); const [userIds, setUserIds] = useState>(new Set()); - const [tab, setTab] = useState<"info" | "apps" | "users">("info"); + const [roleIds, setRoleIds] = useState>(new Set()); + const [tab, setTab] = useState<"info" | "apps" | "users" | "roles">("info"); const [userSearch, setUserSearch] = useState(""); const visibleUsers = (users ?? []).filter((u) => { @@ -2588,6 +2600,7 @@ function GroupDetailEditor({ groupId, onClose, onSaved }: { groupId: number; onC setDescriptionEn(group.descriptionEn ?? ""); setAppIds(new Set(group.appIds)); setUserIds(new Set(group.userIds)); + setRoleIds(new Set(group.roleIds)); } }, [group]); @@ -2624,7 +2637,7 @@ function GroupDetailEditor({ groupId, onClose, onSaved }: { groupId: number; onC
- {(["info", "apps", "users"] as const).map((k) => ( + {(["info", "apps", "users", "roles"] as const).map((k) => (
)} + {tab === "roles" && ( +
+

{t("admin.groups.rolesHint")}

+ {roles?.map((r) => ( + + ))} + {(!roles || roles.length === 0) && ( +

{t("admin.groups.rolesEmpty")}

+ )} +
+ )} + {tab === "users" && (

{t("admin.groups.usersHint")}

@@ -2708,6 +2747,7 @@ function GroupDetailEditor({ groupId, onClose, onSaved }: { groupId: number; onC descriptionEn: descriptionEn || null, appIds: Array.from(appIds), userIds: Array.from(userIds), + roleIds: Array.from(roleIds), }, }, { diff --git a/lib/api-client-react/src/generated/api.schemas.ts b/lib/api-client-react/src/generated/api.schemas.ts index d998e834..5947c93b 100644 --- a/lib/api-client-react/src/generated/api.schemas.ts +++ b/lib/api-client-react/src/generated/api.schemas.ts @@ -169,6 +169,16 @@ export interface AddUserRoleBody { roleName: string; } +export interface Role { + id: number; + name: string; + /** @nullable */ + descriptionAr?: string | null; + /** @nullable */ + descriptionEn?: string | null; + createdAt: string; +} + export interface Group { id: number; name: string; diff --git a/lib/api-client-react/src/generated/api.ts b/lib/api-client-react/src/generated/api.ts index d7e2ab42..67af3f1a 100644 --- a/lib/api-client-react/src/generated/api.ts +++ b/lib/api-client-react/src/generated/api.ts @@ -53,6 +53,7 @@ import type { RequestUploadUrlBody, RequestUploadUrlResponse, ResetPasswordBody, + Role, SendMessageBody, Service, ServiceCategory, @@ -4673,6 +4674,71 @@ export const useRemoveUserRole = < return useMutation(getRemoveUserRoleMutationOptions(options)); }; +/** + * @summary List roles (admin) + */ +export const getListRolesUrl = () => { + return `/api/roles`; +}; + +export const listRoles = async (options?: RequestInit): Promise => { + return customFetch(getListRolesUrl(), { + ...options, + method: "GET", + }); +}; + +export const getListRolesQueryKey = () => { + return [`/api/roles`] as const; +}; + +export const getListRolesQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>(options?: { + query?: UseQueryOptions>, TError, TData>; + request?: SecondParameter; +}) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getListRolesQueryKey(); + + const queryFn: QueryFunction>> = ({ + signal, + }) => listRoles({ signal, ...requestOptions }); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type ListRolesQueryResult = NonNullable< + Awaited> +>; +export type ListRolesQueryError = ErrorType; + +/** + * @summary List roles (admin) + */ + +export function useListRoles< + TData = Awaited>, + TError = ErrorType, +>(options?: { + query?: UseQueryOptions>, TError, TData>; + request?: SecondParameter; +}): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getListRolesQueryOptions(options); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + /** * @summary List groups (admin) */ diff --git a/lib/api-spec/openapi.yaml b/lib/api-spec/openapi.yaml index 67cba5b6..cc290e02 100644 --- a/lib/api-spec/openapi.yaml +++ b/lib/api-spec/openapi.yaml @@ -1243,6 +1243,22 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" + # Roles (admin) + /roles: + get: + operationId: listRoles + tags: [users] + summary: List roles (admin) + responses: + "200": + description: Roles + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Role" + # Groups (admin) /groups: get: @@ -1829,6 +1845,25 @@ components: required: - roleName + Role: + type: object + properties: + id: + type: integer + name: + type: string + descriptionAr: + type: ["string", "null"] + descriptionEn: + type: ["string", "null"] + createdAt: + type: string + format: date-time + required: + - id + - name + - createdAt + GroupSummary: type: object properties: diff --git a/lib/api-zod/src/generated/api.ts b/lib/api-zod/src/generated/api.ts index 5c4a1e5e..959932c4 100644 --- a/lib/api-zod/src/generated/api.ts +++ b/lib/api-zod/src/generated/api.ts @@ -1590,6 +1590,18 @@ export const RemoveUserRoleResponse = zod.object({ createdAt: zod.coerce.date(), }); +/** + * @summary List roles (admin) + */ +export const ListRolesResponseItem = zod.object({ + id: zod.number(), + name: zod.string(), + descriptionAr: zod.string().nullish(), + descriptionEn: zod.string().nullish(), + createdAt: zod.coerce.date(), +}); +export const ListRolesResponse = zod.array(ListRolesResponseItem); + /** * @summary List groups (admin) */