mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
fix(web): tighten settings follow-up
This commit is contained in:
@@ -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
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user