fix(ui): render team_metadata_schema keys as fixed labels (#41482)

* fix(ui): render team_metadata_schema keys as fixed labels

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(ui): update team metadata schema tests for fixed labels

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): derive team metadata schema labels from live key values

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: jesus <jesus@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-10-06 23:21:09 -07:00 • committed by GitHub
parent 43cf7e7493
commit f8ddb20965
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 291 additions and 76 deletions

View file

@ -892,16 +892,16 @@ describe("Teams - schema-declared metadata fields in team create", () => {
});
};
it("should prepopulate the declared key as an ordinary pair row and submit its value", async () => {
it("should show the declared key as a fixed label and submit its value under the declared key", async () => {
await openCreateModal();
fireEvent.change(screen.getByLabelText(/team name/i), { target: { value: "Test Team" } });
fireEvent.change(screen.getByTestId("create-team-models-select"), { target: { value: "gpt-4" } });
await waitFor(() => {
expect((screen.getByPlaceholderText("Key") as HTMLInputElement).value).toBe("cost_center");
});
fireEvent.change(screen.getByPlaceholderText("Value"), { target: { value: "CC-1001" } });
expect(await screen.findByTestId("metadata-schema-label")).toHaveTextContent("Cost Center");
expect(screen.queryByPlaceholderText("Key")).not.toBeInTheDocument();
expect(screen.queryByLabelText("Remove key-value pair")).not.toBeInTheDocument();
fireEvent.change(screen.getByLabelText("Cost Center"), { target: { value: "CC-1001" } });
const createTeamSubmitButtons = screen.getAllByRole("button", { name: /create team/i });
fireEvent.click(createTeamSubmitButtons[createTeamSubmitButtons.length - 1]);
@ -922,10 +922,7 @@ describe("Teams - schema-declared metadata fields in team create", () => {
fireEvent.change(screen.getByLabelText(/team name/i), { target: { value: "Test Team" } });
fireEvent.change(screen.getByTestId("create-team-models-select"), { target: { value: "gpt-4" } });
await waitFor(() => {
expect((screen.getByPlaceholderText("Key") as HTMLInputElement).value).toBe("cost_center");
});
fireEvent.change(screen.getByPlaceholderText("Value"), { target: { value: "CC-9999" } });
fireEvent.change(await screen.findByLabelText("Cost Center"), { target: { value: "CC-9999" } });
const createTeamSubmitButtons = screen.getAllByRole("button", { name: /create team/i });
fireEvent.click(createTeamSubmitButtons[createTeamSubmitButtons.length - 1]);
@ -945,16 +942,12 @@ describe("Teams - schema-declared metadata fields in team create", () => {
expect(screen.queryByRole("button", { name: /add key-value pair/i })).not.toBeInTheDocument();
});
it("should re-seed declared keys when the create modal is closed and reopened", async () => {
it("should re-seed the declared key and drop free-form rows when the create modal is closed and reopened", async () => {
await openCreateModal();
await waitFor(() => {
expect((screen.getByPlaceholderText("Key") as HTMLInputElement).value).toBe("cost_center");
});
fireEvent.click(screen.getByLabelText("Remove key-value pair"));
await waitFor(() => {
expect(screen.queryByPlaceholderText("Key")).not.toBeInTheDocument();
});
fireEvent.change(await screen.findByLabelText("Cost Center"), { target: { value: "CC-1001" } });
fireEvent.click(screen.getByRole("button", { name: /add key-value pair/i }));
fireEvent.change(await screen.findByPlaceholderText("Key"), { target: { value: "region" } });
fireEvent.click(screen.getByRole("button", { name: /^close$/i }));
await waitFor(() => {
@ -966,9 +959,8 @@ describe("Teams - schema-declared metadata fields in team create", () => {
fireEvent.click(createButton);
});
await waitFor(() => {
expect((screen.getByPlaceholderText("Key") as HTMLInputElement).value).toBe("cost_center");
});
expect(await screen.findByLabelText("Cost Center")).toHaveValue("");
expect(screen.queryByPlaceholderText("Key")).not.toBeInTheDocument();
});
});

View file

@ -101,21 +101,24 @@ interface HarnessProps {
initialMetadata?: MetadataPair[];
schemaFields?: TeamMetadataField[];
schemaLoading?: boolean;
fieldsHidden?: boolean;
}
const harnessSchema = z.object({ metadata: metadataPairsSchema });
const Harness: React.FC<HarnessProps> = ({ onFinish, initialMetadata, schemaFields, schemaLoading }) => {
const Harness: React.FC<HarnessProps> = ({ onFinish, initialMetadata, schemaFields, schemaLoading, fieldsHidden }) => {
const form = useZodForm(harnessSchema, { defaultValues: { metadata: initialMetadata ?? [] } });
return (
<form onSubmit={form.handleSubmit((values) => onFinish(values))}>
<MetadataKeyValueFields
control={form.control}
getValues={form.getValues}
name="metadata"
schemaFields={schemaFields}
schemaLoading={schemaLoading}
/>
{!fieldsHidden && (
<MetadataKeyValueFields
control={form.control}
getValues={form.getValues}
name="metadata"
schemaFields={schemaFields}
schemaLoading={schemaLoading}
/>
)}
<button type="submit">Save</button>
</form>
);
@ -218,17 +221,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 () => {
@ -250,9 +277,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([
@ -261,18 +288,147 @@ 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 keep duplicate keys in a free-form row", async () => {
const user = userEvent.setup();
const onFinish = vi.fn();
render(<Harness onFinish={onFinish} schemaFields={[{ key: "cost_center", label: "Cost Center" }]} />);
expect(await screen.findByTestId("metadata-schema-label")).toHaveTextContent("Cost Center");
await user.click(screen.getByRole("button", { name: /add key-value pair/i }));
fireEvent.change(screen.getByPlaceholderText("Key"), { target: { value: "cost_center" } });
expect(screen.getAllByPlaceholderText("Key")).toHaveLength(1);
expect(screen.getByPlaceholderText("Key")).toHaveValue("cost_center");
expect(screen.getAllByLabelText("Remove key-value pair")).toHaveLength(1);
expect(screen.getAllByTestId("metadata-schema-label")).toHaveLength(1);
await user.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => {
expect(screen.getByText("Duplicate key")).toBeInTheDocument();
});
expect(onFinish).not.toHaveBeenCalled();
});
it("should keep an existing row above a declared row editable while a declared key is typed into it", async () => {
const user = userEvent.setup();
const onFinish = vi.fn();
render(
<Harness
onFinish={onFinish}
initialMetadata={[{ key: "region", value: "us" }]}
schemaFields={[
{ key: "cost", label: "Cost" },
{ key: "cost_center", label: "Cost Center" },
]}
/>,
);
await waitFor(() => {
expect(screen.getAllByTestId("metadata-schema-label").map((label) => label.textContent)).toEqual([
"Cost",
"Cost Center",
]);
});
const keyInput = screen.getByPlaceholderText("Key");
await user.clear(keyInput);
await user.type(keyInput, "cost_center");
expect(keyInput).toHaveValue("cost_center");
expect(keyInput).toHaveFocus();
await user.click(screen.getByRole("button", { name: /add key-value pair/i }));
expect(screen.getAllByPlaceholderText("Key").map((input) => (input as HTMLInputElement).value)).toEqual([
"cost_center",
"",
]);
await user.click(screen.getAllByLabelText("Remove key-value pair")[1]);
expect(screen.getAllByPlaceholderText("Key").map((input) => (input as HTMLInputElement).value)).toEqual([
"cost_center",
]);
expect(screen.getAllByLabelText("Remove key-value pair")).toHaveLength(1);
expect(screen.getAllByTestId("metadata-schema-label").map((label) => label.textContent)).toEqual([
"Cost",
"Cost Center",
]);
await user.click(screen.getByRole("button", { name: "Save" }));
expect(await screen.findByText("Duplicate key")).toBeInTheDocument();
expect(onFinish).not.toHaveBeenCalled();
});
it("should keep one editable row per duplicated declared key when the editor remounts", async () => {
const user = userEvent.setup();
const onFinish = vi.fn();
const schemaFields = [{ key: "cost_center", label: "Cost Center" }];
const initialMetadata = [{ key: "region", value: "us" }];
const { rerender } = render(
<Harness onFinish={onFinish} initialMetadata={initialMetadata} schemaFields={schemaFields} />,
);
expect(await screen.findByTestId("metadata-schema-label")).toHaveTextContent("Cost Center");
fireEvent.change(screen.getByPlaceholderText("Key"), { target: { value: "cost_center" } });
rerender(
<Harness onFinish={onFinish} initialMetadata={initialMetadata} schemaFields={schemaFields} fieldsHidden />,
);
rerender(<Harness onFinish={onFinish} initialMetadata={initialMetadata} schemaFields={schemaFields} />);
expect(screen.getAllByTestId("metadata-schema-label")).toHaveLength(1);
expect(screen.getByPlaceholderText("Key")).toHaveValue("cost_center");
expect(screen.getAllByLabelText("Remove key-value pair")).toHaveLength(1);
await user.click(screen.getByRole("button", { name: "Save" }));
expect(await screen.findByText("Duplicate key")).toBeInTheDocument();
expect(onFinish).not.toHaveBeenCalled();
});
it("should label only rows whose current key is declared when the schema arrives after an edit", () => {
const onFinish = vi.fn();
const initialMetadata = [{ key: "cost_center", value: "1" }];
const { rerender } = render(<Harness onFinish={onFinish} initialMetadata={initialMetadata} schemaFields={[]} />);
fireEvent.change(screen.getByPlaceholderText("Key"), { target: { value: "region" } });
rerender(
<Harness
onFinish={onFinish}
initialMetadata={initialMetadata}
schemaFields={[{ key: "cost_center", label: "Cost Center" }]}
/>,
);
expect(screen.getByPlaceholderText("Key")).toHaveValue("region");
expect(screen.getAllByTestId("metadata-schema-label").map((label) => label.textContent)).toEqual(["Cost Center"]);
});
it("should show a skeleton instead of the editor while the schema is loading", () => {
@ -289,9 +445,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",
]);
});
});

View file

@ -1,5 +1,5 @@
import { CircleMinus, Plus } from "lucide-react";
import React, { useEffect, useRef } from "react";
import React, { useEffect, useRef, useState } from "react";
import {
useFieldArray,
type Control,
@ -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 {
@ -78,6 +79,64 @@ interface MetadataKeyValueFieldsProps<TFieldValues extends FieldValues> {
schemaLoading?: boolean;
}
interface MetadataRowProps<TFieldValues extends FieldValues> {
control: Control<TFieldValues>;
name: FieldArrayPath<TFieldValues>;
index: number;
rowId: string;
schemaLabel: string | undefined;
onRemove: () => void;
}
const MetadataRow = <TFieldValues extends FieldValues>({
control,
name,
index,
rowId,
schemaLabel,
onRemove,
}: MetadataRowProps<TFieldValues>) => {
return (
<div 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={`${rowId}-value`}
data-testid="metadata-schema-label"
className="h-9 w-full items-center truncate px-3 font-medium"
>
{schemaLabel}
</Label>
)}
<FormField control={control} name={`${name}.${index}.value` as FieldPath<TFieldValues>}>
{({ ref, value, id, ...rest }) => (
<Input
{...rest}
id={schemaLabel === undefined ? id : `${rowId}-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={onRemove}
>
<CircleMinus className="size-4" />
</Button>
)}
</div>
);
};
const MetadataKeyValueFields = <TFieldValues extends FieldValues>({
control,
getValues,
@ -87,6 +146,24 @@ 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]));
const schemaReady = !schemaLoading && schemaFields.length > 0;
const livePairs: readonly (Partial<MetadataPair> | undefined)[] =
getValues(name as unknown as FieldPath<TFieldValues>) ?? [];
const [keysAtMount, setKeysAtMount] = useState<ReadonlyMap<string, string | undefined>>(() => new Map());
const unseenKeys = schemaReady
? fields.flatMap((field, index) => (keysAtMount.has(field.id) ? [] : [[field.id, livePairs[index]?.key] as const]))
: [];
if (unseenKeys.length > 0) {
setKeysAtMount(new Map([...keysAtMount, ...unseenKeys]));
}
const rowKeysAtMount = fields.map((field, index) =>
keysAtMount.has(field.id) || !schemaReady ? keysAtMount.get(field.id) : livePairs[index]?.key,
);
const schemaLabelAt = (index: number): string | undefined => {
const key = rowKeysAtMount[index];
return key === undefined || rowKeysAtMount.indexOf(key) !== index ? undefined : schemaLabelsByKey.get(key);
};
useEffect(() => {
if (seededRef.current || schemaLoading || schemaFields.length === 0) return;
@ -115,27 +192,15 @@ 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" />
)}
</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>
<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>
<MetadataRow
key={field.id}
control={control}
name={name}
index={index}
rowId={field.id}
schemaLabel={schemaLabelAt(index)}
onRemove={() => remove(index)}
/>
))}
<Button
variant="outline"

View file

@ -1407,7 +1407,7 @@ describe("TeamInfoView", () => {
expect(screen.getByLabelText("Estimated Output Tokens Per Model")).toBeEnabled();
});
it("should keep declared keys as ordinary prefilled rows and submit the edited value", async () => {
it("should show declared keys as fixed labels and submit the edited value", async () => {
const user = userEvent.setup({ delay: null });
vi.mocked(useTeamMetadataSchema).mockReturnValue({
data: [
@ -1428,16 +1428,18 @@ describe("TeamInfoView", () => {
await openSettingsEditor(user);
await waitFor(() => {
expect(screen.getAllByPlaceholderText("Key").map((input) => (input as HTMLInputElement).value)).toEqual([
"cost_center",
"department",
"app_name",
expect(screen.getAllByTestId("metadata-schema-label").map((label) => label.textContent)).toEqual([
"Cost Center",
"Application Name",
]);
});
expect(screen.getAllByPlaceholderText("Value")[0]).toHaveValue("CC-OLD");
expect(screen.getAllByPlaceholderText("Key").map((input) => (input as HTMLInputElement).value)).toEqual([
"department",
]);
expect(screen.getByLabelText("Cost Center")).toHaveValue("CC-OLD");
await user.clear(screen.getAllByPlaceholderText("Value")[0]);
fireEvent.change(screen.getAllByPlaceholderText("Value")[0], { target: { value: "CC-NEW" } });
await user.clear(screen.getByLabelText("Cost Center"));
fireEvent.change(screen.getByLabelText("Cost Center"), { target: { value: "CC-NEW" } });
await user.click(screen.getByRole("button", { name: /save changes/i }));
await waitFor(() => {