diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts
index 6a6441e002a..b0c92acb006 100644
--- a/packages/shared/src/constants.ts
+++ b/packages/shared/src/constants.ts
@@ -905,6 +905,7 @@ export const PLUGIN_RESERVED_COMPANY_SETTINGS_ROUTE_SEGMENTS = [
"general",
"environments",
"access",
+ "members",
"invites",
"secrets",
] as const;
diff --git a/ui/src/App.tsx b/ui/src/App.tsx
index fa320fe4639..abc17fc2487 100644
--- a/ui/src/App.tsx
+++ b/ui/src/App.tsx
@@ -31,7 +31,7 @@ import { Inbox } from "./pages/Inbox";
import { CompanySettings } from "./pages/CompanySettings";
import { CompanyEnvironments } from "./pages/CompanyEnvironments";
import { CompanySettingsPluginPage } from "./pages/CompanySettingsPluginPage";
-import { CompanyAccess } from "./pages/CompanyAccess";
+import { CompanyAccess, CompanyAccessLegacyRoute } from "./pages/CompanyAccess";
import { CompanyInvites } from "./pages/CompanyInvites";
import { CompanySkills } from "./pages/CompanySkills";
import { Secrets } from "./pages/Secrets";
@@ -70,7 +70,8 @@ function boardRoutes() {
} />
} />
} />
- } />
+ } />
+ } />
} />
} />
} />
diff --git a/ui/src/components/CompanySettingsSidebar.test.tsx b/ui/src/components/CompanySettingsSidebar.test.tsx
index adc9fec554e..d10c4e401ea 100644
--- a/ui/src/components/CompanySettingsSidebar.test.tsx
+++ b/ui/src/components/CompanySettingsSidebar.test.tsx
@@ -120,7 +120,7 @@ describe("CompanySettingsSidebar", () => {
expect(container.textContent).toContain("Company Settings");
expect(container.textContent).toContain("General");
expect(container.textContent).toContain("Environments");
- expect(container.textContent).toContain("Access");
+ expect(container.textContent).toContain("Members");
expect(container.textContent).toContain("Invites");
expect(container.textContent).toContain("Secrets");
expect(sidebarNavItemMock).toHaveBeenCalledWith(
@@ -139,8 +139,8 @@ describe("CompanySettingsSidebar", () => {
);
expect(sidebarNavItemMock).toHaveBeenCalledWith(
expect.objectContaining({
- to: "/company/settings/access",
- label: "Access",
+ to: "/company/settings/members",
+ label: "Members",
badge: 2,
end: true,
}),
diff --git a/ui/src/components/CompanySettingsSidebar.tsx b/ui/src/components/CompanySettingsSidebar.tsx
index 522f2f5bb28..ed84e9baed3 100644
--- a/ui/src/components/CompanySettingsSidebar.tsx
+++ b/ui/src/components/CompanySettingsSidebar.tsx
@@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query";
-import { ChevronLeft, KeyRound, MailPlus, MonitorCog, Puzzle, Settings, Shield, SlidersHorizontal } from "lucide-react";
+import { ChevronLeft, KeyRound, MailPlus, MonitorCog, Puzzle, Settings, SlidersHorizontal, Users } from "lucide-react";
import { sidebarBadgesApi } from "@/api/sidebarBadges";
import { ApiError } from "@/api/client";
import { Link } from "@/lib/router";
@@ -67,9 +67,9 @@ export function CompanySettingsSidebar() {
end
/>
diff --git a/ui/src/components/access/CompanySettingsNav.test.tsx b/ui/src/components/access/CompanySettingsNav.test.tsx
index be3daa819c3..7d795ee825a 100644
--- a/ui/src/components/access/CompanySettingsNav.test.tsx
+++ b/ui/src/components/access/CompanySettingsNav.test.tsx
@@ -60,27 +60,29 @@ describe("CompanySettingsNav", () => {
expect(getCompanySettingsTab("/PAP/company/settings")).toBe("general");
expect(getCompanySettingsTab("/company/settings/environments")).toBe("environments");
expect(getCompanySettingsTab("/PAP/company/settings/environments")).toBe("environments");
- expect(getCompanySettingsTab("/company/settings/access")).toBe("access");
- expect(getCompanySettingsTab("/PAP/company/settings/access")).toBe("access");
+ expect(getCompanySettingsTab("/company/settings/members")).toBe("members");
+ expect(getCompanySettingsTab("/PAP/company/settings/members")).toBe("members");
+ expect(getCompanySettingsTab("/company/settings/access")).toBe("members");
+ expect(getCompanySettingsTab("/PAP/company/settings/access")).toBe("members");
expect(getCompanySettingsTab("/company/settings/invites")).toBe("invites");
});
it("renders the active tab and navigates when a different tab is selected", async () => {
- currentPathname = "/PAP/company/settings/access";
+ currentPathname = "/PAP/company/settings/members";
const root = createRoot(container);
await act(async () => {
root.render( );
});
- expect(container.textContent).toContain("access");
+ expect(container.textContent).toContain("members");
expect(pageTabBarMock).toHaveBeenCalledWith(
expect.objectContaining({
- value: "access",
+ value: "members",
items: [
{ value: "general", label: "General" },
{ value: "environments", label: "Environments" },
- { value: "access", label: "Access" },
+ { value: "members", label: "Members" },
{ value: "invites", label: "Invites" },
],
}),
diff --git a/ui/src/components/access/CompanySettingsNav.tsx b/ui/src/components/access/CompanySettingsNav.tsx
index 82fb224551e..ded7a63def8 100644
--- a/ui/src/components/access/CompanySettingsNav.tsx
+++ b/ui/src/components/access/CompanySettingsNav.tsx
@@ -5,7 +5,7 @@ import { useLocation, useNavigate } from "@/lib/router";
const items = [
{ value: "general", label: "General", href: "/company/settings" },
{ value: "environments", label: "Environments", href: "/company/settings/environments" },
- { value: "access", label: "Access", href: "/company/settings/access" },
+ { value: "members", label: "Members", href: "/company/settings/members" },
{ value: "invites", label: "Invites", href: "/company/settings/invites" },
] as const;
@@ -16,8 +16,8 @@ export function getCompanySettingsTab(pathname: string): CompanySettingsTab {
return "environments";
}
- if (pathname.includes("/company/settings/access")) {
- return "access";
+ if (pathname.includes("/company/settings/members") || pathname.includes("/company/settings/access")) {
+ return "members";
}
if (pathname.includes("/company/settings/invites")) {
diff --git a/ui/src/pages/CompanyAccess.test.tsx b/ui/src/pages/CompanyAccess.test.tsx
index eefb7d73054..aaf4979e272 100644
--- a/ui/src/pages/CompanyAccess.test.tsx
+++ b/ui/src/pages/CompanyAccess.test.tsx
@@ -4,23 +4,25 @@ import { act } from "react";
import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { CompanyAccess } from "./CompanyAccess";
+import { CompanyAccess, CompanyAccessLegacyRoute } from "./CompanyAccess";
const listMembersMock = vi.hoisted(() => vi.fn());
const listJoinRequestsMock = vi.hoisted(() => vi.fn());
-const updateMemberAccessMock = vi.hoisted(() => vi.fn());
+const updateMemberMock = vi.hoisted(() => vi.fn());
const archiveMemberMock = vi.hoisted(() => vi.fn());
const listAgentsMock = vi.hoisted(() => vi.fn());
const listIssuesMock = vi.hoisted(() => vi.fn());
+const mockUsePluginSlots = vi.hoisted(() => vi.fn());
+const mockNavigate = vi.hoisted(() => vi.fn());
vi.mock("@/api/access", () => ({
accessApi: {
listMembers: (companyId: string) => listMembersMock(companyId),
listJoinRequests: (companyId: string, status: string) => listJoinRequestsMock(companyId, status),
- updateMember: vi.fn(),
+ updateMember: (companyId: string, memberId: string, input: unknown) =>
+ updateMemberMock(companyId, memberId, input),
updateMemberPermissions: vi.fn(),
- updateMemberAccess: (companyId: string, memberId: string, input: unknown) =>
- updateMemberAccessMock(companyId, memberId, input),
+ updateMemberAccess: vi.fn(),
archiveMember: (companyId: string, memberId: string, input: unknown) =>
archiveMemberMock(companyId, memberId, input),
approveJoinRequest: vi.fn(),
@@ -40,6 +42,18 @@ vi.mock("@/api/issues", () => ({
},
}));
+vi.mock("@/lib/router", () => ({
+ Link: ({ to, children }: { to: string; children: React.ReactNode }) => {children} ,
+ Navigate: ({ to, replace }: { to: string; replace?: boolean }) => {
+ mockNavigate(to, replace);
+ return
{to}
;
+ },
+}));
+
+vi.mock("@/plugins/slots", () => ({
+ usePluginSlots: mockUsePluginSlots,
+}));
+
vi.mock("@/context/CompanyContext", () => ({
useCompany: () => ({
selectedCompanyId: "company-1",
@@ -146,7 +160,7 @@ describe("CompanyAccess", () => {
},
},
]);
- updateMemberAccessMock.mockResolvedValue({});
+ updateMemberMock.mockResolvedValue({});
archiveMemberMock.mockResolvedValue({ reassignedIssueCount: 1 });
listAgentsMock.mockResolvedValue([
{
@@ -164,6 +178,11 @@ describe("CompanyAccess", () => {
status: "todo",
},
]);
+ mockUsePluginSlots.mockReturnValue({
+ slots: [],
+ isLoading: false,
+ errorMessage: null,
+ });
});
afterEach(() => {
@@ -172,7 +191,7 @@ describe("CompanyAccess", () => {
vi.clearAllMocks();
});
- it("keeps the page human-focused and explains implicit versus explicit grants", async () => {
+ it("keeps the page human-focused and hides advanced permission controls", async () => {
const root = createRoot(container);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
@@ -188,10 +207,15 @@ describe("CompanyAccess", () => {
await flushReact();
await flushReact();
- expect(container.textContent).toContain("Manage company user memberships");
+ expect(container.textContent).toContain("Manage the people who can work in Paperclip");
+ expect(container.textContent).toContain("Members can collaborate across the company by default");
+ expect(container.textContent).toContain("Advanced permission controls are available in Paperclip EE");
expect(container.textContent).toContain("Humans");
expect(container.textContent).toContain("Pending human joins");
expect(container.textContent).toContain("User account");
+ expect(container.textContent).not.toContain("Grants");
+ expect(container.textContent).not.toContain("explicit grants");
+ expect(container.textContent).not.toContain("Assign scoped tasks");
expect(container.textContent).not.toContain("Agents");
expect(container.textContent).not.toContain("Pending agent joins");
expect(container.textContent).not.toContain("Open join request queue");
@@ -210,18 +234,16 @@ describe("CompanyAccess", () => {
});
await flushReact();
- expect(document.body.textContent).toContain("Implicit grants from role");
- expect(document.body.textContent).toContain("Owner currently includes these permissions automatically.");
- expect(document.body.textContent).toContain(
- "Included implicitly by the Owner role. Add an explicit grant only if it should stay after the role changes.",
- );
+ expect(document.body.textContent).toContain("Update company role and membership status");
+ expect(document.body.textContent).not.toContain("Implicit grants from role");
+ expect(document.body.textContent).not.toContain("permissionKey");
await act(async () => {
root.unmount();
});
});
- it("saves member role, status, and grants in one request", async () => {
+ it("saves member role and status without touching grants", async () => {
const root = createRoot(container);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
@@ -248,7 +270,7 @@ describe("CompanyAccess", () => {
await flushReact();
const saveButton = Array.from(document.body.querySelectorAll("button")).find(
- (button) => button.textContent === "Save access",
+ (button) => button.textContent === "Save member",
);
expect(saveButton).toBeTruthy();
@@ -257,10 +279,9 @@ describe("CompanyAccess", () => {
});
await flushReact();
- expect(updateMemberAccessMock).toHaveBeenCalledWith("company-1", "member-1", {
+ expect(updateMemberMock).toHaveBeenCalledWith("company-1", "member-1", {
membershipRole: "owner",
status: "active",
- grants: [],
});
await act(async () => {
@@ -382,4 +403,65 @@ describe("CompanyAccess", () => {
root.unmount();
});
});
+
+ it("redirects legacy access deep links to the EE permissions route when installed", async () => {
+ mockUsePluginSlots.mockReturnValue({
+ slots: [
+ {
+ type: "companySettingsPage",
+ id: "permissions",
+ displayName: "Permissions",
+ routePath: "permissions",
+ pluginKey: "paperclip-ee",
+ },
+ ],
+ isLoading: false,
+ errorMessage: null,
+ });
+ const root = createRoot(container);
+ const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+ });
+
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ await flushReact();
+
+ expect(mockNavigate).toHaveBeenCalledWith("/company/settings/permissions", true);
+ expect(container.textContent).toContain("/company/settings/permissions");
+
+ await act(async () => {
+ root.unmount();
+ });
+ });
+
+ it("shows a read-only EE unavailable fallback for legacy access deep links", async () => {
+ const root = createRoot(container);
+ const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+ });
+
+ await act(async () => {
+ root.render(
+
+
+ ,
+ );
+ });
+ await flushReact();
+
+ expect(container.textContent).toContain("Advanced Permissions");
+ expect(container.textContent).toContain("Paperclip EE unavailable");
+ expect(container.textContent).toContain("Open Members");
+ expect(container.textContent).toContain("Open Invites");
+
+ await act(async () => {
+ root.unmount();
+ });
+ });
});
diff --git a/ui/src/pages/CompanyAccess.tsx b/ui/src/pages/CompanyAccess.tsx
index 3f31fe1ef5b..0834e8c0266 100644
--- a/ui/src/pages/CompanyAccess.tsx
+++ b/ui/src/pages/CompanyAccess.tsx
@@ -2,17 +2,14 @@ import { useEffect, useMemo, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
HUMAN_COMPANY_MEMBERSHIP_ROLE_LABELS,
- PERMISSION_KEYS,
type Agent,
- type PermissionKey,
} from "@paperclipai/shared";
-import { ShieldCheck, Trash2, Users } from "lucide-react";
+import { Shield, ShieldCheck, Trash2, Users } from "lucide-react";
import { accessApi, type CompanyMember } from "@/api/access";
import { agentsApi } from "@/api/agents";
import { ApiError } from "@/api/client";
import { issuesApi } from "@/api/issues";
import { Button } from "@/components/ui/button";
-import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
@@ -25,38 +22,13 @@ import { Badge } from "@/components/ui/badge";
import { useBreadcrumbs } from "@/context/BreadcrumbContext";
import { useCompany } from "@/context/CompanyContext";
import { useToast } from "@/context/ToastContext";
+import { Link, Navigate } from "@/lib/router";
import { queryKeys } from "@/lib/queryKeys";
-
-const permissionLabels: Record = {
- "agents:create": "Create agents",
- "users:invite": "Invite humans and agents",
- "users:manage_permissions": "Manage members and grants",
- "tasks:assign": "Assign tasks",
- "tasks:assign_scope": "Assign scoped tasks",
- "tasks:manage_active_checkouts": "Manage active task checkouts",
- "joins:approve": "Approve join requests",
- "environments:manage": "Manage environments",
-};
-
-function formatGrantSummary(member: CompanyMember) {
- if (member.grants.length === 0) return "No explicit grants";
- return member.grants.map((grant) => permissionLabels[grant.permissionKey]).join(", ");
-}
-
-const implicitRoleGrantMap: Record, PermissionKey[]> = {
- owner: ["agents:create", "users:invite", "users:manage_permissions", "tasks:assign", "joins:approve"],
- admin: ["agents:create", "users:invite", "tasks:assign", "joins:approve"],
- operator: ["tasks:assign"],
- viewer: [],
-};
+import { usePluginSlots } from "@/plugins/slots";
const reassignmentIssueStatuses = "backlog,todo,in_progress,in_review,blocked,failed,timed_out";
type EditableMemberStatus = "pending" | "active" | "suspended";
-function getImplicitGrantKeys(role: CompanyMember["membershipRole"]) {
- return role ? implicitRoleGrantMap[role] : [];
-}
-
export function CompanyAccess() {
const { selectedCompany, selectedCompanyId } = useCompany();
const { setBreadcrumbs } = useBreadcrumbs();
@@ -67,13 +39,12 @@ export function CompanyAccess() {
const [reassignmentTarget, setReassignmentTarget] = useState("__unassigned");
const [draftRole, setDraftRole] = useState(null);
const [draftStatus, setDraftStatus] = useState("active");
- const [draftGrants, setDraftGrants] = useState>(new Set());
useEffect(() => {
setBreadcrumbs([
{ label: selectedCompany?.name ?? "Company", href: "/dashboard" },
{ label: "Settings", href: "/company/settings" },
- { label: "Access" },
+ { label: "Members" },
]);
}, [selectedCompany?.name, setBreadcrumbs]);
@@ -103,11 +74,10 @@ export function CompanyAccess() {
};
const updateMemberMutation = useMutation({
- mutationFn: async (input: { memberId: string; membershipRole: CompanyMember["membershipRole"]; status: EditableMemberStatus; grants: PermissionKey[] }) => {
- return accessApi.updateMemberAccess(selectedCompanyId!, input.memberId, {
+ mutationFn: async (input: { memberId: string; membershipRole: CompanyMember["membershipRole"]; status: EditableMemberStatus }) => {
+ return accessApi.updateMember(selectedCompanyId!, input.memberId, {
membershipRole: input.membershipRole,
status: input.status,
- grants: input.grants.map((permissionKey) => ({ permissionKey })),
});
},
onSuccess: async () => {
@@ -223,7 +193,6 @@ export function CompanyAccess() {
if (!editingMember) return;
setDraftRole(editingMember.membershipRole);
setDraftStatus(isEditableMemberStatus(editingMember.status) ? editingMember.status : "suspended");
- setDraftGrants(new Set(editingMember.grants.map((grant) => grant.permissionKey)));
}, [editingMember]);
useEffect(() => {
@@ -255,8 +224,6 @@ export function CompanyAccess() {
joinRequestsQuery.data?.filter((request) => request.requestType === "human") ?? [];
const joinRequestActionPending =
approveJoinRequestMutation.isPending || rejectJoinRequestMutation.isPending;
- const implicitGrantKeys = getImplicitGrantKeys(draftRole);
- const implicitGrantSet = new Set(implicitGrantKeys);
const activeReassignmentUsers = members.filter(
(member) =>
member.status === "active" &&
@@ -271,11 +238,14 @@ export function CompanyAccess() {
-
Company Access
+ Company Members
- Manage company user memberships, membership status, and explicit permission grants for {selectedCompany?.name}.
+ Manage the people who can work in {selectedCompany?.name}. Members can collaborate across the company by default.
+
+ Advanced permission controls are available in Paperclip EE. Core keeps this page focused on membership, invite approvals, and safe member removal.
+
{access && !access.currentUserRole && (
@@ -291,7 +261,7 @@ export function CompanyAccess() {
Humans
- Manage human company memberships, status, and grants here.
+ Manage human company memberships and status here.
@@ -340,11 +310,10 @@ export function CompanyAccess() {
) : null}
-
+
User account
Role
Status
-
Grants
Action
{members.length === 0 ? (
@@ -356,7 +325,7 @@ export function CompanyAccess() {
return (
{member.user?.name?.trim() || member.user?.email || member.principalId}
@@ -372,7 +341,6 @@ export function CompanyAccess() {
{member.status.replace("_", " ")}
-
{formatGrantSummary(member)}
setEditingMemberId(member.id)}>
@@ -405,7 +373,7 @@ export function CompanyAccess() {
Edit member
- Update company role, membership status, and explicit grants for {editingMember?.user?.name || editingMember?.user?.email || editingMember?.principalId}.
+ Update company role and membership status for {editingMember?.user?.name || editingMember?.user?.email || editingMember?.principalId}.
{editingMember && (
@@ -443,66 +411,6 @@ export function CompanyAccess() {
-
-
-
-
Grants
-
- Roles provide implicit grants automatically. Explicit grants below are only for overrides and extra access that should persist even if the role changes.
-
-
-
-
Implicit grants from role
-
- {draftRole
- ? `${HUMAN_COMPANY_MEMBERSHIP_ROLE_LABELS[draftRole]} currently includes these permissions automatically.`
- : "No role is selected, so this member has no implicit grants right now."}
-
- {implicitGrantKeys.length > 0 ? (
-
- {implicitGrantKeys.map((permissionKey) => (
-
- {permissionLabels[permissionKey]}
-
- ))}
-
- ) : null}
-
-
- {PERMISSION_KEYS.map((permissionKey) => (
-
- {
- setDraftGrants((current) => {
- const next = new Set(current);
- if (checked) next.add(permissionKey);
- else next.delete(permissionKey);
- return next;
- });
- }}
- />
-
- {permissionLabels[permissionKey]}
- {permissionKey}
- {implicitGrantSet.has(permissionKey) ? (
-
- Included implicitly by the {draftRole ? HUMAN_COMPANY_MEMBERSHIP_ROLE_LABELS[draftRole] : "selected"} role. Add an explicit grant only if it should stay after the role changes.
-
- ) : null}
- {draftGrants.has(permissionKey) ? (
-
- Stored explicitly for this member.
-
- ) : null}
-
-
- ))}
-
-
)}
@@ -516,12 +424,11 @@ export function CompanyAccess() {
memberId: editingMember.id,
membershipRole: draftRole,
status: draftStatus,
- grants: [...draftGrants],
});
}}
disabled={updateMemberMutation.isPending}
>
- {updateMemberMutation.isPending ? "Saving…" : "Save access"}
+ {updateMemberMutation.isPending ? "Saving…" : "Save member"}
@@ -616,6 +523,66 @@ export function CompanyAccess() {
);
}
+export function CompanyAccessLegacyRoute() {
+ const { selectedCompanyId } = useCompany();
+ const { setBreadcrumbs } = useBreadcrumbs();
+ const { slots, isLoading, errorMessage } = usePluginSlots({
+ slotTypes: ["companySettingsPage"],
+ companyId: selectedCompanyId,
+ enabled: !!selectedCompanyId,
+ });
+
+ useEffect(() => {
+ setBreadcrumbs([
+ { label: "Settings", href: "/company/settings" },
+ { label: "Access" },
+ ]);
+ }, [setBreadcrumbs]);
+
+ const permissionsSlot = slots.find((slot) => slot.routePath === "permissions");
+ if (permissionsSlot) {
+ return
;
+ }
+
+ if (isLoading) {
+ return
Checking for Paperclip EE permissions...
;
+ }
+
+ return (
+
+
+
+
+
Advanced Permissions
+
+
+ Advanced access, scoped assignment, and explicit grant controls are now owned by the Paperclip EE Permissions plugin.
+
+
+
+
+
+
Paperclip EE unavailable
+
+ Core Paperclip keeps enforcing company boundaries and any existing restrictive policy data, but editing advanced permissions requires Paperclip EE.
+
+ {errorMessage ? (
+
Plugin extensions unavailable: {errorMessage}
+ ) : null}
+
+
+
+ Open Members
+
+
+ Open Invites
+
+
+
+
+ );
+}
+
function memberDisplayName(member: CompanyMember | null) {
if (!member) return "this member";
return member.user?.name?.trim() || member.user?.email || member.principalId;
diff --git a/ui/src/pages/CompanyInvites.test.tsx b/ui/src/pages/CompanyInvites.test.tsx
index 03937030a7d..27961dbc73c 100644
--- a/ui/src/pages/CompanyInvites.test.tsx
+++ b/ui/src/pages/CompanyInvites.test.tsx
@@ -152,7 +152,8 @@ describe("CompanyInvites", () => {
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.");
+ expect(container.textContent).toContain("Everything in Admin, plus managing members.");
+ expect(container.textContent).not.toContain("permission grants");
expect(listInvitesMock).toHaveBeenCalledWith("company-1", { limit: 5, offset: 0 });
const viewMoreButton = Array.from(container.querySelectorAll("button")).find(
diff --git a/ui/src/pages/CompanyInvites.tsx b/ui/src/pages/CompanyInvites.tsx
index 0d52bf314dc..f20ee636b3a 100644
--- a/ui/src/pages/CompanyInvites.tsx
+++ b/ui/src/pages/CompanyInvites.tsx
@@ -14,8 +14,8 @@ const inviteRoleOptions = [
{
value: "viewer",
label: "Viewer",
- description: "Can view company work and follow along without operational permissions.",
- gets: "No built-in grants.",
+ description: "Can view company work and follow along.",
+ gets: "View-only company membership.",
},
{
value: "operator",
@@ -32,8 +32,8 @@ const inviteRoleOptions = [
{
value: "owner",
label: "Owner",
- description: "Full company access, including membership and permission management.",
- gets: "Everything in Admin, plus managing members and permission grants.",
+ description: "Full company access, including membership management.",
+ gets: "Everything in Admin, plus managing members.",
},
] as const;
diff --git a/ui/src/pages/InviteLanding.test.tsx b/ui/src/pages/InviteLanding.test.tsx
index 0fe56c2dc2f..a800bd6f036 100644
--- a/ui/src/pages/InviteLanding.test.tsx
+++ b/ui/src/pages/InviteLanding.test.tsx
@@ -403,16 +403,16 @@ describe("InviteLandingPage", () => {
expect(container.textContent).toContain("Request to join Acme Robotics");
expect(container.textContent).toContain("A company admin must approve your request to join.");
expect(container.textContent).toContain(
- "Ask them to visit Company Settings → Access to approve your request.",
+ "Ask them to visit Company Settings → Members to approve your request.",
);
expect(container.querySelector('img[alt="Acme Robotics logo"]')).not.toBeNull();
- expect(container.textContent).not.toContain("http://localhost/company/settings/access");
+ expect(container.textContent).not.toContain("http://localhost/company/settings/members");
const approvalLinks = Array.from(container.querySelectorAll("a")).filter(
- (link) => link.textContent === "Company Settings → Access",
+ (link) => link.textContent === "Company Settings → Members",
);
expect(approvalLinks).toHaveLength(2);
- const expectedApprovalUrl = `${window.location.origin}/company/settings/access`;
+ const expectedApprovalUrl = `${window.location.origin}/company/settings/members`;
for (const link of approvalLinks) {
expect(link.getAttribute("href")).toBe(expectedApprovalUrl);
}
@@ -471,7 +471,7 @@ describe("InviteLandingPage", () => {
expect(container.querySelector('[data-testid="invite-pending-approval"]')).not.toBeNull();
expect(container.textContent).toContain("Your request is still awaiting approval.");
expect(container.textContent).toContain(
- "Ask them to visit Company Settings → Access to approve your request.",
+ "Ask them to visit Company Settings → Members to approve your request.",
);
await act(async () => {
diff --git a/ui/src/pages/InviteLanding.tsx b/ui/src/pages/InviteLanding.tsx
index 7dcef629b3f..cb394505900 100644
--- a/ui/src/pages/InviteLanding.tsx
+++ b/ui/src/pages/InviteLanding.tsx
@@ -160,7 +160,7 @@ function AwaitingJoinApprovalPanel({
claimApiKeyPath = null,
onboardingTextUrl = null,
}: AwaitingJoinApprovalPanelProps) {
- const approvalUrl = `${window.location.origin}/company/settings/access`;
+ const approvalUrl = `${window.location.origin}/company/settings/members`;
const approverLabel = invitedByUserName ?? "A company admin";
return (
@@ -185,11 +185,11 @@ function AwaitingJoinApprovalPanel({
href={approvalUrl}
className="text-sm text-zinc-200 underline underline-offset-2 hover:text-zinc-100"
>
- Company Settings → Access
+ Company Settings → Members
- Ask them to visit Company Settings → Access to approve your request.
+ Ask them to visit Company Settings → Members to approve your request.
Refresh this page after you've been approved — you'll be redirected automatically.
diff --git a/ui/src/pages/InviteUxLab.tsx b/ui/src/pages/InviteUxLab.tsx
index 941a4875512..9a356d0a27d 100644
--- a/ui/src/pages/InviteUxLab.tsx
+++ b/ui/src/pages/InviteUxLab.tsx
@@ -23,8 +23,8 @@ const inviteRoleOptions = [
{
value: "viewer",
label: "Viewer",
- description: "Can view company work and follow along without operational permissions.",
- gets: "No built-in grants.",
+ description: "Can view company work and follow along.",
+ gets: "View-only company membership.",
},
{
value: "operator",
@@ -41,8 +41,8 @@ const inviteRoleOptions = [
{
value: "owner",
label: "Owner",
- description: "Full company access, including membership and permission management.",
- gets: "Everything in Admin, plus managing members and permission grants.",
+ description: "Full company access, including membership management.",
+ gets: "Everything in Admin, plus managing members.",
},
] as const;
@@ -423,8 +423,8 @@ function InviteResultPreview({
<>
@@ -897,7 +897,7 @@ export function InviteUxLab() {
/>