fix(web): tighten settings follow-up

This commit is contained in:
Val Alexander
2026-05-11 08:02:08 -05:00
parent 457422bd87
commit 7abd808fd4
3 changed files with 132 additions and 21 deletions
+23
View File
@@ -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<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-[color:var(--line)]",
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
className,
)}
{...props}
/>
));
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };
+104 -19
View File
@@ -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<string, unknown>;
memberships?: Array<typeof orgMembership>;
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(<Settings />);
@@ -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(<Settings />);
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(<Settings />);
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(<Settings />);
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(<Settings />);
expect(navigateMock).toHaveBeenCalledWith({ search: { view: "tokens" }, replace: true });
});
});
+5 -2
View File
@@ -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.
</p>
</header>
<hr className="border-[color:var(--line)]" />
<Separator />
<div className="flex flex-col gap-6 lg:flex-row lg:items-start">
<aside className="lg:sticky lg:top-[var(--settings-sticky-top)] lg:w-[272px] lg:shrink-0">