diff --git a/frontend/__tests__/components/features/user/user-context-menu.test.tsx b/frontend/__tests__/components/features/user/user-context-menu.test.tsx index d02207b518..4ba98cdd86 100644 --- a/frontend/__tests__/components/features/user/user-context-menu.test.tsx +++ b/frontend/__tests__/components/features/user/user-context-menu.test.tsx @@ -54,7 +54,7 @@ describe("UserContextMenu", () => { }); it("should render the default context items for a user", () => { - renderUserContextMenu({ type: "user", onClose: vi.fn }); + renderUserContextMenu({ type: "member", onClose: vi.fn }); screen.getByTestId("org-selector"); screen.getByText("ACCOUNT_SETTINGS$LOGOUT"); @@ -83,7 +83,7 @@ describe("UserContextMenu", () => { }, }); - renderUserContextMenu({ type: "user", onClose: vi.fn }); + renderUserContextMenu({ type: "member", onClose: vi.fn }); // Wait for config to load and verify that navigation items are rendered (except organization-members/org which are filtered out) const expectedItems = SAAS_NAV_ITEMS.filter( @@ -99,14 +99,14 @@ describe("UserContextMenu", () => { }); it("should not display Organization Members menu item for regular users (filtered out)", () => { - renderUserContextMenu({ type: "user", onClose: vi.fn }); + renderUserContextMenu({ type: "member", onClose: vi.fn }); // Organization Members is filtered out from nav items for all users expect(screen.queryByText("Organization Members")).not.toBeInTheDocument(); }); it("should render a documentation link", () => { - renderUserContextMenu({ type: "user", onClose: vi.fn }); + renderUserContextMenu({ type: "member", onClose: vi.fn }); const docsLink = screen.getByText("SIDEBAR$DOCS").closest("a"); expect(docsLink).toHaveAttribute("href", "https://docs.openhands.dev"); @@ -131,7 +131,7 @@ describe("UserContextMenu", () => { }); it("should render OSS_NAV_ITEMS when in OSS mode", async () => { - renderUserContextMenu({ type: "user", onClose: vi.fn }); + renderUserContextMenu({ type: "member", onClose: vi.fn }); // Wait for the config to load and OSS nav items to appear await waitFor(() => { @@ -147,7 +147,7 @@ describe("UserContextMenu", () => { }); it("should not display Organization Members menu item in OSS mode", async () => { - renderUserContextMenu({ type: "user", onClose: vi.fn }); + renderUserContextMenu({ type: "member", onClose: vi.fn }); // Wait for the config to load await waitFor(() => { @@ -177,7 +177,7 @@ describe("UserContextMenu", () => { }, }); - renderUserContextMenu({ type: "user", onClose: vi.fn }); + renderUserContextMenu({ type: "member", onClose: vi.fn }); await waitFor(() => { // Other nav items should still be visible @@ -204,7 +204,7 @@ describe("UserContextMenu", () => { }, }); - renderUserContextMenu({ type: "user", onClose: vi.fn }); + renderUserContextMenu({ type: "member", onClose: vi.fn }); await waitFor(() => { expect( @@ -234,7 +234,7 @@ describe("UserContextMenu", () => { it("should call the logout handler when Logout is clicked", async () => { const logoutSpy = vi.spyOn(AuthService, "logout"); - renderUserContextMenu({ type: "user", onClose: vi.fn }); + renderUserContextMenu({ type: "member", onClose: vi.fn }); const logoutButton = screen.getByText("ACCOUNT_SETTINGS$LOGOUT"); await userEvent.click(logoutButton); @@ -257,7 +257,7 @@ describe("UserContextMenu", () => { }, }); - renderUserContextMenu({ type: "user", onClose: vi.fn }); + renderUserContextMenu({ type: "member", onClose: vi.fn }); // Wait for config to load and test a few representative nav items have the correct href await waitFor(() => { @@ -298,7 +298,7 @@ describe("UserContextMenu", () => { it("should call the onClose handler when clicking outside the context menu", async () => { const onCloseMock = vi.fn(); - renderUserContextMenu({ type: "user", onClose: onCloseMock }); + renderUserContextMenu({ type: "member", onClose: onCloseMock }); const contextMenu = screen.getByTestId("user-context-menu"); await userEvent.click(contextMenu); @@ -350,7 +350,7 @@ describe("UserContextMenu", () => { test("the user can change orgs", async () => { const onCloseMock = vi.fn(); - renderUserContextMenu({ type: "user", onClose: onCloseMock }); + renderUserContextMenu({ type: "member", onClose: onCloseMock }); const orgSelector = screen.getByTestId("org-selector"); expect(orgSelector).toBeInTheDocument(); @@ -369,7 +369,7 @@ describe("UserContextMenu", () => { it("should have Personal Account as the default selected option with null value", async () => { const onCloseMock = vi.fn(); - renderUserContextMenu({ type: "user", onClose: onCloseMock }); + renderUserContextMenu({ type: "member", onClose: onCloseMock }); const orgSelector = screen.getByTestId("org-selector"); diff --git a/frontend/__tests__/routes/manage-org.test.tsx b/frontend/__tests__/routes/manage-org.test.tsx index cfbf1d91ac..9501f5965d 100644 --- a/frontend/__tests__/routes/manage-org.test.tsx +++ b/frontend/__tests__/routes/manage-org.test.tsx @@ -95,7 +95,7 @@ describe("Manage Org Route", () => { org_id: string; user_id: string; email: string; - role: "owner" | "admin" | "user"; + role: "owner" | "admin" | "member"; llm_api_key: string; max_iterations: number; llm_model: string; @@ -680,9 +680,9 @@ describe("Manage Org Route", () => { expect(deleteButton).not.toBeDisabled(); }); - it.each<{ role: "admin" | "user"; roleName: string }>([ + it.each<{ role: "admin" | "member"; roleName: string }>([ { role: "admin", roleName: "Admin" }, - { role: "user", roleName: "User" }, + { role: "member", roleName: "Member" }, ])( "should not show delete organization button when user lacks canDeleteOrganization permission ($roleName role)", async ({ role }) => { diff --git a/frontend/__tests__/routes/manage-organization-members.test.tsx b/frontend/__tests__/routes/manage-organization-members.test.tsx index 4e39a15722..55f82a4d0a 100644 --- a/frontend/__tests__/routes/manage-organization-members.test.tsx +++ b/frontend/__tests__/routes/manage-organization-members.test.tsx @@ -39,7 +39,7 @@ const RouteStub = createRoutesStub([ }, { Component: () =>
, - path: "/settings/user", + path: "/settings/member", }, ], }, @@ -147,7 +147,7 @@ describe("Manage Organization Members Route", () => { org_id: string; user_id: string; email: string; - role: "owner" | "admin" | "user"; + role: "owner" | "admin" | "member"; llm_api_key: string; max_iterations: number; llm_model: string; @@ -173,7 +173,7 @@ describe("Manage Organization Members Route", () => { org_id: string; user_id: string; email: string; - role: "owner" | "admin" | "user"; + role: "owner" | "admin" | "member"; llm_api_key: string; max_iterations: number; llm_model: string; @@ -222,7 +222,7 @@ describe("Manage Organization Members Route", () => { const expectAllRoleOptionsPresent = (dropdown: HTMLElement) => { expect(within(dropdown).getByText(/owner/i)).toBeInTheDocument(); expect(within(dropdown).getByText(/admin/i)).toBeInTheDocument(); - expect(within(dropdown).getByText(/user/i)).toBeInTheDocument(); + expect(within(dropdown).getByText(/member/i)).toBeInTheDocument(); }; // Helper function to close dropdown by clicking outside @@ -305,11 +305,11 @@ describe("Manage Organization Members Route", () => { const memberListItems = await screen.findAllByTestId("member-item"); const userRoleMember = memberListItems[2]; // third member is "user" - let userCombobox = within(userRoleMember).getByText(/^User$/i); + let userCombobox = within(userRoleMember).getByText(/^Member$/i); expect(userCombobox).toBeInTheDocument(); // Change role from user to admin - await changeMemberRole(userRoleMember, "user", "admin"); + await changeMemberRole(userRoleMember, "member", "admin"); expect(updateMemberRoleSpy).toHaveBeenCalledExactlyOnceWith({ userId: "3", // assuming the third member is the one being updated @@ -323,16 +323,16 @@ describe("Manage Organization Members Route", () => { expect(userCombobox).toBeInTheDocument(); // Revert the role back to user - await changeMemberRole(userRoleMember, "admin", "user"); + await changeMemberRole(userRoleMember, "admin", "member"); expect(updateMemberRoleSpy).toHaveBeenNthCalledWith(2, { userId: "3", orgId: "1", - role: "user", + role: "member", }); // Verify the role has been reverted in the UI - userCombobox = within(userRoleMember).getByText(/^User$/i); + userCombobox = within(userRoleMember).getByText(/^Member$/i); expect(userCombobox).toBeInTheDocument(); }); @@ -410,7 +410,7 @@ describe("Manage Organization Members Route", () => { const userEmail = within(userRoleMember).getByText("charlie@acme.org"); expect(userEmail).toBeInTheDocument(); - const userCombobox = within(userRoleMember).getByText(/^User$/i); + const userCombobox = within(userRoleMember).getByText(/^Member$/i); await userEvent.click(userCombobox); const dropdown = within(userRoleMember).getByTestId( @@ -487,7 +487,7 @@ describe("Manage Organization Members Route", () => { org_id: "1", user_id: "4", email: "tom@acme.org", - role: "user", + role: "member", llm_api_key: "**********", max_iterations: 20, llm_model: "gpt-4", @@ -510,7 +510,7 @@ describe("Manage Organization Members Route", () => { expect(invitedBadge).toBeInTheDocument(); // should not have a role combobox - await userEvent.click(within(invitedMember).getByText(/^User$/i)); + await userEvent.click(within(invitedMember).getByText(/^Member$/i)); expect( within(invitedMember).queryByTestId( "organization-member-role-context-menu", @@ -553,7 +553,7 @@ describe("Manage Organization Members Route", () => { org_id: "1", user_id: "1", email: "test@example.com", - role: "user" as const, + role: "member" as const, llm_api_key: "**********", max_iterations: 20, llm_model: "gpt-4", @@ -638,7 +638,7 @@ describe("Manage Organization Members Route", () => { // Test with user member const userMember = await findMemberByEmail("charlie@acme.org"); - const userDropdown = await openRoleDropdown(userMember, "user"); + const userDropdown = await openRoleDropdown(userMember, "member"); // Verify all three role options are present for user member expectAllRoleOptionsPresent(userDropdown); @@ -665,7 +665,7 @@ describe("Manage Organization Members Route", () => { // Check user member dropdown const userMember = memberListItems[2]; // user member - const userDropdown = await openRoleDropdown(userMember, "user"); + const userDropdown = await openRoleDropdown(userMember, "member"); expectOwnerOptionNotPresent(userDropdown); await closeDropdown(); @@ -674,7 +674,7 @@ describe("Manage Organization Members Route", () => { const anotherUserMember = memberListItems[3]; // another user member const anotherUserDropdown = await openRoleDropdown( anotherUserMember, - "user", + "member", ); expectOwnerOptionNotPresent(anotherUserDropdown); } @@ -713,7 +713,7 @@ describe("Manage Organization Members Route", () => { // Test changing user to owner const userMember = await findMemberByEmail("charlie@acme.org"); - await changeMemberRole(userMember, "user", "owner"); + await changeMemberRole(userMember, "member", "owner"); expect(updateMemberRoleSpy).toHaveBeenNthCalledWith(2, { userId: "3", @@ -765,16 +765,16 @@ describe("Manage Organization Members Route", () => { }, orgIndex: 2, // org 3 memberEmail: "stephan@all-hands.dev", - currentRole: "user", - newRole: "user", + currentRole: "member", + newRole: "member", expectedApiCall: { userId: "9", orgId: "3", - role: "user" as const, + role: "member" as const, }, }, { - description: "Admin should be able to change user's role to admin", + description: "Admin should be able to change member's role to admin", userData: { org_id: "3", user_id: "7", // Ray is admin in org 3 @@ -789,7 +789,7 @@ describe("Manage Organization Members Route", () => { }, orgIndex: 2, // org 3 memberEmail: "stephan@all-hands.dev", - currentRole: "user", + currentRole: "member", newRole: "admin", expectedApiCall: { userId: "9", diff --git a/frontend/src/components/features/org/organization-member-list-item.tsx b/frontend/src/components/features/org/organization-member-list-item.tsx index fd5155a62d..4882bf5187 100644 --- a/frontend/src/components/features/org/organization-member-list-item.tsx +++ b/frontend/src/components/features/org/organization-member-list-item.tsx @@ -51,12 +51,14 @@ export function OrganizationMemberListItem({ > {email} + {status === "invited" && ( {t(I18nKey.ORG$STATUS_INVITED)} )}
+
} + {roleSelectionIsPermitted && contextMenuOpen && ( setContextMenuOpen(false)} diff --git a/frontend/src/components/features/org/organization-member-role-context-menu.tsx b/frontend/src/components/features/org/organization-member-role-context-menu.tsx index 960156d80e..84278035e6 100644 --- a/frontend/src/components/features/org/organization-member-role-context-menu.tsx +++ b/frontend/src/components/features/org/organization-member-role-context-menu.tsx @@ -94,15 +94,15 @@ export function OrganizationMemberRoleContextMenu({ /> )} - {availableRolesToChangeTo.includes("user") && ( + {availableRolesToChangeTo.includes("member") && ( handleRoleChangeClick(event, "user")} + testId="member-option" + onClick={(event) => handleRoleChangeClick(event, "member")} className={contextMenuListItemClassName} > } - text={t(I18nKey.ORG$ROLE_USER)} + text={t(I18nKey.ORG$ROLE_MEMBER)} className="capitalize" /> diff --git a/frontend/src/components/features/settings/settings-navigation.tsx b/frontend/src/components/features/settings/settings-navigation.tsx index 1c6bf20293..3f8eda1a9a 100644 --- a/frontend/src/components/features/settings/settings-navigation.tsx +++ b/frontend/src/components/features/settings/settings-navigation.tsx @@ -28,7 +28,7 @@ export function SettingsNavigation({ const { t } = useTranslation(); - const isUser = me?.role === "user"; + const isUser = me?.role === "member"; return ( <> diff --git a/frontend/src/components/features/sidebar/user-actions.tsx b/frontend/src/components/features/sidebar/user-actions.tsx index b30baebc3b..d5738f765f 100644 --- a/frontend/src/components/features/sidebar/user-actions.tsx +++ b/frontend/src/components/features/sidebar/user-actions.tsx @@ -52,7 +52,7 @@ export function UserActions({ user, isLoading }: UserActionsProps) { )} >
diff --git a/frontend/src/components/features/user/user-context-menu.tsx b/frontend/src/components/features/user/user-context-menu.tsx index b1fe8dc29c..5236480d74 100644 --- a/frontend/src/components/features/user/user-context-menu.tsx +++ b/frontend/src/components/features/user/user-context-menu.tsx @@ -52,7 +52,7 @@ export function UserContextMenu({ type, onClose }: UserContextMenuProps) { const [inviteMemberModalIsOpen, setInviteMemberModalIsOpen] = React.useState(false); - const isUser = type === "user"; + const isMember = type === "member"; const handleLogout = () => { logout(); @@ -120,7 +120,7 @@ export function UserContextMenu({ type, onClose }: UserContextMenuProps) { /> - {!isUser && ( + {!isMember && ( <> = { org_id: "1", user_id: "3", email: "charlie@acme.org", - role: "user", + role: "member", llm_api_key: "**********", max_iterations: 20, llm_model: "gpt-4", @@ -103,7 +103,7 @@ const INITIAL_MOCK_MEMBERS: Record = { org_id: "2", user_id: "4", email: "tony@gamma.org", - role: "user", + role: "member", llm_api_key: "**********", max_iterations: 20, llm_model: "gpt-4", @@ -153,7 +153,7 @@ const INITIAL_MOCK_MEMBERS: Record = { org_id: "3", user_id: "8", email: "chuck@all-hands.dev", - role: "user", + role: "member", llm_api_key: "**********", max_iterations: 20, llm_model: "gpt-4", @@ -165,7 +165,7 @@ const INITIAL_MOCK_MEMBERS: Record = { org_id: "3", user_id: "9", email: "stephan@all-hands.dev", - role: "user", + role: "member", llm_api_key: "**********", max_iterations: 20, llm_model: "gpt-4", @@ -177,7 +177,7 @@ const INITIAL_MOCK_MEMBERS: Record = { org_id: "3", user_id: "10", email: "tim@all-hands.dev", - role: "user", + role: "member", llm_api_key: "**********", max_iterations: 20, llm_model: "gpt-4", @@ -224,19 +224,19 @@ export const ORG_HANDLERS = [ ); } - let role: OrganizationUserRole = "user"; + let role: OrganizationUserRole = "member"; switch (orgId) { case "1": role = "owner"; break; case "2": - role = "user"; + role = "member"; break; case "3": role = "admin"; break; default: - role = "user"; + role = "member"; } const me: OrganizationMember = { @@ -430,7 +430,7 @@ export const ORG_HANDLERS = [ org_id: orgId, user_id: String(members.length + index + 1), email, - role: "user" as const, + role: "member" as const, llm_api_key: "**********", max_iterations: 20, llm_model: "gpt-4", diff --git a/frontend/src/routes/manage-org.tsx b/frontend/src/routes/manage-org.tsx index 1e2e89f60f..6ae4a47c2c 100644 --- a/frontend/src/routes/manage-org.tsx +++ b/frontend/src/routes/manage-org.tsx @@ -218,7 +218,7 @@ export const clientLoader = async () => { queryClient.setQueryData(["organizations", selectedOrgId, "me"], me); } - if (!me || me.role === "user") { + if (!me || me.role === "member") { // if user is USER role, redirect to user settings return redirect("/settings/user"); } diff --git a/frontend/src/routes/manage-organization-members.tsx b/frontend/src/routes/manage-organization-members.tsx index e95a362a6b..43c13a02b4 100644 --- a/frontend/src/routes/manage-organization-members.tsx +++ b/frontend/src/routes/manage-organization-members.tsx @@ -27,7 +27,7 @@ export const clientLoader = async () => { queryClient.setQueryData(["organizations", selectedOrgId, "me"], me); } - if (!me || me.role === "user") { + if (!me || me.role === "member") { // if user is USER role, redirect to user settings return redirect("/settings/user"); } @@ -44,7 +44,7 @@ function ManageOrganizationMembers() { const [inviteModalOpen, setInviteModalOpen] = React.useState(false); - const currentUserRole = user?.role || "user"; + const currentUserRole = user?.role || "member"; const hasPermissionToInvite = rolePermissions[currentUserRole].includes( "invite_user_to_organization", ); @@ -87,8 +87,8 @@ function ManageOrganizationMembers() { if (userPermissions.includes("change_user_role:admin")) { availableRoles.push("admin"); } - if (userPermissions.includes("change_user_role:user")) { - availableRoles.push("user"); + if (userPermissions.includes("change_user_role:member")) { + availableRoles.push("member"); } return availableRoles; diff --git a/frontend/src/types/org.ts b/frontend/src/types/org.ts index 8128d6cf81..0ad174ec48 100644 --- a/frontend/src/types/org.ts +++ b/frontend/src/types/org.ts @@ -1,4 +1,4 @@ -export type OrganizationUserRole = "user" | "admin" | "owner"; +export type OrganizationUserRole = "member" | "admin" | "owner"; export interface Organization { id: string; diff --git a/frontend/src/utils/org/permissions.ts b/frontend/src/utils/org/permissions.ts index d4090608d5..fcd01d88d3 100644 --- a/frontend/src/utils/org/permissions.ts +++ b/frontend/src/utils/org/permissions.ts @@ -24,18 +24,18 @@ const ownerPerms: UserPermission[] = [ "add_credits", "change_user_role:owner", "change_user_role:admin", - "change_user_role:user", + "change_user_role:member", ]; const adminPerms: UserPermission[] = [ "invite_user_to_organization", "add_credits", "change_user_role:admin", - "change_user_role:user", + "change_user_role:member", ]; const userPerms: UserPermission[] = []; export const rolePermissions: Record = { owner: ownerPerms, admin: adminPerms, - user: userPerms, + member: userPerms, };