From 3af737186dfb91d79d8c69a6bc87b5e6cb99a8e3 Mon Sep 17 00:00:00 2001 From: riyadhafraa <49757212-riyadhafraa@users.noreply.replit.com> Date: Wed, 13 May 2026 19:11:10 +0000 Subject: [PATCH] Improve admin modal design and accessibility with shared component Refactors AdminFormDialog to create a reusable component for admin modals, including aria-labelledby/describedby attributes and Escape key closing functionality. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: fa532550-4873-409b-840a-ac4c474132f3 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/kI0sxlu Replit-Helium-Checkpoint-Created: true --- artifacts/tx-os/src/pages/admin.tsx | 467 +++++++++++++++++----------- 1 file changed, 277 insertions(+), 190 deletions(-) diff --git a/artifacts/tx-os/src/pages/admin.tsx b/artifacts/tx-os/src/pages/admin.tsx index 4b31d66c..fd18f5cc 100644 --- a/artifacts/tx-os/src/pages/admin.tsx +++ b/artifacts/tx-os/src/pages/admin.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, useMemo, type ReactNode } from "react"; +import { useState, useEffect, useRef, useMemo, useId, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { useLocation } from "wouter"; import { @@ -361,6 +361,118 @@ function DeletionWarningDialog({ ); } +function AdminFormDialog({ + title, + subtitle, + icon, + onCancel, + onSubmit, + submitLabel, + cancelLabel, + submitDisabled, + isPending, + children, + testId, + submitTestId, + maxWidth = "sm", +}: { + title: string; + subtitle?: string; + icon: ReactNode; + onCancel: () => void; + onSubmit: () => void; + submitLabel?: string; + cancelLabel?: string; + submitDisabled?: boolean; + isPending?: boolean; + children: ReactNode; + testId?: string; + submitTestId?: string; + maxWidth?: "sm" | "md" | "lg"; +}) { + const { t } = useTranslation(); + const titleId = useId(); + const subtitleId = useId(); + const widthClass = + maxWidth === "lg" ? "max-w-lg" : maxWidth === "md" ? "max-w-md" : "max-w-sm"; + useEffect(() => { + const handleKey = (e: KeyboardEvent) => { + if (e.key === "Escape" && !isPending) { + e.stopPropagation(); + onCancel(); + } + }; + window.addEventListener("keydown", handleKey); + return () => window.removeEventListener("keydown", handleKey); + }, [isPending, onCancel]); + return ( +
+
+
+ +
+

+ {title} +

+ {subtitle && ( +

+ {subtitle} +

+ )} +
+
+ +
+ {children} +
+ +
+ + +
+
+
+ ); +} + // Local-state permission picker used by the "Add app" dialog. The full // AppPermissionsEditor below talks to /apps/:id/permissions, which can't // run before the app exists. This picker just collects ids into the form @@ -1754,41 +1866,37 @@ export default function AdminPage() { {/* Create User Modal */} {newUserForm && ( -
-
-

{t("admin.addUser")}

-
- - setNewUserForm({ ...newUserForm, username: e.target.value })} - className="bg-white/70 border-slate-200" - /> -
-
- - setNewUserForm({ ...newUserForm, email: e.target.value })} - className="bg-white/70 border-slate-200" - /> -
-
- - setNewUserForm({ ...newUserForm, password: e.target.value })} - className="bg-white/70 border-slate-200" - /> -
-
- - -
+ } + onCancel={() => setNewUserForm(null)} + onSubmit={handleCreateUser} + isPending={createUser.isPending} + > +
+ + setNewUserForm({ ...newUserForm, username: e.target.value })} + />
-
+
+ + setNewUserForm({ ...newUserForm, email: e.target.value })} + /> +
+
+ + setNewUserForm({ ...newUserForm, password: e.target.value })} + /> +
+ )}
@@ -4063,80 +4171,75 @@ function UsersPanel({
{newUserOpen && ( -
-
-

{t("admin.addUser")}

-
- - setNewUserForm({ ...newUserForm, username: e.target.value })} className="bg-white/70 border-slate-200" /> -
-
- - setNewUserForm({ ...newUserForm, email: e.target.value })} className="bg-white/70 border-slate-200" /> -
-
- - setNewUserForm({ ...newUserForm, password: e.target.value })} className="bg-white/70 border-slate-200" /> -
-
- -
- {(groups ?? []).length === 0 && ( -
{t("admin.groups.empty")}
- )} - {(groups ?? []).map((g) => { - const checked = newUserForm.groupIds.includes(g.id); - return ( - - ); - })} -
-
-
- - + } + onCancel={() => setNewUserOpen(false)} + isPending={createUser.isPending} + onSubmit={() => { + createUser.mutate( + { + data: { + username: newUserForm.username, + email: newUserForm.email, + password: newUserForm.password, + ...(newUserForm.groupIds.length > 0 ? { groupIds: newUserForm.groupIds } : {}), + }, + }, + { + onSuccess: () => { + invalidateUsers(); + setNewUserOpen(false); + setNewUserForm({ username: "", email: "", password: "", groupIds: [] }); + toast({ title: t("common.success") }); + }, + onError: () => toast({ title: t("common.error"), variant: "destructive" }), + }, + ); + }} + > +
+ + setNewUserForm({ ...newUserForm, username: e.target.value })} /> +
+
+ + setNewUserForm({ ...newUserForm, email: e.target.value })} /> +
+
+ + setNewUserForm({ ...newUserForm, password: e.target.value })} /> +
+
+ +
+ {(groups ?? []).length === 0 && ( +
{t("admin.groups.empty")}
+ )} + {(groups ?? []).map((g) => { + const checked = newUserForm.groupIds.includes(g.id); + return ( + + ); + })}
-
+ )} {editingUser && ( @@ -4709,42 +4812,36 @@ function GroupsPanel({
{createOpen && ( -
-
-

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

-
- - setCreateForm({ ...createForm, name: e.target.value })} className="bg-white/70 border-slate-200" data-testid="group-name-input" /> -
-
- - setCreateForm({ ...createForm, descriptionAr: e.target.value })} className="bg-white/70 border-slate-200" dir="rtl" /> -
-
- - setCreateForm({ ...createForm, descriptionEn: e.target.value })} className="bg-white/70 border-slate-200" dir="ltr" /> -
-
- - -
+ } + onCancel={() => setCreateOpen(false)} + submitDisabled={!createForm.name.trim()} + isPending={createGroup.isPending} + submitTestId="create-group-submit" + onSubmit={() => + createGroup.mutate( + { data: { name: createForm.name.trim(), descriptionAr: createForm.descriptionAr || null, descriptionEn: createForm.descriptionEn || null } }, + { + onSuccess: () => { invalidate(); setCreateOpen(false); toast({ title: t("common.success") }); }, + onError: () => toast({ title: t("common.error"), variant: "destructive" }), + }, + ) + } + > +
+ + setCreateForm({ ...createForm, name: e.target.value })} data-testid="group-name-input" />
-
+
+ + setCreateForm({ ...createForm, descriptionAr: e.target.value })} dir="rtl" /> +
+
+ + setCreateForm({ ...createForm, descriptionEn: e.target.value })} dir="ltr" /> +
+ )} {editingGroupId != null && ( @@ -6496,55 +6593,45 @@ function RolesPanel({
{createOpen && ( -
-
-

{t("admin.roles.addRole")}

-
- - setCreateForm({ ...createForm, name: e.target.value })} - className="bg-white/70 border-slate-200" - placeholder={t("admin.roles.namePlaceholder")} - data-testid="role-name-input" - /> -

{t("admin.roles.nameHint")}

-
-
- - setCreateForm({ ...createForm, descriptionAr: e.target.value })} - className="bg-white/70 border-slate-200" - dir="rtl" - data-testid="role-desc-ar-input" - /> -
-
- - setCreateForm({ ...createForm, descriptionEn: e.target.value })} - className="bg-white/70 border-slate-200" - dir="ltr" - data-testid="role-desc-en-input" - /> -
-
- - -
+ } + onCancel={() => setCreateOpen(false)} + onSubmit={handleCreate} + submitDisabled={!createForm.name.trim()} + isPending={createRole.isPending} + testId="create-role-dialog" + submitTestId="create-role-submit" + > +
+ + setCreateForm({ ...createForm, name: e.target.value })} + placeholder={t("admin.roles.namePlaceholder")} + data-testid="role-name-input" + /> +

{t("admin.roles.nameHint")}

-
+
+ + setCreateForm({ ...createForm, descriptionAr: e.target.value })} + dir="rtl" + data-testid="role-desc-ar-input" + /> +
+
+ + setCreateForm({ ...createForm, descriptionEn: e.target.value })} + dir="ltr" + data-testid="role-desc-en-input" + /> +
+ )} {editingId != null && (