Merge pull request #25808 from BerriAI/litellm_sendInviteEmailToggle

[Feature] UI - Users: Add Send Invitation Email Toggle
This commit is contained in:
yuneng-jiang 2026-04-24 17:40:14 -07:00 • committed by GitHub
commit 91d6626254
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 142 additions and 3 deletions

View file

@ -354,4 +354,126 @@ describe("CreateUserButton", () => {
expect(mockOrganizationMemberAddCall).not.toHaveBeenCalled();
});
});
describe("send invitation email toggle", () => {
it("should send send_invite_email true by default in embedded mode", async () => {
const user = userEvent.setup();
mockUserCreateCall.mockResolvedValue({ data: { user_id: "default-on-user" } });
renderWithProviders(
<CreateUserButton {...defaultProps} possibleUIRoles={{ proxy_user: { ui_label: "User", description: "" } }} isEmbedded />,
);
await user.type(screen.getByLabelText(/user email/i), "default@example.com");
await user.click(screen.getByRole("combobox", { name: /user role/i }));
await user.click(screen.getByText("User"));
await user.click(screen.getByRole("button", { name: /create user/i }));
await waitFor(() => {
expect(mockUserCreateCall).toHaveBeenCalledWith("token", null, expect.objectContaining({
send_invite_email: true,
}));
});
});
it("should send send_invite_email false when the checkbox is unchecked in embedded mode", async () => {
const user = userEvent.setup();
mockUserCreateCall.mockResolvedValue({ data: { user_id: "unchecked-user" } });
renderWithProviders(
<CreateUserButton {...defaultProps} possibleUIRoles={{ proxy_user: { ui_label: "User", description: "" } }} isEmbedded />,
);
await user.type(screen.getByLabelText(/user email/i), "off@example.com");
await user.click(screen.getByRole("combobox", { name: /user role/i }));
await user.click(screen.getByText("User"));
await user.click(screen.getByRole("checkbox", { name: /send invitation email/i }));
await user.click(screen.getByRole("button", { name: /create user/i }));
await waitFor(() => {
expect(mockUserCreateCall).toHaveBeenCalledWith("token", null, expect.objectContaining({
send_invite_email: false,
}));
});
});
it("should send send_invite_email true by default in standalone mode", async () => {
const user = userEvent.setup();
mockUserCreateCall.mockResolvedValue({ data: { user_id: "standalone-default-user" } });
mockInvitationCreateCall.mockResolvedValue({
id: "inv-default",
user_id: "standalone-default-user",
has_user_setup_sso: false,
} as any);
renderWithProviders(
<CreateUserButton {...defaultProps} possibleUIRoles={{ proxy_user: { ui_label: "User", description: "" } }} />,
);
await waitFor(() => {
expect(screen.getByRole("button", { name: /\+ invite user/i })).toBeInTheDocument();
});
await user.click(screen.getByRole("button", { name: /\+ invite user/i }));
const dialog = screen.getByRole("dialog", { name: /invite user/i });
await user.type(within(dialog).getByLabelText(/user email/i), "standalone-default@example.com");
await user.click(within(dialog).getByRole("combobox", { name: /global proxy role/i }));
await user.click(screen.getByText("User"));
await user.click(within(dialog).getByRole("button", { name: /invite user/i }));
await waitFor(() => {
expect(mockUserCreateCall).toHaveBeenCalledWith("token", null, expect.objectContaining({
send_invite_email: true,
}));
});
});
it("should send send_invite_email false when the checkbox is unchecked in standalone mode", async () => {
const user = userEvent.setup();
mockUserCreateCall.mockResolvedValue({ data: { user_id: "standalone-off-user" } });
mockInvitationCreateCall.mockResolvedValue({
id: "inv-off",
user_id: "standalone-off-user",
has_user_setup_sso: false,
} as any);
renderWithProviders(
<CreateUserButton {...defaultProps} possibleUIRoles={{ proxy_user: { ui_label: "User", description: "" } }} />,
);
await waitFor(() => {
expect(screen.getByRole("button", { name: /\+ invite user/i })).toBeInTheDocument();
});
await user.click(screen.getByRole("button", { name: /\+ invite user/i }));
const dialog = screen.getByRole("dialog", { name: /invite user/i });
await user.type(within(dialog).getByLabelText(/user email/i), "standalone-off@example.com");
await user.click(within(dialog).getByRole("combobox", { name: /global proxy role/i }));
await user.click(screen.getByText("User"));
await user.click(within(dialog).getByRole("checkbox", { name: /send invitation email/i }));
await user.click(within(dialog).getByRole("button", { name: /invite user/i }));
await waitFor(() => {
expect(mockUserCreateCall).toHaveBeenCalledWith("token", null, expect.objectContaining({
send_invite_email: false,
}));
});
});
it("should keep the checkbox checked by default when the modal is opened in standalone mode", async () => {
const user = userEvent.setup();
renderWithProviders(
<CreateUserButton {...defaultProps} possibleUIRoles={{ proxy_user: { ui_label: "User", description: "" } }} />,
);
await waitFor(() => {
expect(screen.getByRole("button", { name: /\+ invite user/i })).toBeInTheDocument();
});
await user.click(screen.getByRole("button", { name: /\+ invite user/i }));
const dialog = screen.getByRole("dialog", { name: /invite user/i });
expect(within(dialog).getByRole("checkbox", { name: /send invitation email/i })).toBeChecked();
});
});
});

View file

@ -2,7 +2,7 @@ import { InfoCircleOutlined, UserAddOutlined } from "@ant-design/icons";
import { useQueryClient } from "@tanstack/react-query";
import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
import { Accordion, AccordionBody, AccordionHeader, SelectItem, TextInput } from "@tremor/react";
import { Alert, Button, Form, Input, Modal, Select, Select as Select2, Space, Tooltip, Typography } from "antd";
import { Alert, Button, Checkbox, Form, Input, Modal, Select, Select as Select2, Space, Tooltip, Typography } from "antd";
import React, { useEffect, useMemo, useState } from "react";
import BulkCreateUsers from "./bulk_create_users_button";
import TeamDropdown from "./common_components/team_dropdown";
@ -113,6 +113,7 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
user_role: string;
organization_ids?: string[];
organizations?: string[];
send_invite_email?: boolean;
}) => {
try {
NotificationsManager.info("Making API Call");
@ -181,7 +182,7 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
labelAlign="left"
initialValues={{ user_role: "internal_user_viewer" }}
initialValues={{ user_role: "internal_user_viewer", send_invite_email: true }}
>
<Alert
message="Email invitations"
@ -224,6 +225,14 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
<Input.TextArea rows={4} placeholder="Enter metadata as JSON" />
</Form.Item>
<Form.Item
label="Send invitation email"
name="send_invite_email"
valuePropName="checked"
>
<Checkbox />
</Form.Item>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button htmlType="submit">Create User</Button>
</div>
@ -270,7 +279,7 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
labelAlign="left"
initialValues={{ user_role: "internal_user_viewer" }}
initialValues={{ user_role: "internal_user_viewer", send_invite_email: true }}
>
<Form.Item label="User Email" name="user_email">
<Input />
@ -326,6 +335,13 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
<Form.Item label="Metadata" name="metadata">
<Input.TextArea rows={4} placeholder="Enter metadata as JSON" />
</Form.Item>
<Form.Item
label="Send invitation email"
name="send_invite_email"
valuePropName="checked"
>
<Checkbox />
</Form.Item>
<Accordion>
<AccordionHeader>
<Text strong>Personal Key Creation</Text>
@ -360,6 +376,7 @@ export const CreateUserButton: React.FC<CreateuserProps> = ({
</Form.Item>
</AccordionBody>
</Accordion>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button type="primary" icon={<UserAddOutlined />} htmlType="submit">
Invite User