mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
fix(ui): render team_metadata_schema keys as fixed labels
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
b6143b3711
commit
74bc75001b
2 changed files with 101 additions and 40 deletions
|
|
@ -212,17 +212,41 @@ describe("MetadataKeyValueFields with a declared schema", () => {
|
|||
{ key: "app_name", label: "Application Name" },
|
||||
];
|
||||
|
||||
it("should prepopulate one ordinary editable pair row per declared key", async () => {
|
||||
it("should render each declared key as a fixed label with only the value editable", async () => {
|
||||
render(<Harness onFinish={vi.fn()} schemaFields={schema} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByPlaceholderText("Key").map((input) => (input as HTMLInputElement).value)).toEqual([
|
||||
"cost_center",
|
||||
"app_name",
|
||||
expect(screen.getAllByTestId("metadata-schema-label").map((label) => label.textContent)).toEqual([
|
||||
"Cost Center",
|
||||
"Application Name",
|
||||
]);
|
||||
});
|
||||
screen.getAllByPlaceholderText("Key").forEach((input) => expect(input).toBeEnabled());
|
||||
expect(screen.getAllByLabelText("Remove key-value pair")).toHaveLength(2);
|
||||
expect(screen.queryByPlaceholderText("Key")).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Remove key-value pair")).not.toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Cost Center")).toHaveAttribute("placeholder", "Value");
|
||||
});
|
||||
|
||||
it("should fall back to the key when a declared field has no label", async () => {
|
||||
render(<Harness onFinish={vi.fn()} schemaFields={[{ key: "cost_center" }]} />);
|
||||
|
||||
expect(await screen.findByTestId("metadata-schema-label")).toHaveTextContent("cost_center");
|
||||
});
|
||||
|
||||
it("should treat an existing pair that matches a declared key as fixed too", async () => {
|
||||
render(
|
||||
<Harness
|
||||
onFinish={vi.fn()}
|
||||
schemaFields={[{ key: "cost_center", label: "Cost Center" }]}
|
||||
initialMetadata={[
|
||||
{ key: "cost_center", value: "CC-1001" },
|
||||
{ key: "region", value: "us" },
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(await screen.findByLabelText("Cost Center")).toHaveValue("CC-1001");
|
||||
expect(screen.getAllByPlaceholderText("Key").map((input) => (input as HTMLInputElement).value)).toEqual(["region"]);
|
||||
expect(screen.getAllByLabelText("Remove key-value pair")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should submit a prepopulated key with its typed value", async () => {
|
||||
|
|
@ -244,9 +268,9 @@ describe("MetadataKeyValueFields with a declared schema", () => {
|
|||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByPlaceholderText("Key").map((input) => (input as HTMLInputElement).value)).toEqual([
|
||||
"cost_center",
|
||||
"app_name",
|
||||
expect(screen.getAllByTestId("metadata-schema-label").map((label) => label.textContent)).toEqual([
|
||||
"Cost Center",
|
||||
"Application Name",
|
||||
]);
|
||||
});
|
||||
expect(screen.getAllByPlaceholderText("Value").map((input) => (input as HTMLInputElement).value)).toEqual([
|
||||
|
|
@ -255,18 +279,32 @@ describe("MetadataKeyValueFields with a declared schema", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it("should let the user remove a prepopulated row", async () => {
|
||||
it("should still let the user add and remove free-form pairs below the declared keys", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Harness onFinish={vi.fn()} schemaFields={schema} />);
|
||||
const onFinish = vi.fn();
|
||||
render(<Harness onFinish={onFinish} schemaFields={[{ key: "cost_center", label: "Cost Center" }]} />);
|
||||
|
||||
await screen.findAllByPlaceholderText("Key");
|
||||
await user.click(screen.getAllByLabelText("Remove key-value pair")[0]);
|
||||
await screen.findByTestId("metadata-schema-label");
|
||||
await user.click(screen.getByRole("button", { name: /add key-value pair/i }));
|
||||
fireEvent.change(screen.getByPlaceholderText("Key"), { target: { value: "region" } });
|
||||
fireEvent.change(screen.getAllByPlaceholderText("Value")[1], { target: { value: "us" } });
|
||||
fireEvent.change(screen.getByLabelText("Cost Center"), { target: { value: "CC-1001" } });
|
||||
await user.click(screen.getByRole("button", { name: "Save" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByPlaceholderText("Key").map((input) => (input as HTMLInputElement).value)).toEqual([
|
||||
"app_name",
|
||||
]);
|
||||
expect(onFinish).toHaveBeenCalledWith({
|
||||
metadata: [
|
||||
{ key: "cost_center", value: "CC-1001" },
|
||||
{ key: "region", value: "us" },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
await user.click(screen.getByLabelText("Remove key-value pair"));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByPlaceholderText("Key")).not.toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId("metadata-schema-label")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show a skeleton instead of the editor while the schema is loading", () => {
|
||||
|
|
@ -283,9 +321,9 @@ describe("MetadataKeyValueFields with a declared schema", () => {
|
|||
rerender(<Harness onFinish={onFinish} schemaFields={schema} schemaLoading={false} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByPlaceholderText("Key").map((input) => (input as HTMLInputElement).value)).toEqual([
|
||||
"cost_center",
|
||||
"app_name",
|
||||
expect(screen.getAllByTestId("metadata-schema-label").map((label) => label.textContent)).toEqual([
|
||||
"Cost Center",
|
||||
"Application Name",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { TeamMetadataField } from "@/app/(dashboard)/hooks/teams/useTeamMetadata
|
|||
import { FormField } from "@/components/shared/form/FormField";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export interface MetadataPair {
|
||||
|
|
@ -87,6 +88,7 @@ const MetadataKeyValueFields = <TFieldValues extends FieldValues>({
|
|||
}: MetadataKeyValueFieldsProps<TFieldValues>) => {
|
||||
const { fields, append, remove } = useFieldArray({ control, name });
|
||||
const seededRef = useRef(false);
|
||||
const schemaLabelsByKey = new Map(schemaFields.map((field) => [field.key, field.label || field.key]));
|
||||
|
||||
useEffect(() => {
|
||||
if (seededRef.current || schemaLoading || schemaFields.length === 0) return;
|
||||
|
|
@ -114,29 +116,50 @@ const MetadataKeyValueFields = <TFieldValues extends FieldValues>({
|
|||
|
||||
return (
|
||||
<>
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="mb-2 flex items-start gap-2">
|
||||
<FormField control={control} name={`${name}.${index}.key` as FieldPath<TFieldValues>}>
|
||||
{({ ref, value, ...rest }) => (
|
||||
<Input {...rest} ref={ref} value={(value as string) ?? ""} placeholder="Key" />
|
||||
{fields.map((field, index) => {
|
||||
const schemaLabel = schemaLabelsByKey.get((field as unknown as Partial<MetadataPair>).key ?? "");
|
||||
return (
|
||||
<div key={field.id} className="mb-2 flex items-start gap-2">
|
||||
{schemaLabel === undefined ? (
|
||||
<FormField control={control} name={`${name}.${index}.key` as FieldPath<TFieldValues>}>
|
||||
{({ ref, value, ...rest }) => (
|
||||
<Input {...rest} ref={ref} value={(value as string) ?? ""} placeholder="Key" />
|
||||
)}
|
||||
</FormField>
|
||||
) : (
|
||||
<Label
|
||||
htmlFor={`${field.id}-value`}
|
||||
data-testid="metadata-schema-label"
|
||||
className="h-9 flex-1 items-center truncate px-3 font-medium"
|
||||
>
|
||||
{schemaLabel}
|
||||
</Label>
|
||||
)}
|
||||
</FormField>
|
||||
<FormField control={control} name={`${name}.${index}.value` as FieldPath<TFieldValues>}>
|
||||
{({ ref, value, ...rest }) => (
|
||||
<Input {...rest} ref={ref} value={(value as string) ?? ""} placeholder="Value" />
|
||||
<FormField control={control} name={`${name}.${index}.value` as FieldPath<TFieldValues>}>
|
||||
{({ ref, value, id, ...rest }) => (
|
||||
<Input
|
||||
{...rest}
|
||||
id={schemaLabel === undefined ? id : `${field.id}-value`}
|
||||
ref={ref}
|
||||
value={(value as string) ?? ""}
|
||||
placeholder="Value"
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
{schemaLabel === undefined && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label="Remove key-value pair"
|
||||
className="mt-1 text-destructive"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<CircleMinus className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
</FormField>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label="Remove key-value pair"
|
||||
className="mt-1 text-destructive"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<CircleMinus className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full border-dashed"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue