Fix problems with key value input and empty entries (#19421)

This commit is contained in:
Jon Koops 2023-04-04 10:09:31 +02:00 committed by GitHub
parent 9f15cf432b
commit 84e763b472
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 215 additions and 150 deletions

View file

@ -542,7 +542,8 @@ describe("Clients test", () => {
it("Should delete attribute from client role", () => { it("Should delete attribute from client role", () => {
commonPage.tableUtils().clickRowItemLink(itemId); commonPage.tableUtils().clickRowItemLink(itemId);
rolesTab.goToAttributesTab(); rolesTab.goToAttributesTab();
attributesTab.deleteAttribute(1); attributesTab.deleteAttribute(0);
attributesTab.assertEmpty();
commonPage commonPage
.masthead() .masthead()
.checkNotificationMessage("The role has been saved", true); .checkNotificationMessage("The role has been saved", true);

View file

@ -420,7 +420,8 @@ describe("Group test", () => {
}); });
it("Remove attribute", () => { it("Remove attribute", () => {
attributesTab.deleteAttribute(1).assertRowItemsEqualTo(1); attributesTab.deleteAttribute(0);
attributesTab.assertEmpty();
groupPage.assertNotificationGroupUpdated(); groupPage.assertNotificationGroupUpdated();
}); });
@ -429,7 +430,7 @@ describe("Group test", () => {
.addAttribute("key", "value") .addAttribute("key", "value")
.addAnAttributeButton() .addAnAttributeButton()
.revert() .revert()
.assertRowItemsEqualTo(1); .assertEmpty();
}); });
}); });

View file

@ -248,10 +248,10 @@ describe("Realm roles test", () => {
listingPage.itemExist(editRoleName).goToItemDetails(editRoleName); listingPage.itemExist(editRoleName).goToItemDetails(editRoleName);
createRealmRolePage.goToAttributesTab(); createRealmRolePage.goToAttributesTab();
keyValue.fillKeyValue({ key: "one", value: "1" }).validateRows(2); keyValue.fillKeyValue({ key: "one", value: "1" }).validateRows(1);
keyValue.save(); keyValue.save();
masthead.checkNotificationMessage("The role has been saved", true); masthead.checkNotificationMessage("The role has been saved", true);
keyValue.validateRows(2); keyValue.validateRows(1);
}); });
it("should add attribute multiple", () => { it("should add attribute multiple", () => {
@ -259,17 +259,17 @@ describe("Realm roles test", () => {
createRealmRolePage.goToAttributesTab(); createRealmRolePage.goToAttributesTab();
keyValue keyValue
.fillKeyValue({ key: "two", value: "2" }, 1) .fillKeyValue({ key: "two", value: "2" })
.fillKeyValue({ key: "three", value: "3" }, 2) .fillKeyValue({ key: "three", value: "3" })
.save() .save()
.validateRows(4); .validateRows(3);
}); });
it("should delete attribute", () => { it("should delete attribute", () => {
listingPage.itemExist(editRoleName).goToItemDetails(editRoleName); listingPage.itemExist(editRoleName).goToItemDetails(editRoleName);
createRealmRolePage.goToAttributesTab(); createRealmRolePage.goToAttributesTab();
keyValue.deleteRow(1).save().validateRows(3); keyValue.deleteRow(1).save().validateRows(2);
}); });
}); });

View file

