From 27401076d4ea2229630c7a1de54fbcfc0279fa52 Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 10 Apr 2026 16:52:04 -0500 Subject: [PATCH] Refine invite role selection UI --- ui/src/pages/CompanyInvites.test.tsx | 13 +++-- ui/src/pages/CompanyInvites.tsx | 83 ++++++++++++++++++++++------ 2 files changed, 74 insertions(+), 22 deletions(-) diff --git a/ui/src/pages/CompanyInvites.test.tsx b/ui/src/pages/CompanyInvites.test.tsx index cc2686e3c21..2f29b9a718f 100644 --- a/ui/src/pages/CompanyInvites.test.tsx +++ b/ui/src/pages/CompanyInvites.test.tsx @@ -161,14 +161,15 @@ describe("CompanyInvites", () => { expect(container.textContent).not.toContain("Expired invites"); expect(container.textContent).not.toContain("OpenClaw shortcut"); - const roleSelect = container.querySelector("select") as HTMLSelectElement | null; - expect(roleSelect).not.toBeNull(); + expect(container.textContent).toContain("Choose a role"); + expect(container.textContent).toContain("Each invite link is single-use."); + expect(container.textContent).toContain("Can create agents, invite users, assign tasks, and approve join requests."); + expect(container.textContent).toContain("Everything in Admin, plus managing members and permission grants."); await act(async () => { - if (roleSelect) { - roleSelect.value = "viewer"; - roleSelect.dispatchEvent(new Event("change", { bubbles: true })); - } + const viewerRadio = container.querySelector('input[type="radio"][value="viewer"]') as HTMLInputElement | null; + viewerRadio?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + viewerRadio?.dispatchEvent(new Event("change", { bubbles: true })); }); const buttons = Array.from(container.querySelectorAll("button")); diff --git a/ui/src/pages/CompanyInvites.tsx b/ui/src/pages/CompanyInvites.tsx index 356bc2443ad..3ae189a5e45 100644 --- a/ui/src/pages/CompanyInvites.tsx +++ b/ui/src/pages/CompanyInvites.tsx @@ -10,6 +10,33 @@ import { useToast } from "@/context/ToastContext"; import { Link } from "@/lib/router"; import { queryKeys } from "@/lib/queryKeys"; +const inviteRoleOptions = [ + { + value: "viewer", + label: "Viewer", + description: "Can view company work and follow along without operational permissions.", + gets: "No built-in grants.", + }, + { + value: "operator", + label: "Operator", + description: "Recommended for people who need to help run work without managing access.", + gets: "Can assign tasks.", + }, + { + value: "admin", + label: "Admin", + description: "Recommended for operators who need to invite people, create agents, and approve joins.", + gets: "Can create agents, invite users, assign tasks, and approve join requests.", + }, + { + value: "owner", + label: "Owner", + description: "Full company access, including membership and permission management.", + gets: "Everything in Admin, plus managing members and permission grants.", + }, +] as const; + export function CompanyInvites() { const { selectedCompany, selectedCompanyId } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); @@ -115,25 +142,49 @@ export function CompanyInvites() {

Create invite

- Generate a human invite link and choose the default role it should request. + Generate a human invite link and choose the default access it should request.

- +
+ Choose a role +
+ {inviteRoleOptions.map((option, index) => { + const checked = humanRole === option.value; + return ( + + ); + })} +
+
+ +
+ Each invite link is single-use. The first successful use creates a join request and consumes the link, even before approval. +