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>
This commit is contained in:
jesus 2026-09-19 23:46:50 +00:00
parent 3089f2acc6
commit 3fb02ee537
2 changed files with 40 additions and 1 deletions

View file

@ -307,6 +307,28 @@ describe("MetadataKeyValueFields with a declared schema", () => {
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 show a skeleton instead of the editor while the schema is loading", () => {
render(<Harness onFinish={vi.fn()} schemaLoading />);

View file

@ -2,6 +2,7 @@ import { CircleMinus, Plus } from "lucide-react";
import React, { useEffect, useRef } from "react";
import {
useFieldArray,
useWatch,
type Control,
type FieldArrayPath,
type FieldPath,
@ -87,6 +88,10 @@ const MetadataKeyValueFields = <TFieldValues extends FieldValues>({
schemaLoading = false,
}: MetadataKeyValueFieldsProps<TFieldValues>) => {
const { fields, append, remove } = useFieldArray({ control, name });
const watchedPairs = (useWatch({ control, name: name as unknown as FieldPath<TFieldValues> }) ?? []) as readonly (
| Partial<MetadataPair>
| undefined
)[];
const seededRef = useRef(false);
const schemaLabelsByKey = new Map(schemaFields.map((field) => [field.key, field.label || field.key]));
@ -114,10 +119,22 @@ const MetadataKeyValueFields = <TFieldValues extends FieldValues>({
);
}
const getSchemaLabel = (index: number): string | undefined => {
const key = watchedPairs[index]?.key;
if (
key === undefined ||
!schemaLabelsByKey.has(key) ||
watchedPairs.findIndex((pair) => pair?.key === key) !== index
) {
return undefined;
}
return schemaLabelsByKey.get(key);
};
return (
<>
{fields.map((field, index) => {
const schemaLabel = schemaLabelsByKey.get((field as unknown as Partial<MetadataPair>).key ?? "");
const schemaLabel = getSchemaLabel(index);
return (
<div key={field.id} className="mb-2 flex items-start gap-2">
{schemaLabel === undefined ? (