diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx
new file mode 100644
index 00000000..883771c2
--- /dev/null
+++ b/src/components/ui/separator.tsx
@@ -0,0 +1,23 @@
+import * as SeparatorPrimitive from "@radix-ui/react-separator";
+import * as React from "react";
+import { cn } from "../../lib/utils";
+
+const Separator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
+
+));
+Separator.displayName = SeparatorPrimitive.Root.displayName;
+
+export { Separator };
diff --git a/src/routes/-settings.test.tsx b/src/routes/-settings.test.tsx
index aad26369..8dbf5e7c 100644
--- a/src/routes/-settings.test.tsx
+++ b/src/routes/-settings.test.tsx
@@ -1,5 +1,5 @@
/* @vitest-environment jsdom */
-import { render, screen } from "@testing-library/react";
+import { fireEvent, render, screen } from "@testing-library/react";
import type { ReactNode } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { api } from "../../convex/_generated/api";
@@ -29,8 +29,65 @@ vi.mock("@tanstack/react-router", () => ({
useSearch: () => searchMock(),
}));
+const signedInUser = {
+ _id: "user_123",
+ displayName: "Patrick",
+ name: "Patrick",
+ handle: "patrick",
+ email: "patrick@example.com",
+ image: null,
+ bio: null,
+};
+
+const orgMembership = {
+ publisher: {
+ _id: "publisher_openclaw",
+ handle: "openclaw",
+ displayName: "OpenClaw Team",
+ kind: "org",
+ image: null,
+ bio: "OpenClaw publisher",
+ },
+ role: "owner",
+};
+
+const orgMembers = {
+ publisher: { _id: "publisher_openclaw", handle: "openclaw" },
+ members: [
+ {
+ role: "owner",
+ user: {
+ _id: "user_123",
+ handle: "patrick",
+ displayName: "Patrick",
+ image: null,
+ },
+ },
+ ],
+};
+
+function mockSignedInSettings({
+ search = {},
+ memberships = [orgMembership],
+ members = orgMembers,
+}: {
+ search?: Record;
+ memberships?: Array;
+ members?: typeof orgMembers;
+} = {}) {
+ searchMock.mockReturnValue(search);
+ useQueryMock.mockImplementation((query, args) => {
+ if (query === api.users.me) return signedInUser;
+ if (args === "skip") return undefined;
+ if (args && typeof args === "object" && "publisherHandle" in args) return members;
+ if (args && typeof args === "object") return [];
+ return memberships;
+ });
+}
+
describe("Settings", () => {
beforeEach(() => {
+ window.history.replaceState(null, "", "/settings");
useQueryMock.mockReset();
useMutationMock.mockReset();
useAuthActionsMock.mockReset();
@@ -54,24 +111,7 @@ describe("Settings", () => {
});
it("renders account and appearance inside signed-in account preferences", () => {
- useQueryMock.mockImplementation((query, args) => {
- if (query === api.users.me) {
- return {
- _id: "user_123",
- displayName: "Patrick",
- name: "Patrick",
- handle: "patrick",
- email: "patrick@example.com",
- image: null,
- bio: null,
- };
- }
- if (args === "skip") return undefined;
- if (args && typeof args === "object" && "publisherHandle" in args) {
- return undefined;
- }
- return [];
- });
+ mockSignedInSettings();
render();
@@ -87,4 +127,49 @@ describe("Settings", () => {
expect(screen.queryByText(/high contrast/i)).toBeNull();
expect(screen.queryByText(/experimental features/i)).toBeNull();
});
+
+ it("does not load organization members on the default account view", () => {
+ mockSignedInSettings();
+
+ render();
+
+ expect(useQueryMock).toHaveBeenCalledWith(api.publishers.listMembers, "skip");
+ expect(screen.queryByRole("heading", { name: "Members" })).toBeNull();
+ });
+
+ it("navigates to a focused settings view from the section navigation", () => {
+ mockSignedInSettings();
+
+ render();
+
+ fireEvent.click(screen.getByRole("button", { name: "Organizations" }));
+
+ expect(navigateMock).toHaveBeenCalledWith({ search: { view: "organizations" } });
+ });
+
+ it("renders organization management and loads members only on the organizations view", async () => {
+ mockSignedInSettings({ search: { view: "organizations" } });
+
+ render();
+
+ expect(screen.getByRole("button", { name: "Organizations" }).getAttribute("aria-current")).toBe(
+ "true",
+ );
+ expect(await screen.findByText("OpenClaw Team")).toBeTruthy();
+ expect(screen.getByText("@openclaw ยท owner")).toBeTruthy();
+ expect(screen.getByRole("heading", { name: "Members" })).toBeTruthy();
+ expect(screen.getByText("Patrick")).toBeTruthy();
+ expect(useQueryMock).toHaveBeenCalledWith(api.publishers.listMembers, {
+ publisherHandle: "openclaw",
+ });
+ });
+
+ it("migrates legacy hash settings URLs to focused query params", () => {
+ window.history.replaceState(null, "", "/settings#tokens");
+ mockSignedInSettings();
+
+ render();
+
+ expect(navigateMock).toHaveBeenCalledWith({ search: { view: "tokens" }, replace: true });
+ });
});
diff --git a/src/routes/settings.tsx b/src/routes/settings.tsx
index 29c6050d..c9265887 100644
--- a/src/routes/settings.tsx
+++ b/src/routes/settings.tsx
@@ -49,6 +49,7 @@ import {
SelectTrigger,
SelectValue,
} from "../components/ui/select";
+import { Separator } from "../components/ui/separator";
import { Textarea } from "../components/ui/textarea";
import { ToggleGroup, ToggleGroupItem } from "../components/ui/toggle-group";
import { useThemeMode } from "../lib/theme";
@@ -185,7 +186,9 @@ export function Settings() {
const revokedTokens = (tokens ?? []).filter((token) => token.revokedAt);
const orgMembers = useQuery(
api.publishers.listMembers,
- selectedOrg ? { publisherHandle: selectedOrg.publisher.handle } : "skip",
+ activeView === "organizations" && selectedOrg
+ ? { publisherHandle: selectedOrg.publisher.handle }
+ : "skip",
) as OrgMembersResult | null | undefined;
useEffect(() => {
@@ -325,7 +328,7 @@ export function Settings() {
Account identity, publishing organizations, and API access for ClawHub.
-
+