@ -2,9 +2,10 @@ export default class AttributesTab {
private saveAttributeBtn = "save-attributes"; private saveAttributeBtn = "save-attributes";
private addAttributeBtn = "attributes-add-row"; private addAttributeBtn = "attributes-add-row";
private attributesTab = "attributes"; private attributesTab = "attributes";
private attributeRow = "row"; private keyInput = "attributes-key";
private keyInput = (index: number) => `attributes[${index}].key`; private valueInput = "attributes-value";
private valueInput = (index: number) => `attributes[${index}].value`; private removeBtn = "attributes-remove";
private emptyState = "attributes-empty-state";
public goToAttributesTab() { public goToAttributesTab() {
cy.findByTestId(this.attributesTab).click(); cy.findByTestId(this.attributesTab).click();
@ -13,14 +14,15 @@ export default class AttributesTab {
} }
public addAttribute(key: string, value: string) { public addAttribute(key: string, value: string) {
cy.findAllByTestId(this.attributeRow) cy.findByTestId(this.addAttributeBtn).click();
cy.findAllByTestId(this.keyInput)
.its("length") .its("length")
.then((index) => { .then((length) => {
cy.findByTestId(this.keyInput(index - 1)).type(key, { force: true }); this.keyInputAt(length - 1).type(key, { force: true });
cy.findByTestId(this.valueInput(index - 1)).type(value, { this.valueInputAt(length - 1).type(value, { force: true });
force: true,
});
}); });
return this; return this;
} }
@ -35,7 +37,7 @@ export default class AttributesTab {
} }
public deleteAttributeButton(row: number) { public deleteAttributeButton(row: number) {
cy.findByTestId(`attributes[${row - 1}].remove`).click({ force: true }); this.removeButtonAt(row).click({ force: true });
return this; return this;
} }
@ -48,19 +50,27 @@ export default class AttributesTab {
this.deleteAttributeButton(rowIndex); this.deleteAttributeButton(rowIndex);
this.save(); this.save();
cy.findAllByTestId(`attributes[${rowIndex - 1}].key`).should(
"have.value",
""
);
cy.findAllByTestId(`attributes[${rowIndex - 1}].value`).should(
"have.value",
""
);
return this; return this;
} }
public assertEmpty() {
cy.findByTestId(this.emptyState).should("exist");
}
public assertRowItemsEqualTo(amount: number) { public assertRowItemsEqualTo(amount: number) {
cy.findAllByTestId("row").its("length").should("be.eq", amount); cy.findAllByTestId(this.keyInput).its("length").should("be.eq", amount);
return this; return this;
} }
private keyInputAt(index: number) {
return cy.findAllByTestId(this.keyInput).eq(index);
}
private valueInputAt(index: number) {
return cy.findAllByTestId(this.valueInput).eq(index);
}
private removeButtonAt(index: number) {
return cy.findAllByTestId(this.removeBtn).eq(index);
}
} }

View file

@ -7,22 +7,26 @@ export default class KeyValueInput {
this.name = name; this.name = name;
} }
fillKeyValue({ key, value }: KeyValueType, index = 0) { fillKeyValue({ key, value }: KeyValueType) {
cy.findByTestId(`${this.name}[${index}].key`).clear();
cy.findByTestId(`${this.name}[${index}].key`).type(key);
cy.findByTestId(`${this.name}[${index}].value`).clear();
cy.findByTestId(`${this.name}[${index}].value`).type(value);
cy.findByTestId(`${this.name}-add-row`).click(); cy.findByTestId(`${this.name}-add-row`).click();
cy.findAllByTestId(`${this.name}-key`)
.its("length")
.then((length) => {
this.keyInputAt(length - 1).type(key);
this.valueInputAt(length - 1).type(value);
});
return this; return this;
} }
deleteRow(index: number) { deleteRow(index: number) {
cy.findByTestId(`${this.name}[${index}].remove`).click(); cy.findAllByTestId(`${this.name}-remove`).eq(index).click();
return this; return this;
} }
validateRows(numberOfRows: number) { validateRows(numberOfRows: number) {
cy.findAllByTestId("row").should("have.length", numberOfRows); cy.findAllByTestId(`${this.name}-key`).should("have.length", numberOfRows);
return this; return this;
} }
@ -30,4 +34,12 @@ export default class KeyValueInput {
cy.findByTestId("save-attributes").click(); cy.findByTestId("save-attributes").click();
return this; return this;
} }
keyInputAt(index: number) {
return cy.findAllByTestId(`${this.name}-key`).eq(index);
}
valueInputAt(index: number) {
return cy.findAllByTestId(`${this.name}-value`).eq(index);
}
} }

View file

@ -0,0 +1,33 @@
import type { KeyValueType } from "../../../../../src/components/key-value-form/key-value-convert";
export default class LegacyKeyValueInput {
private name: string;
constructor(name: string) {
this.name = name;
}
fillKeyValue({ key, value }: KeyValueType, index = 0) {
cy.findByTestId(`${this.name}.${index}.key`).clear();
cy.findByTestId(`${this.name}.${index}.key`).type(key);
cy.findByTestId(`${this.name}.${index}.value`).clear();
cy.findByTestId(`${this.name}.${index}.value`).type(value);
cy.findByTestId(`${this.name}-add-row`).click();
return this;
}
deleteRow(index: number) {
cy.findByTestId(`${this.name}.${index}.remove`).click();
return this;
}
validateRows(numberOfRows: number) {
cy.findAllByTestId("row").should("have.length", numberOfRows);
return this;
}
save() {
cy.findByTestId("save-attributes").click();
return this;
}
}

View file

@ -1,4 +1,4 @@
import KeyValueInput from "../KeyValueInput"; import LegacyKeyValueInput from "../LegacyKeyValueInput";
export default class AddMapperPage { export default class AddMapperPage {
private mappersTab = "mappers-tab"; private mappersTab = "mappers-tab";
@ -23,8 +23,8 @@ export default class AddMapperPage {
private mappersUrl = "/oidc/mappers"; private mappersUrl = "/oidc/mappers";
private regexAttributeValuesSwitch = "are.attribute.values.regex"; private regexAttributeValuesSwitch = "are.attribute.values.regex";
private syncmodeSelectToggle = "#syncMode"; private syncmodeSelectToggle = "#syncMode";
private attributesKeyInput = '[data-testid="config.attributes[0].key"]'; private attributesKeyInput = '[data-testid="config.attributes.0.key"]';
private attributesValueInput = '[data-testid="config.attributes[0].value"]'; private attributesValueInput = '[data-testid="config.attributes.0.value"]';
private template = "template"; private template = "template";
private target = "#target"; private target = "#target";
@ -392,7 +392,7 @@ export default class AddMapperPage {
cy.findByTestId(this.idpMapperSelect).contains("Claim to Role").click(); cy.findByTestId(this.idpMapperSelect).contains("Claim to Role").click();
const keyValue = new KeyValueInput("config.claims"); const keyValue = new LegacyKeyValueInput("config.claims");
keyValue.fillKeyValue({ key: "key", value: "value" }); keyValue.fillKeyValue({ key: "key", value: "value" });

View file

@ -17,8 +17,9 @@ export default class UserProfile {
private newAttributeRequiredFor = 'input[name="roles"]'; private newAttributeRequiredFor = 'input[name="roles"]';
private newAttributeRequiredWhen = 'input[name="requiredWhen"]'; private newAttributeRequiredWhen = 'input[name="requiredWhen"]';
private newAttributeEmptyValidators = ".kc-emptyValidators"; private newAttributeEmptyValidators = ".kc-emptyValidators";
private newAttributeAnnotationKey = 'input[name="annotations[0].key"]'; private newAttributeAnnotationBtn = "annotations-add-row";
private newAttributeAnnotationValue = 'input[name="annotations[0].value"]'; private newAttributeAnnotationKey = "annotations-key";
private newAttributeAnnotationValue = "annotations-value";
private validatorRolesList = "#validator"; private validatorRolesList = "#validator";
private validatorsList = 'tbody [data-label="name"]'; private validatorsList = 'tbody [data-label="name"]';
private saveNewAttributeBtn = "attribute-create"; private saveNewAttributeBtn = "attribute-create";
@ -106,8 +107,9 @@ export default class UserProfile {
cy.get(this.newAttributeRequiredFor).first().check({ force: true }); cy.get(this.newAttributeRequiredFor).first().check({ force: true });
cy.get(this.newAttributeRequiredWhen).first().check(); cy.get(this.newAttributeRequiredWhen).first().check();
cy.get(this.newAttributeEmptyValidators).contains("No validators."); cy.get(this.newAttributeEmptyValidators).contains("No validators.");
cy.get(this.newAttributeAnnotationKey).type("test"); cy.findByTestId(this.newAttributeAnnotationBtn).click();
cy.get(this.newAttributeAnnotationValue).type("123"); cy.findByTestId(this.newAttributeAnnotationKey).type("test");
cy.findByTestId(this.newAttributeAnnotationValue).type("123");
return this; return this;
} }

View file

@ -172,10 +172,13 @@
"years": "Years" "years": "Years"
}, },
"attributes": "Attributes", "attributes": "Attributes",
"missingAttributes": "No attributes have been defined yet. Click the below button to add attributes, key and value are required for a key pair.",
"addAttribute": "Add an attribute", "addAttribute": "Add an attribute",
"removeAttribute": "Remove attribute", "removeAttribute": "Remove attribute",
"keyPlaceholder": "Type a key", "keyPlaceholder": "Type a key",
"valuePlaceholder": "Type a value", "valuePlaceholder": "Type a value",
"keyError": "A key must be provided.",
"valueError": "A value must be provided.",
"credentials": "Credentials", "credentials": "Credentials",
"clientId": "Client ID", "clientId": "Client ID",
"clientName": "Name", "clientName": "Name",

View file

@ -103,14 +103,14 @@ const ValueInput = ({
<Td> <Td>
{resources || attributeValues?.length ? ( {resources || attributeValues?.length ? (
<Controller <Controller
name={`${name}[${rowIndex}].value`} name={`${name}.${rowIndex}.value`}
defaultValue={[]} defaultValue={[]}
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<Select <Select
toggleId={`${attribute.id}-value`} toggleId={`${attribute.id}-value`}
className="kc-attribute-value-selectable" className="kc-attribute-value-selectable"
name={`${name}[${rowIndex}].value`} name={`${name}.${rowIndex}.value`}
chipGroupProps={{ chipGroupProps={{
numChips: 1, numChips: 1,
expandedText: t("common:hide"), expandedText: t("common:hide"),
@ -171,7 +171,7 @@ export const KeyBasedAttributeInput = ({
} }
}, [fields]); }, [fields]);
const watchLastValue = watch(`${name}[${fields.length - 1}].value`, ""); const watchLastValue = watch(`${name}.${fields.length - 1}.value`, "");
return ( return (
<TableComposable <TableComposable
@ -190,14 +190,14 @@ export const KeyBasedAttributeInput = ({
<Tr key={attribute.id} data-testid="attribute-row"> <Tr key={attribute.id} data-testid="attribute-row">
<Td> <Td>
<Controller <Controller
name={`${name}[${rowIndex}].key`} name={`${name}.${rowIndex}.key`}
defaultValue="" defaultValue=""
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<Select <Select
toggleId={`${name}[${rowIndex}].key`} toggleId={`${name}.${rowIndex}.key`}
className="kc-attribute-key-selectable" className="kc-attribute-key-selectable"
name={`${name}[${rowIndex}].key`} name={`${name}.${rowIndex}.key`}
onToggle={(open) => toggleKeySelect(rowIndex, open)} onToggle={(open) => toggleKeySelect(rowIndex, open)}
isOpen={isKeyOpenArray[rowIndex]} isOpen={isKeyOpenArray[rowIndex]}
variant={SelectVariant.typeahead} variant={SelectVariant.typeahead}

View file

@ -84,11 +84,11 @@ export const MapComponent = ({ name, label, helpText }: ComponentProps) => {
<Flex key={attribute.id} data-testid="row"> <Flex key={attribute.id} data-testid="row">
<FlexItem grow={{ default: "grow" }}> <FlexItem grow={{ default: "grow" }}>
<TextInput <TextInput
name={`${fieldName}[${index}].key`} name={`${fieldName}.${index}.key`}
placeholder={t("common:keyPlaceholder")} placeholder={t("common:keyPlaceholder")}
aria-label={t("key")} aria-label={t("key")}
defaultValue={attribute.key} defaultValue={attribute.key}
data-testid={`${fieldName}[${index}].key`} data-testid={`${fieldName}.${index}.key`}
onChange={(value) => updateKey(index, value)} onChange={(value) => updateKey(index, value)}
onBlur={() => update()} onBlur={() => update()}
/> />
@ -98,11 +98,11 @@ export const MapComponent = ({ name, label, helpText }: ComponentProps) => {
spacer={{ default: "spacerNone" }} spacer={{ default: "spacerNone" }}
> >
<TextInput <TextInput
name={`${fieldName}[${index}].value`} name={`${fieldName}.${index}.value`}
placeholder={t("common:valuePlaceholder")} placeholder={t("common:valuePlaceholder")}
aria-label={t("common:value")} aria-label={t("common:value")}
defaultValue={attribute.value} defaultValue={attribute.value}
data-testid={`${fieldName}[${index}].value`} data-testid={`${fieldName}.${index}.value`}
onChange={(value) => updateValue(index, value)} onChange={(value) => updateValue(index, value)}
onBlur={() => update()} onBlur={() => update()}
/> />
@ -113,7 +113,7 @@ export const MapComponent = ({ name, label, helpText }: ComponentProps) => {
title={t("common:removeAttribute")} title={t("common:removeAttribute")}
isDisabled={map.length === 1} isDisabled={map.length === 1}
onClick={() => remove(index)} onClick={() => remove(index)}
data-testid={`${fieldName}[${index}].remove`} data-testid={`${fieldName}.${index}.remove`}
> >
<MinusCircleIcon /> <MinusCircleIcon />
</Button> </Button>

View file

@ -2,16 +2,20 @@ import {
ActionList, ActionList,
ActionListItem, ActionListItem,
Button, Button,
Flex, EmptyState,
FlexItem, EmptyStateBody,
Grid,
GridItem,
HelperText,
HelperTextItem,
InputGroup,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons"; import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons";
import { useEffect } from "react"; import { Fragment } from "react";
import { useFieldArray, useFormContext, useWatch } from "react-hook-form"; import { useFieldArray, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { KeycloakTextInput } from "../keycloak-text-input/KeycloakTextInput"; import { KeycloakTextInput } from "../keycloak-text-input/KeycloakTextInput";
import { KeyValueType } from "./key-value-convert";
type KeyValueInputProps = { type KeyValueInputProps = {
name: string; name: string;
@ -19,83 +23,82 @@ type KeyValueInputProps = {
export const KeyValueInput = ({ name }: KeyValueInputProps) => { export const KeyValueInput = ({ name }: KeyValueInputProps) => {
const { t } = useTranslation("common"); const { t } = useTranslation("common");
const { control, register } = useFormContext(); const {
control,
register,
formState: { errors },
} = useFormContext();
const { fields, append, remove } = useFieldArray({ const { fields, append, remove } = useFieldArray({
control, control,
name, name,
}); });
const watchFields = useWatch({ const appendNew = () => append({ key: "", value: "" });
control,
name,
defaultValue: [{ key: "", value: "" }],
});
const isValid = return fields.length > 0 ? (
Array.isArray(watchFields) &&
watchFields.every(
({ key, value }: KeyValueType) =>
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
key?.trim().length !== 0 && value?.trim().length !== 0
);
useEffect(() => {
if (!fields.length) {
append({ key: "", value: "" }, { shouldFocus: false });
}
}, [fields]);
return (
<> <>
<Flex direction={{ default: "column" }}> <Grid hasGutter>
<Flex> <GridItem className="pf-c-form__label" span={6}>
<FlexItem <span className="pf-c-form__label-text">{t("key")}</span>
grow={{ default: "grow" }} </GridItem>
spacer={{ default: "spacerNone" }} <GridItem className="pf-c-form__label" span={6}>
> <span className="pf-c-form__label-text">{t("value")}</span>
<strong>{t("key")}</strong> </GridItem>
</FlexItem> {fields.map((attribute, index) => {
<FlexItem grow={{ default: "grow" }}> const keyError = !!(errors as any)[name]?.[index]?.key;
<strong>{t("value")}</strong> const valueError = !!(errors as any)[name]?.[index]?.value;
</FlexItem>
</Flex> return (
{fields.map((attribute, index) => ( <Fragment key={attribute.id}>
<Flex key={attribute.id} data-testid="row"> <GridItem span={6}>
<FlexItem grow={{ default: "grow" }}> <KeycloakTextInput
<KeycloakTextInput placeholder={t("keyPlaceholder")}
placeholder={t("keyPlaceholder")} aria-label={t("key")}
aria-label={t("key")} data-testid={`${name}-key`}
defaultValue="" {...register(`${name}.${index}.key`, { required: true })}
data-testid={`${name}[${index}].key`} validated={keyError ? "error" : "default"}
{...register(`${name}[${index}].key`)} isRequired
/> />
</FlexItem> {keyError && (
<FlexItem <HelperText>
grow={{ default: "grow" }} <HelperTextItem variant="error">
spacer={{ default: "spacerNone" }} {t("keyError")}
> </HelperTextItem>
<KeycloakTextInput </HelperText>
placeholder={t("valuePlaceholder")} )}
aria-label={t("value")} </GridItem>
defaultValue="" <GridItem span={6}>
data-testid={`${name}[${index}].value`} <InputGroup>
{...register(`${name}[${index}].value`)} <KeycloakTextInput
/> placeholder={t("valuePlaceholder")}
</FlexItem> aria-label={t("value")}
<FlexItem> data-testid={`${name}-value`}
<Button {...register(`${name}.${index}.value`, { required: true })}
variant="link" validated={valueError ? "error" : "default"}
title={t("removeAttribute")} isRequired
isDisabled={watchFields.length === 1} />
onClick={() => remove(index)} <Button
data-testid={`${name}[${index}].remove`} variant="link"
> title={t("removeAttribute")}
<MinusCircleIcon /> onClick={() => remove(index)}
</Button> data-testid={`${name}-remove`}
</FlexItem> >
</Flex> <MinusCircleIcon />
))} </Button>
</Flex> </InputGroup>
{valueError && (
<HelperText>
<HelperTextItem variant="error">
{t("valueError")}
</HelperTextItem>
</HelperText>
)}
</GridItem>
</Fragment>
);
})}
</Grid>
<ActionList> <ActionList>
<ActionListItem> <ActionListItem>
<Button <Button
@ -103,13 +106,29 @@ export const KeyValueInput = ({ name }: KeyValueInputProps) => {
className="pf-u-px-0 pf-u-mt-sm" className="pf-u-px-0 pf-u-mt-sm"
variant="link" variant="link"
icon={<PlusCircleIcon />} icon={<PlusCircleIcon />}
isDisabled={!isValid} onClick={appendNew}
onClick={() => append({ key: "", value: "" })}
> >
{t("addAttribute")} {t("addAttribute")}
</Button> </Button>
</ActionListItem> </ActionListItem>
</ActionList> </ActionList>
</> </>
) : (
<EmptyState
data-testid={`${name}-empty-state`}
className="pf-u-p-0"
variant="xs"
>
<EmptyStateBody>{t("missingAttributes")}</EmptyStateBody>
<Button
data-testid={`${name}-add-row`}
variant="link"
icon={<PlusCircleIcon />}
isSmall
onClick={appendNew}
>
{t("addAttribute")}
</Button>
</EmptyState>
); );
}; };

View file

@ -41,18 +41,6 @@ describe("Tests the convert functions for attribute input", () => {
expect(result).toEqual({ theKey: ["theValue"] }); expect(result).toEqual({ theKey: ["theValue"] });
}); });
it("convert empty object to attributes", () => {
const given: {
[key: string]: string[];
} = {};
//when
const result = arrayToKeyValue(given);
//then
expect(result).toEqual([{ key: "", value: "" }]);
});
it("convert object to attributes", () => { it("convert object to attributes", () => {
const given = { one: ["1"], two: ["2"] }; const given = { one: ["1"], two: ["2"] };
@ -63,7 +51,6 @@ describe("Tests the convert functions for attribute input", () => {
expect(result).toEqual([ expect(result).toEqual([
{ key: "one", value: "1" }, { key: "one", value: "1" },
{ key: "two", value: "2" }, { key: "two", value: "2" },
{ key: "", value: "" },
]); ]);
}); });

View file

@ -22,5 +22,5 @@ export function arrayToKeyValue<T>(attributes: Record<string, string[]> = {}) {
value.map<KeyValueType>((value) => ({ key, value })) value.map<KeyValueType>((value) => ({ key, value }))
); );
return result.concat({ key: "", value: "" }) as PathValue<T, Path<T>>; return result as PathValue<T, Path<T>>;
} }

View file

@ -85,7 +85,7 @@ export const MultiLineInput = ({
<TextInput <TextInput
data-testid={name + index} data-testid={name + index}
onChange={(value) => updateValue(index, value)} onChange={(value) => updateValue(index, value)}
name={`${name}[${index}].value`} name={`${name}.${index}.value`}
value={value} value={value}
isDisabled={isDisabled} isDisabled={isDisabled}
{...rest} {...rest}

View file

@ -26,10 +26,7 @@ describe("Tests the form convert util functions", () => {
expect(values).toEqual({ expect(values).toEqual({
name: "client", name: "client",
other: { one: "1", two: "2" }, other: { one: "1", two: "2" },
attributes: [ attributes: [{ key: "one", value: "1" }],
{ key: "one", value: "1" },
{ key: "", value: "" },
],
}); });
}); });