Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update @rjsf/* to 5.22.4 #1744

Merged
merged 14 commits into from
Nov 16, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Refactor RadioGroupWidgetRJSF to use new enumMetadata and shared code…
…, the possibility to have number was removed as it doesn't make sense, and it is hard to cast to string safely, new ValueAdapter was created to handle conversion between boolean <-> string as react-aria RadioGroup only support string values, the previous logic (creating "value-1", "value-2") was cumbersome
  • Loading branch information
MarekBodinger committed Nov 14, 2024
commit 63d7baf688c8b2a2f7e134c2ba785bb71102b897
162 changes: 99 additions & 63 deletions next/components/forms/widget-wrappers/RadioGroupWidgetRJSF.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,78 @@
import { StrictRJSFSchema, WidgetProps } from '@rjsf/utils'
import WidgetWrapper from 'components/forms/widget-wrappers/WidgetWrapper'
import { WithEnumOptions } from 'forms-shared/form-utils/WithEnumOptions'
import { mergeEnumOptionsMetadata } from 'forms-shared/generator/optionItems'
import { RadioGroupUiOptions } from 'forms-shared/generator/uiOptionsTypes'
import React from 'react'
import React, { ReactNode, useMemo } from 'react'

import Radio from '../widget-components/RadioButton/Radio'
import RadioGroup from '../widget-components/RadioButton/RadioGroup'

type ValueType = string | number | boolean | undefined
type ValueType = string | boolean | undefined

interface RadioGroupWidgetRJSFProps extends WidgetProps {
options: RadioGroupUiOptions & Pick<WidgetProps['options'], 'enumOptions'>
options: WithEnumOptions<RadioGroupUiOptions>
value: ValueType
errorMessage?: string
schema: StrictRJSFSchema
onChange: (value?: ValueType) => void
}

interface ValueAdapterProps {
schema: StrictRJSFSchema
value: ValueType
onChange: (value: ValueType) => void
children: (props: { value: string | null; onChange: (value: string | null) => void }) => ReactNode
}

/**
* RadioGroup component only supports string as value, in RJSF we want to support both string and boolean.
* Therefore, if the value is boolean, we need to convert it to string before passing it to the RadioGroup component.
* It also handles conversion between null (RadioGroup) and undefined (RJSF).
*/
const ValueAdapter = ({ schema, value, onChange, children }: ValueAdapterProps) => {
if (schema.type === 'boolean') {
const mappedValue = typeof value === 'boolean' ? value.toString() : null
const handleChange = (newValue: string | null) => {
if (newValue === null) {
// eslint-disable-next-line unicorn/no-useless-undefined
onChange(undefined)
return
}
if (newValue === 'true') {
onChange(true)
return
}
if (newValue === 'false') {
onChange(false)
return
}
// eslint-disable-next-line unicorn/no-useless-undefined
onChange(undefined)
}

return <>{children({ value: mappedValue, onChange: handleChange })}</>
}
if (schema.type === 'string') {
const mappedValue = value === undefined ? null : (value as string)
const handleChange = (newValue: string | null) => {
if (newValue === null) {
// eslint-disable-next-line unicorn/no-useless-undefined
onChange(undefined)
return
}
onChange(newValue)
}

return <>{children({ value: mappedValue, onChange: handleChange })}</>
}

return null
}

const RadioGroupWidgetRJSF = ({
id,
schema,
options,
value,
onChange,
Expand All @@ -28,80 +83,61 @@ const RadioGroupWidgetRJSF = ({
}: RadioGroupWidgetRJSFProps) => {
const {
enumOptions,
enumMetadata,
className,
variant,
radioOptions = [],
orientations,
size,
labelSize,
helptext,
helptextHeader,
} = options

if (!enumOptions) return null

// RadioGroup doesn't support any other value than string, so we need to map the values to strings.
// It also doesn't support undefined, so we need to map it to null.
const valueIndex = value == null ? -1 : enumOptions.findIndex((option) => option.value === value)
const valueMapped = valueIndex === -1 ? null : `value-${valueIndex}`

// In the onChange handler we need to map the string value back to the original value.
const handleChange = (value: string | null) => {
if (value == null) {
onChange()
return
}

const match = value.match(/^value-(\d+)$/)

if (!match) {
return
}

const index = parseInt(match[1], 10)
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
onChange(enumOptions[index].value)
}
const mergedOptions = useMemo(
() => mergeEnumOptionsMetadata(enumOptions, enumMetadata),
[enumOptions, enumMetadata],
)

const radioGroupHasDescription = radioOptions.some((option) => option.description)
const radioGroupHasDescription = mergedOptions.some((option) => option.description)

return (
<WidgetWrapper id={id} options={options}>
<RadioGroup
errorMessage={rawErrors}
value={valueMapped}
onChange={handleChange}
className={className}
label={label}
orientation={orientations === 'row' ? 'horizontal' : 'vertical'}
required={required}
disabled={readonly}
size={size}
labelSize={labelSize}
helptext={helptext}
helptextHeader={helptextHeader}
displayOptionalLabel
>
{enumOptions.map((option, radioIndex: number) => {
const radioValue = `value-${radioIndex}`
const radioInOptions = radioOptions.find(
(innerOption) => innerOption.value === option.value,
)
const { description } = radioInOptions ?? {}
<ValueAdapter schema={schema} value={value} onChange={onChange}>
{({ value: wrapperValue, onChange: wrapperOnChange }) => (
<RadioGroup
errorMessage={rawErrors}
value={wrapperValue}
onChange={wrapperOnChange}
className={className}
label={label}
orientation={orientations === 'row' ? 'horizontal' : 'vertical'}
required={required}
disabled={readonly}
size={size}
labelSize={labelSize}
helptext={helptext}
helptextHeader={helptextHeader}
displayOptionalLabel
>
{mergedOptions.map((option) => {
const radioValue =
typeof option.value === 'boolean' ? option.value.toString() : option.value

return (
<Radio
key={radioValue}
variant={variant}
value={radioValue}
description={description}
radioGroupHasDescription={radioGroupHasDescription}
>
{option.label}
</Radio>
)
})}
</RadioGroup>
return (
<Radio
key={radioValue}
variant={variant}
value={radioValue}
description={option.description}
radioGroupHasDescription={radioGroupHasDescription}
>
{option.label}
</Radio>
)
})}
</RadioGroup>
)}
</ValueAdapter>
</WidgetWrapper>
)
}
Expand Down