diff --git a/CHANGELOG.md b/CHANGELOG.md index 0910098483..36245b6173 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,31 +23,39 @@ should change the heading of the (upcoming) version to include a major version b ## @rjsf/bootstrap-4 - Added support for new `style` prop on `FieldTemplate` and `WrapIfAdditionalTemplate` rendering them on the outermost wrapper, partially fixing [#1200](https://github.com/rjsf-team/react-jsonschema-form/issues/1200) +- Updated `CheckboxesWidget` to treat the value as an array when selecting/deselecting values and when determining the checked state - fixing [#2141](https://github.com/rjsf-team/react-jsonschema-form/issues/2141) ## @rjsf/chakra-ui - Added support for new `style` prop on `FieldTemplate` and `WrapIfAdditionalTemplate` rendering them on the outermost wrapper, partially fixing [#1200](https://github.com/rjsf-team/react-jsonschema-form/issues/1200) +- Updated `CheckboxesWidget` to treat the value as an array when selecting/deselecting values and when determining the checked state - fixing [#2141](https://github.com/rjsf-team/react-jsonschema-form/issues/2141) ## @rjsf/core - Updated `SchemaField` to handle the new `style` prop in the `uiSchema` similarly to `classNames`, passing it to the `FieldTemplate` and removing it from being passed down to children. - Also, added support for new `style` prop on `FieldTemplate` and `WrapIfAdditionalTemplate` rendering them on the outermost wrapper - This partially fixes [#1200](https://github.com/rjsf-team/react-jsonschema-form/issues/1200) +- Updated `CheckboxesWidget` to treat the value as an array when selecting/deselecting values and when determining the checked state - fixing [#2141](https://github.com/rjsf-team/react-jsonschema-form/issues/2141) ## @rjsf/fluent-ui - Added support for new `style` prop on `FieldTemplate` rendering them on the outermost wrapper, partially fixing [#1200](https://github.com/rjsf-team/react-jsonschema-form/issues/1200) +- Updated `CheckboxesWidget` to treat the value as an array when selecting/deselecting values and when determining the checked state - fixing [#2141](https://github.com/rjsf-team/react-jsonschema-form/issues/2141) ## @rjsf/material-ui - Updated `SelectWidget` to support additional `TextFieldProps` in a manner similar to how `BaseInputTemplate` does - Added support for new `style` prop on `FieldTemplate` and `WrapIfAdditionalTemplate` rendering them on the outermost wrapper, partially fixing [#1200](https://github.com/rjsf-team/react-jsonschema-form/issues/1200) +- Updated `CheckboxesWidget` to treat the value as an array when selecting/deselecting values and when determining the checked state - fixing [#2141](https://github.com/rjsf-team/react-jsonschema-form/issues/2141) ## @rjsf/mui - Updated `SelectWidget` to support additional `TextFieldProps` in a manner similar to how `BaseInputTemplate` does - Added support for new `style` prop on `FieldTemplate` and `WrapIfAdditionalTemplate` rendering them on the outermost wrapper, partially fixing [#1200](https://github.com/rjsf-team/react-jsonschema-form/issues/1200) +- Updated `CheckboxesWidget` to treat the value as an array when selecting/deselecting values and when determining the checked state - fixing [#2141](https://github.com/rjsf-team/react-jsonschema-form/issues/2141) ## @rjsf/semantic-ui - Added support for new `style` prop on `FieldTemplate` and `WrapIfAdditionalTemplate` rendering them on the outermost wrapper, partially fixing [#1200](https://github.com/rjsf-team/react-jsonschema-form/issues/1200) +- Updated `CheckboxesWidget` to treat the value as an array when selecting/deselecting values and when determining the checked state - fixing [#2141](https://github.com/rjsf-team/react-jsonschema-form/issues/2141) ## @rjsf/utils -- Updated the `FieldTemplateProps`, `WrapIfAdditionalTemplateProps` and `UIOptionsBaseType` types to add `style?: StyleHTMLAttributes`, partially fixing [#1200](https://github.com/rjsf-team/react-jsonschema-form/issues/1200) +- Updated the `FieldTemplateProps`, `WrapIfAdditionalTemplateProps` and `UIOptionsBaseType` types to add `style?: StyleHTMLAttributes`, partially fixing [#1200](https://github.com/rjsf-team/react-jsonschema-form/issues/1200) +- Added `enumOptionsDeselectValue()` and `enumOptionsSelectValue()` as a loose refactor of the duplicated functions in the various `CheckboxesWidget` implementations ## @rjsf/validator-ajv8 - Remove alias for ajv -> ajv8 in package.json. This fixes [#3215](https://github.com/rjsf-team/react-jsonschema-form/issues/3215). @@ -57,6 +65,7 @@ should change the heading of the (upcoming) version to include a major version b - In the playground, change Vite `preserveSymlinks` to `true`, which provides an alternative fix for [#3228](https://github.com/rjsf-team/react-jsonschema-form/issues/3228) since the prior fix caused [#3215](https://github.com/rjsf-team/react-jsonschema-form/issues/3215). - Updated the `custom-templates.md` and `uiSchema.md` to document the new `style` prop - Updated the `validation.md` documentation to describe the new `uiSchema` parameter passed to the `customValidate()` and `transformError()` functions +- Updated the `utility-functions` documentation to add the new `enumOptionsDeselectValue()` and `enumOptionsSelectValue()` functions # 5.0.0-beta-16 diff --git a/docs/api-reference/utility-functions.md b/docs/api-reference/utility-functions.md index 65b3b4c76b..7ff21cdddd 100644 --- a/docs/api-reference/utility-functions.md +++ b/docs/api-reference/utility-functions.md @@ -76,8 +76,28 @@ Implements a deep equals using the `lodash.isEqualWith` function, that provides #### Returns - boolean: True if the `a` and `b` are deeply equal, false otherwise -### findSchemaDefinition\() +### enumOptionsDeselectValue\() +Removes the `value` from the currently `selected` list of values. + +#### Parameters +- value: EnumOptionsType["value"] - The value that should be selected +- selected: EnumOptionsType["value"][] - The current list of selected values + +#### Returns +- EnumOptionsType["value"][]: The updated `selected` list with the `value` removed from it + +### enumOptionsSelectValue\() +Add the `value` to the list of `selected` values in the proper order as defined by `allEnumOptions`. +#### Parameters +- value: EnumOptionsType["value"] - The value that should be selected +- selected: EnumOptionsType["value"][] - The current list of selected values +- allEnumOptions: EnumOptionsType[] - The list of all the known enumOptions + +#### Returns +- EnumOptionsType["value"][]: The updated list of selected enum values with `value` added to it in the proper location + +### findSchemaDefinition\() Given the name of a `$ref` from within a schema, using the `rootSchema`, look up and return the sub-schema using the path provided by that reference. If `#` is not the first character of the reference, or the path does not exist in the schema, then throw an Error. Otherwise return the sub-schema. Also deals with nested `$ref`s in the sub-schema. diff --git a/packages/bootstrap-4/src/CheckboxesWidget/CheckboxesWidget.tsx b/packages/bootstrap-4/src/CheckboxesWidget/CheckboxesWidget.tsx index 38db6f7e02..d325398210 100644 --- a/packages/bootstrap-4/src/CheckboxesWidget/CheckboxesWidget.tsx +++ b/packages/bootstrap-4/src/CheckboxesWidget/CheckboxesWidget.tsx @@ -1,25 +1,14 @@ import React from "react"; import Form from "react-bootstrap/Form"; import { + enumOptionsDeselectValue, + enumOptionsSelectValue, FormContextType, RJSFSchema, StrictRJSFSchema, WidgetProps, } from "@rjsf/utils"; -const selectValue = (value: any, selected: any, all: any) => { - const at = all.indexOf(value); - const updated = selected.slice(0, at).concat(value, selected.slice(at)); - - // As inserting values at predefined index positions doesn't work with empty - // arrays, we need to reorder the updated selection to match the initial order - return updated.sort((a: any, b: any) => all.indexOf(a) > all.indexOf(b)); -}; - -const deselectValue = (value: any, selected: any) => { - return selected.filter((v: any) => v !== value); -}; - export default function CheckboxesWidget< T = any, S extends StrictRJSFSchema = RJSFSchema, @@ -37,16 +26,17 @@ export default function CheckboxesWidget< onFocus, }: WidgetProps) { const { enumOptions, enumDisabled, inline } = options; + const checkboxesValues = Array.isArray(value) ? value : [value]; const _onChange = (option: any) => ({ target: { checked } }: React.ChangeEvent) => { - const all = (enumOptions as any).map(({ value }: any) => value); - if (checked) { - onChange(selectValue(option.value, value, all)); + onChange( + enumOptionsSelectValue(option.value, checkboxesValues, enumOptions) + ); } else { - onChange(deselectValue(option.value, value)); + onChange(enumOptionsDeselectValue(option.value, checkboxesValues)); } }; @@ -60,7 +50,7 @@ export default function CheckboxesWidget< {Array.isArray(enumOptions) && enumOptions.map((option, index: number) => { - const checked = value.indexOf(option.value) !== -1; + const checked = checkboxesValues.includes(option.value); const itemDisabled = Array.isArray(enumDisabled) && enumDisabled.indexOf(option.value) !== -1; diff --git a/packages/bootstrap-4/test/__snapshots__/CheckboxesWidget.test.tsx.snap b/packages/bootstrap-4/test/__snapshots__/CheckboxesWidget.test.tsx.snap index 92d3fb4df5..1928973404 100644 --- a/packages/bootstrap-4/test/__snapshots__/CheckboxesWidget.test.tsx.snap +++ b/packages/bootstrap-4/test/__snapshots__/CheckboxesWidget.test.tsx.snap @@ -9,7 +9,7 @@ exports[`CheckboxesWidget inline 1`] = ` > {Array.isArray(enumOptions) && enumOptions.map((option) => { - const checked = value.indexOf(option.value) !== -1; + const checked = checkboxesValues.includes(option.value); const itemDisabled = Array.isArray(enumDisabled) && enumDisabled.indexOf(option.value) !== -1; diff --git a/packages/core/src/components/widgets/CheckboxesWidget.tsx b/packages/core/src/components/widgets/CheckboxesWidget.tsx index 2040e1aa69..9c886b4404 100644 --- a/packages/core/src/components/widgets/CheckboxesWidget.tsx +++ b/packages/core/src/components/widgets/CheckboxesWidget.tsx @@ -1,23 +1,13 @@ import React, { ChangeEvent } from "react"; import { + enumOptionsDeselectValue, + enumOptionsSelectValue, FormContextType, WidgetProps, RJSFSchema, StrictRJSFSchema, } from "@rjsf/utils"; -function selectValue(value: any, selected: any[], all: any[]) { - const at = all.indexOf(value); - const updated = selected.slice(0, at).concat(value, selected.slice(at)); - // As inserting values at predefined index positions doesn't work with empty - // arrays, we need to reorder the updated selection to match the initial order - return updated.sort((a, b) => Number(all.indexOf(a) > all.indexOf(b))); -} - -function deselectValue(value: any, selected: any[]) { - return selected.filter((v) => v !== value); -} - /** The `CheckboxesWidget` is a widget for rendering checkbox groups. * It is typically used to represent an array of enums. * @@ -36,11 +26,12 @@ function CheckboxesWidget< readonly, onChange, }: WidgetProps) { + const checkboxesValues = Array.isArray(value) ? value : [value]; return (
{Array.isArray(enumOptions) && enumOptions.map((option, index) => { - const checked = value.indexOf(option.value) !== -1; + const checked = checkboxesValues.includes(option.value); const itemDisabled = Array.isArray(enumDisabled) && enumDisabled.indexOf(option.value) != -1; @@ -48,11 +39,18 @@ function CheckboxesWidget< disabled || itemDisabled || readonly ? "disabled" : ""; const handleChange = (event: ChangeEvent) => { - const all = enumOptions.map(({ value }) => value); if (event.target.checked) { - onChange(selectValue(option.value, value, all)); + onChange( + enumOptionsSelectValue( + option.value, + checkboxesValues, + enumOptions + ) + ); } else { - onChange(deselectValue(option.value, value)); + onChange( + enumOptionsDeselectValue(option.value, checkboxesValues) + ); } }; diff --git a/packages/core/test/ArrayField_test.js b/packages/core/test/ArrayField_test.js index 3bab538173..35f315422c 100644 --- a/packages/core/test/ArrayField_test.js +++ b/packages/core/test/ArrayField_test.js @@ -1344,7 +1344,38 @@ describe("ArrayField", () => { ); }); - it("should fill field with data", () => { + it("should fill properly field with data that is not an array and handle change event", () => { + const { node, onChange } = createFormComponent({ + schema, + uiSchema, + formData: "foo", + }); + + let labels = [].map.call( + node.querySelectorAll("[type=checkbox]"), + (node) => node.checked + ); + expect(labels).eql([true, false, false]); + + Simulate.change(node.querySelectorAll("[type=checkbox]")[2], { + target: { checked: true }, + }); + + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: ["foo", "fuzz"], + }, + "root" + ); + labels = [].map.call( + node.querySelectorAll("[type=checkbox]"), + (node) => node.checked + ); + expect(labels).eql([true, false, true]); + }); + + it("should fill field with array of data", () => { const { node } = createFormComponent({ schema, uiSchema, diff --git a/packages/fluent-ui/src/CheckboxesWidget/CheckboxesWidget.tsx b/packages/fluent-ui/src/CheckboxesWidget/CheckboxesWidget.tsx index 57d2cd507f..3639c47ec3 100644 --- a/packages/fluent-ui/src/CheckboxesWidget/CheckboxesWidget.tsx +++ b/packages/fluent-ui/src/CheckboxesWidget/CheckboxesWidget.tsx @@ -1,6 +1,8 @@ import React from "react"; import { Checkbox, Label } from "@fluentui/react"; import { + enumOptionsDeselectValue, + enumOptionsSelectValue, FormContextType, RJSFSchema, StrictRJSFSchema, @@ -17,19 +19,6 @@ const styles_red = { fontFamily: `"Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;`, }; -const selectValue = (value: any, selected: any, all: any) => { - const at = all.indexOf(value); - const updated = selected.slice(0, at).concat(value, selected.slice(at)); - - // As inserting values at predefined index positions doesn't work with empty - // arrays, we need to reorder the updated selection to match the initial order - return updated.sort((a: any, b: any) => all.indexOf(a) > all.indexOf(b)); -}; - -const deselectValue = (value: any, selected: any) => { - return selected.filter((v: any) => v !== value); -}; - export default function CheckboxesWidget< T = any, S extends StrictRJSFSchema = RJSFSchema, @@ -50,16 +39,17 @@ export default function CheckboxesWidget< rawErrors = [], }: WidgetProps) { const { enumOptions, enumDisabled } = options; + const checkboxesValues = Array.isArray(value) ? value : [value]; const _onChange = (option: any) => (_ev?: React.FormEvent, checked?: boolean) => { - const all = (enumOptions as any).map(({ value }: any) => value); - if (checked) { - onChange(selectValue(option.value, value, all)); + onChange( + enumOptionsSelectValue(option.value, checkboxesValues, enumOptions) + ); } else { - onChange(deselectValue(option.value, value)); + onChange(enumOptionsDeselectValue(option.value, checkboxesValues)); } }; @@ -81,7 +71,7 @@ export default function CheckboxesWidget< {Array.isArray(enumOptions) && enumOptions.map((option, index: number) => { - const checked = value.indexOf(option.value) !== -1; + const checked = checkboxesValues.includes(option.value); const itemDisabled = Array.isArray(enumDisabled) && enumDisabled.indexOf(option.value) !== -1; diff --git a/packages/material-ui/src/CheckboxesWidget/CheckboxesWidget.tsx b/packages/material-ui/src/CheckboxesWidget/CheckboxesWidget.tsx index 0848816be0..c58b912400 100644 --- a/packages/material-ui/src/CheckboxesWidget/CheckboxesWidget.tsx +++ b/packages/material-ui/src/CheckboxesWidget/CheckboxesWidget.tsx @@ -4,25 +4,14 @@ import FormControlLabel from "@material-ui/core/FormControlLabel"; import FormGroup from "@material-ui/core/FormGroup"; import FormLabel from "@material-ui/core/FormLabel"; import { + enumOptionsDeselectValue, + enumOptionsSelectValue, FormContextType, WidgetProps, RJSFSchema, StrictRJSFSchema, } from "@rjsf/utils"; -const selectValue = (value: any, selected: any, all: any) => { - const at = all.indexOf(value); - const updated = selected.slice(0, at).concat(value, selected.slice(at)); - - // As inserting values at predefined index positions doesn't work with empty - // arrays, we need to reorder the updated selection to match the initial order - return updated.sort((a: any, b: any) => all.indexOf(a) > all.indexOf(b)); -}; - -const deselectValue = (value: any, selected: any) => { - return selected.filter((v: any) => v !== value); -}; - /** The `CheckboxesWidget` is a widget for rendering checkbox groups. * It is typically used to represent an array of enums. * @@ -47,16 +36,17 @@ export default function CheckboxesWidget< onFocus, }: WidgetProps) { const { enumOptions, enumDisabled, inline } = options; + const checkboxesValues = Array.isArray(value) ? value : [value]; const _onChange = (option: any) => ({ target: { checked } }: React.ChangeEvent) => { - const all = (enumOptions as any).map(({ value }: any) => value); - if (checked) { - onChange(selectValue(option.value, value, all)); + onChange( + enumOptionsSelectValue(option.value, checkboxesValues, enumOptions) + ); } else { - onChange(deselectValue(option.value, value)); + onChange(enumOptionsDeselectValue(option.value, checkboxesValues)); } }; @@ -75,7 +65,7 @@ export default function CheckboxesWidget< {Array.isArray(enumOptions) && enumOptions.map((option, index: number) => { - const checked = value.indexOf(option.value) !== -1; + const checked = checkboxesValues.includes(option.value); const itemDisabled = Array.isArray(enumDisabled) && enumDisabled.indexOf(option.value) !== -1; diff --git a/packages/mui/src/CheckboxesWidget/CheckboxesWidget.tsx b/packages/mui/src/CheckboxesWidget/CheckboxesWidget.tsx index e02f105fcf..e46e064b1e 100644 --- a/packages/mui/src/CheckboxesWidget/CheckboxesWidget.tsx +++ b/packages/mui/src/CheckboxesWidget/CheckboxesWidget.tsx @@ -4,25 +4,14 @@ import FormControlLabel from "@mui/material/FormControlLabel"; import FormGroup from "@mui/material/FormGroup"; import FormLabel from "@mui/material/FormLabel"; import { + enumOptionsDeselectValue, + enumOptionsSelectValue, FormContextType, WidgetProps, RJSFSchema, StrictRJSFSchema, } from "@rjsf/utils"; -const selectValue = (value: any, selected: any, all: any) => { - const at = all.indexOf(value); - const updated = selected.slice(0, at).concat(value, selected.slice(at)); - - // As inserting values at predefined index positions doesn't work with empty - // arrays, we need to reorder the updated selection to match the initial order - return updated.sort((a: any, b: any) => all.indexOf(a) > all.indexOf(b)); -}; - -const deselectValue = (value: any, selected: any) => { - return selected.filter((v: any) => v !== value); -}; - /** The `CheckboxesWidget` is a widget for rendering checkbox groups. * It is typically used to represent an array of enums. * @@ -47,16 +36,17 @@ export default function CheckboxesWidget< onFocus, }: WidgetProps) { const { enumOptions, enumDisabled, inline } = options; + const checkboxesValues = Array.isArray(value) ? value : [value]; const _onChange = (option: any) => ({ target: { checked } }: React.ChangeEvent) => { - const all = (enumOptions as any).map(({ value }: any) => value); - if (checked) { - onChange(selectValue(option.value, value, all)); + onChange( + enumOptionsSelectValue(option.value, checkboxesValues, enumOptions) + ); } else { - onChange(deselectValue(option.value, value)); + onChange(enumOptionsDeselectValue(option.value, checkboxesValues)); } }; @@ -75,7 +65,7 @@ export default function CheckboxesWidget< {Array.isArray(enumOptions) && enumOptions.map((option, index: number) => { - const checked = value.indexOf(option.value) !== -1; + const checked = checkboxesValues.includes(option.value); const itemDisabled = Array.isArray(enumDisabled) && enumDisabled.indexOf(option.value) !== -1; diff --git a/packages/semantic-ui/src/CheckboxesWidget/CheckboxesWidget.tsx b/packages/semantic-ui/src/CheckboxesWidget/CheckboxesWidget.tsx index d642f16a30..a7a41ae4ed 100644 --- a/packages/semantic-ui/src/CheckboxesWidget/CheckboxesWidget.tsx +++ b/packages/semantic-ui/src/CheckboxesWidget/CheckboxesWidget.tsx @@ -1,34 +1,17 @@ import React from "react"; import { Form } from "semantic-ui-react"; import { - getTemplate, EnumOptionsType, FormContextType, + getTemplate, RJSFSchema, + enumOptionsDeselectValue, + enumOptionsSelectValue, StrictRJSFSchema, WidgetProps, } from "@rjsf/utils"; import { getSemanticProps } from "../util"; -function selectValue( - value: EnumOptionsType["value"], - selected: any, - all: any[] -) { - const at = all.indexOf(value); - const updated = selected.slice(0, at).concat(value, selected.slice(at)); - // As inserting values at predefined index positions doesn't work with empty - // arrays, we need to reorder the updated selection to match the initial order - return updated.sort((a: any, b: any) => all.indexOf(a) > all.indexOf(b)); -} - -function deselectValue( - value: EnumOptionsType["value"], - selected: any -) { - return selected.filter((v: any) => v !== value); -} - /** The `CheckboxesWidget` is a widget for rendering checkbox groups. * It is typically used to represent an array of enums. * @@ -61,6 +44,7 @@ export default function CheckboxesWidget< options ); const { enumOptions, enumDisabled, inline } = options; + const checkboxesValues = Array.isArray(value) ? value : [value]; const { title } = schema; const semanticProps = getSemanticProps({ options, @@ -74,11 +58,12 @@ export default function CheckboxesWidget< (option: EnumOptionsType) => ({ target: { checked } }: React.ChangeEvent) => { // eslint-disable-next-line no-shadow - const all = enumOptions ? enumOptions.map(({ value }) => value) : []; if (checked) { - onChange(selectValue(option.value, value, all)); + onChange( + enumOptionsSelectValue(option.value, checkboxesValues, enumOptions) + ); } else { - onChange(deselectValue(option.value, value)); + onChange(enumOptionsDeselectValue(option.value, checkboxesValues)); } }; @@ -99,7 +84,7 @@ export default function CheckboxesWidget< {Array.isArray(enumOptions) && enumOptions.map((option, index) => { - const checked = value.indexOf(option.value) !== -1; + const checked = checkboxesValues.includes(option.value); const itemDisabled = Array.isArray(enumDisabled) && enumDisabled.indexOf(option.value) !== -1; diff --git a/packages/utils/src/enumOptionsDeselectValue.ts b/packages/utils/src/enumOptionsDeselectValue.ts new file mode 100644 index 0000000000..c8ffa60009 --- /dev/null +++ b/packages/utils/src/enumOptionsDeselectValue.ts @@ -0,0 +1,13 @@ +import { EnumOptionsType, RJSFSchema, StrictRJSFSchema } from "./types"; + +/** Removes the `value` from the currently `selected` list of values + * + * @param value - The value to be removed from the selected list + * @param selected - The current list of selected values + * @returns - The updated `selected` list with the `value` removed from it + */ +export default function enumOptionsDeselectValue< + S extends StrictRJSFSchema = RJSFSchema +>(value: EnumOptionsType["value"], selected: EnumOptionsType["value"][]) { + return selected.filter((v) => v !== value); +} diff --git a/packages/utils/src/enumOptionsSelectValue.ts b/packages/utils/src/enumOptionsSelectValue.ts new file mode 100644 index 0000000000..6f07820308 --- /dev/null +++ b/packages/utils/src/enumOptionsSelectValue.ts @@ -0,0 +1,27 @@ +import { EnumOptionsType, RJSFSchema, StrictRJSFSchema } from "./types"; + +/** Add the `value` to the list of `selected` values in the proper order as defined by `allEnumOptions` + * + * @param value - The value that should be selected + * @param selected - The current list of selected values + * @param allEnumOptions - The list of all the known enumOptions + * @returns - The updated list of selected enum values with `value` added to it in the proper location + */ +export default function enumOptionsSelectValue< + S extends StrictRJSFSchema = RJSFSchema +>( + value: EnumOptionsType["value"], + selected: EnumOptionsType["value"][], + allEnumOptions: EnumOptionsType[] = [] +) { + const all = allEnumOptions.map(({ value }) => value); + const at = all.indexOf(value); + // If location of the value is not in the list of all enum values, just put it at the end + const updated = + at === -1 + ? selected.concat(value) + : selected.slice(0, at).concat(value, selected.slice(at)); + // As inserting values at predefined index positions doesn't work with empty + // arrays, we need to reorder the updated selection to match the initial order + return updated.sort((a, b) => Number(all.indexOf(a) > all.indexOf(b))); +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index b4e447d423..0a91754b31 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -4,6 +4,8 @@ import canExpand from "./canExpand"; import createSchemaUtils from "./createSchemaUtils"; import dataURItoBlob from "./dataURItoBlob"; import deepEquals from "./deepEquals"; +import enumOptionsDeselectValue from "./enumOptionsDeselectValue"; +import enumOptionsSelectValue from "./enumOptionsSelectValue"; import ErrorSchemaBuilder from "./ErrorSchemaBuilder"; import findSchemaDefinition from "./findSchemaDefinition"; import getInputProps from "./getInputProps"; @@ -46,6 +48,8 @@ export { createSchemaUtils, dataURItoBlob, deepEquals, + enumOptionsDeselectValue, + enumOptionsSelectValue, ErrorSchemaBuilder, findSchemaDefinition, getInputProps, diff --git a/packages/utils/test/enumOptionsDeselectValue.test.ts b/packages/utils/test/enumOptionsDeselectValue.test.ts new file mode 100644 index 0000000000..c710673de9 --- /dev/null +++ b/packages/utils/test/enumOptionsDeselectValue.test.ts @@ -0,0 +1,21 @@ +import { enumOptionsDeselectValue, EnumOptionsType } from "../src"; + +const ALL_VALUES: EnumOptionsType["value"][] = ["foo", "bar", "baz"]; + +describe("enumOptionsDeselectValue", () => { + let selected: EnumOptionsType["value"][]; + it("removes a value from a selected list", () => { + const value = ALL_VALUES[1]; + selected = enumOptionsDeselectValue(value, ALL_VALUES); + expect(selected).toEqual([ALL_VALUES[0], ALL_VALUES[2]]); + }); + it("removes a second value from a selected list", () => { + const value = ALL_VALUES[0]; + selected = enumOptionsDeselectValue(value, selected); + expect(selected).toEqual([ALL_VALUES[2]]); + }); + it("removes a third value from a selected list", () => { + const value = ALL_VALUES[2]; + expect(enumOptionsDeselectValue(value, selected)).toEqual([]); + }); +}); diff --git a/packages/utils/test/enumOptionsSelectValue.test.ts b/packages/utils/test/enumOptionsSelectValue.test.ts new file mode 100644 index 0000000000..b2c19aeaa4 --- /dev/null +++ b/packages/utils/test/enumOptionsSelectValue.test.ts @@ -0,0 +1,62 @@ +import { enumOptionsSelectValue, EnumOptionsType } from "../src"; + +const ALL_OPTIONS: EnumOptionsType[] = [ + { value: "foo", label: "Foo" }, + { value: "bar", label: "Bar" }, + { value: "baz", label: "Baz" }, + { value: "boo", label: "Boo" }, +]; + +describe("enumOptionsSelectValue", () => { + let selected: EnumOptionsType["value"][]; + describe("no enumOptions", () => { + it("adds a value to an empty list", () => { + const { value } = ALL_OPTIONS[2]; + selected = enumOptionsSelectValue(value, []); + expect(selected).toEqual([value]); + }); + it("adds a second value to an existing list in the correct position", () => { + const { value } = ALL_OPTIONS[0]; + const expected = [...selected, value]; + selected = enumOptionsSelectValue(value, selected); + expect(selected).toEqual(expected); + }); + it("adds a third value to an existing list in the correct position", () => { + const { value } = ALL_OPTIONS[3]; + const expected = [...selected, value]; + selected = enumOptionsSelectValue(value, selected); + expect(selected).toEqual(expected); + }); + it("adds the last value to an existing list in the correct position", () => { + const { value } = ALL_OPTIONS[1]; + const expected = [...selected, value]; + expect(enumOptionsSelectValue(value, selected)).toEqual(expected); + }); + }); + describe("enumOptions", () => { + it("adds a value to an empty list", () => { + const { value } = ALL_OPTIONS[2]; + selected = enumOptionsSelectValue(value, [], ALL_OPTIONS); + expect(selected).toEqual([value]); + }); + it("adds a second value to an existing list in the correct position", () => { + const { value } = ALL_OPTIONS[0]; + const expected = [value, ...selected]; + selected = enumOptionsSelectValue(value, selected, ALL_OPTIONS); + expect(selected).toEqual(expected); + }); + it("adds a third value to an existing list in the correct position", () => { + const { value } = ALL_OPTIONS[3]; + const expected = [...selected, value]; + selected = enumOptionsSelectValue(value, selected, ALL_OPTIONS); + expect(selected).toEqual(expected); + }); + it("adds the last value to an existing list in the correct position", () => { + const { value } = ALL_OPTIONS[1]; + const expected = ALL_OPTIONS.map(({ value }) => value); + expect(enumOptionsSelectValue(value, selected, ALL_OPTIONS)).toEqual( + expected + ); + }); + }); +});