refactor(ui): decouple bulk invite from the invite user button

The bulk invite button was rendered from inside CreateUserButton, so the two
actions were locked together and only the bulk one had been migrated, leaving
the users page with an antd primary button sitting next to a shadcn one.

Move BulkCreateUsersButton up to the users page toolbar so each action stands
on its own, and migrate CreateUserButton's buttons to the shared shadcn Button
so both triggers render identically. The teams prop only ever fed the bulk
button, so it goes away from CreateUserButton and its other call site.
This commit is contained in:
Yuneng Jiang 2026-08-15 15:39:50 -07:00
parent 4eadf92ade
commit ff3da21aab
No known key found for this signature in database
5 changed files with 37 additions and 37 deletions

View file

@ -148,12 +148,25 @@ describe("ViewUserDashboard", () => {
expect(screen.getByRole("textbox", { name: "Default setting" })).toHaveValue("unsaved change");
});
it("renders invite and bulk invite as toolbar actions alongside the other admin controls", async () => {
renderDashboard();
const inviteButton = await screen.findByRole("button", { name: /\+ invite user/i });
const bulkInviteButton = screen.getByRole("button", { name: /\+ bulk invite users/i });
const toolbar = screen.getByTestId("toggle-user-selection").parentElement;
expect(inviteButton.parentElement).toBe(toolbar);
expect(bulkInviteButton.parentElement).toBe(toolbar);
});
it("shows the users table without admin controls for non-proxy admins", async () => {
renderDashboard({ userRole: "Internal User" });
expect(await screen.findByText("test@example.com")).toBeInTheDocument();
expect(screen.queryByRole("tab")).not.toBeInTheDocument();
expect(screen.queryByTestId("toggle-user-selection")).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /\+ invite user/i })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /\+ bulk invite users/i })).not.toBeInTheDocument();
});
it("keeps actions unavailable while the user list is loading", () => {

View file

@ -2,6 +2,7 @@ import { parseAsString, useQueryState } from "nuqs";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import BulkEditUserModal from "./BulkEditUsers";
import BulkCreateUsersButton from "@/components/bulk_create_users_button";
import { CreateUserButton } from "@/components/CreateUserButton";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
@ -365,12 +366,11 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
{!userListQuery.isLoading && userID && accessToken && (
<>
{isProxyAdmin && (
<CreateUserButton
userID={userID}
accessToken={accessToken}
teams={teams}
possibleUIRoles={possibleUIRoles}
/>
<CreateUserButton userID={userID} accessToken={accessToken} possibleUIRoles={possibleUIRoles} />
)}
{isProxyAdmin && (
<BulkCreateUsersButton accessToken={accessToken} teams={teams} possibleUIRoles={possibleUIRoles} />
)}
{isProxyAdmin && (

View file

@ -20,10 +20,6 @@ vi.mock("./networking", () => ({
getProxyBaseUrl: vi.fn().mockReturnValue("http://localhost"),
}));
vi.mock("./bulk_create_users_button", () => ({
default: () => <div data-testid="bulk-create-users">Bulk Create Users</div>,
}));
vi.mock("@/app/(dashboard)/hooks/organizations/useOrganizations", () => ({
useOrganizations: vi.fn().mockReturnValue({ data: [], isLoading: false }),
}));
@ -42,7 +38,6 @@ const createQueryClient = () =>
const defaultProps = {
userID: "123",
accessToken: "token",
teams: [],
possibleUIRoles: null as Record<string, Record<string, string>> | null,
};
@ -75,6 +70,14 @@ describe("CreateUserButton", () => {
});
});
it("should not render the bulk invite button", async () => {
renderWithProviders(<CreateUserButton {...defaultProps} />);
await waitFor(() => {
expect(screen.getByRole("button", { name: /\+ invite user/i })).toBeInTheDocument();
});
expect(screen.queryByRole("button", { name: /bulk invite users/i })).not.toBeInTheDocument();
});
it("should open the invite modal when invite user button is clicked", async () => {
const user = userEvent.setup();
renderWithProviders(<CreateUserButton {...defaultProps} />);

View file

@ -1,22 +1,11 @@
import { InfoCircleOutlined, UserAddOutlined } from "@ant-design/icons";
import { InfoCircleOutlined } from "@ant-design/icons";
import { useQueryClient } from "@tanstack/react-query";
import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
import { Button } from "@/components/ui/button";
import { Accordion, AccordionBody, AccordionHeader, SelectItem, TextInput } from "@tremor/react";
import {
Alert,
Button,
Checkbox,
Form,
Input,
Modal,
Select,
Select as Select2,
Space,
Tooltip,
Typography,
} from "antd";
import { Alert, Checkbox, Form, Input, Modal, Select, Select as Select2, Space, Tooltip, Typography } from "antd";
import { UserPlus } from "lucide-react";
import React, { useEffect, useState } from "react";
import BulkCreateUsers from "./bulk_create_users_button";
import TeamDropdown from "./common_components/team_dropdown";
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
import NotificationsManager from "./molecules/notifications_manager";
@ -46,7 +35,6 @@ const generateUUID = (): string => {
interface CreateuserProps {
userID: string;
accessToken: string;
teams: any[] | null;
possibleUIRoles: null | Record<string, Record<string, string>>;
onUserCreated?: (userId: string) => void;
isEmbedded?: boolean;
@ -63,7 +51,6 @@ interface UISettings {
export const CreateUserButton: React.FC<CreateuserProps> = ({
userID,
accessToken,
teams,
possibleUIRoles,
onUserCreated,
isEmbedded = false,
@ -79,8 +66,6 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
const [baseUrl, setBaseUrl] = useState<string | null>(null);
const { data: organizations = [] } = useOrganizations();
// Derive teams from the user's organizations, falling back to the teams prop
useEffect(() => {
const fetchData = async () => {
try {
@ -237,7 +222,7 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
</Form.Item>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button htmlType="submit">Create User</Button>
<Button type="submit">Create User</Button>
</div>
</Form>
);
@ -245,11 +230,10 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
// Original return for standalone mode
return (
<div className="flex gap-2">
<Button type="primary" className="mb-0" onClick={() => setIsModalVisible(true)}>
<>
<Button type="button" onClick={() => setIsModalVisible(true)}>
+ Invite User
</Button>
<BulkCreateUsers accessToken={accessToken} teams={teams} possibleUIRoles={possibleUIRoles} />
<Modal
title="Invite User"
open={isModalVisible}
@ -377,7 +361,8 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
</Accordion>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button type="primary" icon={<UserAddOutlined />} htmlType="submit">
<Button type="submit">
<UserPlus />
Invite User
</Button>
</div>
@ -391,6 +376,6 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
invitationLinkData={invitationLinkData}
/>
)}
</div>
</>
);
};

View file

@ -1718,7 +1718,6 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey, autoOp
<CreateUserButton
userID={userID}
accessToken={accessToken}
teams={teams}
possibleUIRoles={possibleUIRoles}
onUserCreated={handleUserCreated}
isEmbedded={true}