From bc74fba3f5a70729447c5f339b81c50955bc26ec Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Thu, 15 Oct 2026 14:55:00 +0800 Subject: [PATCH] fix(settings): leave user manage view when switching admin sections (#1649) The admin panel stays mounted across Settings pages, so an open user manage view kept replacing every other admin section until Back to users was clicked. Reset it whenever the selected section changes. --- src/ui/sidebar/AdminSettingsPanel.tsx | 6 + .../tests/sidebar/AdminSettingsPanel.test.tsx | 113 ++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 src/ui/tests/sidebar/AdminSettingsPanel.test.tsx diff --git a/src/ui/sidebar/AdminSettingsPanel.tsx b/src/ui/sidebar/AdminSettingsPanel.tsx index 535126c2d..4c91c589c 100644 --- a/src/ui/sidebar/AdminSettingsPanel.tsx +++ b/src/ui/sidebar/AdminSettingsPanel.tsx @@ -217,6 +217,12 @@ export function AdminSettingsPanel({ setUserPage(0); }, [userSearch]); + // The panel stays mounted across Settings pages, so an open user view + // would otherwise replace whichever section is picked next. + useEffect(() => { + setManageUser(null); + }, [section]); + useEffect(() => { onEditingChange?.(manageUser !== null); return () => onEditingChange?.(false); diff --git a/src/ui/tests/sidebar/AdminSettingsPanel.test.tsx b/src/ui/tests/sidebar/AdminSettingsPanel.test.tsx new file mode 100644 index 000000000..8402b00b9 --- /dev/null +++ b/src/ui/tests/sidebar/AdminSettingsPanel.test.tsx @@ -0,0 +1,113 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; +import { cleanup, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +import { AdminSettingsPanel } from "../../sidebar/AdminSettingsPanel"; +import type { AdminUser } from "../../sidebar/AdminManagementSections"; + +vi.mock("@/main-axios", () => ({ + getUserList: vi.fn(async () => ({ users: [], total: 0 })), + getSessions: vi.fn(async () => ({ sessions: [] })), + getRoles: vi.fn(async () => ({ roles: [] })), + getApiKeys: vi.fn(async () => ({ apiKeys: [] })), + getUserRoles: vi.fn(async () => ({ roles: [] })), + isElectron: () => false, +})); + +vi.mock("@/api/settings-api", () => ({})); +vi.mock("@/api/tls-api", () => ({ getTlsStatus: vi.fn(async () => null) })); +vi.mock("@/api/private-endpoints-api", () => ({})); + +vi.mock("@/contexts/BrandingContext", () => ({ + useBranding: () => ({ applyBranding: vi.fn() }), +})); + +vi.mock("@/components/surface/surface-scope", () => ({ + useConfirm: () => vi.fn(async () => true), +})); + +vi.mock("@/settings/FeatureSettingsSections", () => ({ + FeatureSettingsSection: () => null, + featureSectionId: (id: string) => `feature:${id}`, + useFeatureSettings: () => [], +})); + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})); + +vi.mock("sonner", () => ({ + toast: { success: vi.fn(), error: vi.fn() }, +})); + +const { bob, section } = vi.hoisted(() => ({ + bob: { + id: "u2", + username: "bob", + isAdmin: false, + isExternal: false, + passwordHash: "hash", + dataUnlocked: true, + } as AdminUser, + section: + (label: string) => + ({ open }: { open: boolean }) => + open ?
{label}
: null, +})); + +vi.mock("../../sidebar/AdminManagementSections", () => ({ + AdminUsersSection: ({ + open, + onManageUser, + }: { + open: boolean; + onManageUser: (user: AdminUser) => void; + }) => + open ? : null, + AdminSessionsSection: section("sessions section"), + AdminRolesSection: section("roles section"), +})); + +vi.mock("../../sidebar/AdminSettingsSections", () => ({ + AdminGeneralSettingsSection: section("general section"), + AdminHostDefaultsSection: section("host defaults section"), + AdminDatabaseSection: section("database section"), + AdminSSLSection: section("ssl section"), +})); + +vi.mock("../../sidebar/AdminApiKeysSection", () => ({ + AdminApiKeysSection: section("api keys section"), +})); +vi.mock("../../sidebar/AdminAuditLogSection", () => ({ + AdminAuditLogSection: section("audit log section"), +})); +vi.mock("../../sidebar/AdminBrandingSection", () => ({ + AdminBrandingSection: section("branding section"), +})); +vi.mock("../../sidebar/AdminUserDialogs", () => ({ + AdminCreateUserDialog: () => null, + AdminEditUserDialog: () => null, + AdminLinkAccountDialog: () => null, + AdminUnlinkAccountDialog: () => null, +})); +vi.mock("../../sidebar/AdminUserManagePanel", () => ({ + AdminUserManagePanel: ({ user }: { user: AdminUser }) => ( +
managing {user.username}
+ ), +})); + +afterEach(cleanup); + +describe("AdminSettingsPanel", () => { + it("leaves the user manage view when another section is selected", async () => { + const { rerender } = render(); + + await userEvent.click(screen.getByText("manage bob")); + expect(screen.getByText("managing bob")).toBeTruthy(); + + rerender(); + + expect(screen.getByText("sessions section")).toBeTruthy(); + expect(screen.queryByText("managing bob")).toBeNull(); + }); +});