|
| 1 | +import { |
| 2 | + FallbackFieldProps, |
| 3 | + FieldPathId, |
| 4 | + FormContextType, |
| 5 | + getTemplate, |
| 6 | + getUiOptions, |
| 7 | + hashObject, |
| 8 | + RJSFSchema, |
| 9 | + StrictRJSFSchema, |
| 10 | + toFieldPathId, |
| 11 | + TranslatableString, |
| 12 | + useDeepCompareMemo, |
| 13 | +} from '@rjsf/utils'; |
| 14 | +import { useMemo, useState } from 'react'; |
| 15 | +import { JSONSchema7TypeName } from 'json-schema'; |
| 16 | + |
| 17 | +/** |
| 18 | + * Get the schema for the type selection component. |
| 19 | + * @param title - The translated title for the type selection schema. |
| 20 | + */ |
| 21 | +function getFallbackTypeSelectionSchema(title: string): RJSFSchema { |
| 22 | + return { |
| 23 | + type: 'string', |
| 24 | + enum: ['string', 'number', 'boolean'], |
| 25 | + default: 'string', |
| 26 | + title: title, |
| 27 | + }; |
| 28 | +} |
| 29 | + |
| 30 | +/** |
| 31 | + * Determines the JSON Schema type of the given formData. |
| 32 | + * @param formData - The form data whose type is to be determined. |
| 33 | + */ |
| 34 | +function getTypeOfFormData(formData: any): JSONSchema7TypeName { |
| 35 | + const dataType = typeof formData; |
| 36 | + if (dataType === 'string' || dataType === 'number' || dataType === 'boolean') { |
| 37 | + return dataType; |
| 38 | + } |
| 39 | + // Treat everything else as a string |
| 40 | + return 'string'; |
| 41 | +} |
| 42 | + |
| 43 | +/** |
| 44 | + * Casts the given formData to the specified type. |
| 45 | + * @param formData - The form data to be casted. |
| 46 | + * @param newType - The target type to which the form data should be casted. |
| 47 | + */ |
| 48 | +function castToNewType<T = any>(formData: T, newType: JSONSchema7TypeName): T { |
| 49 | + switch (newType) { |
| 50 | + case 'string': |
| 51 | + return String(formData) as T; |
| 52 | + case 'number': { |
| 53 | + const castedNumber = Number(formData); |
| 54 | + return (isNaN(castedNumber) ? 0 : castedNumber) as T; |
| 55 | + } |
| 56 | + case 'boolean': |
| 57 | + return Boolean(formData) as T; |
| 58 | + default: |
| 59 | + return formData; |
| 60 | + } |
| 61 | +} |
| 62 | + |
| 63 | +/** |
| 64 | + * The `FallbackField` component is used to render a field for unsupported or unknown schema types. If |
| 65 | + * `useFallbackUiForUnsupportedType` is enabled in the `globalUiOptions`, it provides a type selector |
| 66 | + */ |
| 67 | +export default function FallbackField< |
| 68 | + T = any, |
| 69 | + S extends StrictRJSFSchema = RJSFSchema, |
| 70 | + F extends FormContextType = any, |
| 71 | +>(props: FallbackFieldProps<T, S, F>) { |
| 72 | + const { |
| 73 | + id, |
| 74 | + formData, |
| 75 | + displayLabel = true, |
| 76 | + schema, |
| 77 | + name, |
| 78 | + uiSchema, |
| 79 | + required, |
| 80 | + disabled = false, |
| 81 | + readonly = false, |
| 82 | + onBlur, |
| 83 | + onFocus, |
| 84 | + registry, |
| 85 | + fieldPathId, |
| 86 | + onChange, |
| 87 | + errorSchema, |
| 88 | + } = props; |
| 89 | + const { translateString, fields, globalFormOptions } = registry; |
| 90 | + const [type, setType] = useState<JSONSchema7TypeName>(getTypeOfFormData(formData)); |
| 91 | + |
| 92 | + const uiOptions = getUiOptions<T, S, F>(uiSchema); |
| 93 | + |
| 94 | + const typeSelectorInnerFieldPathId = useDeepCompareMemo<FieldPathId>( |
| 95 | + toFieldPathId('__internal_type_selector', globalFormOptions, fieldPathId), |
| 96 | + ); |
| 97 | + |
| 98 | + const schemaTitle = translateString(TranslatableString.Type); |
| 99 | + const typesOptionSchema = useMemo(() => getFallbackTypeSelectionSchema(schemaTitle), [schemaTitle]); |
| 100 | + |
| 101 | + const onTypeChange = (newType: T | undefined) => { |
| 102 | + if (newType != null) { |
| 103 | + setType(newType as JSONSchema7TypeName); |
| 104 | + onChange(castToNewType<T>(formData as T, newType as JSONSchema7TypeName), fieldPathId.path, errorSchema, id); |
| 105 | + } |
| 106 | + }; |
| 107 | + |
| 108 | + if (!globalFormOptions.useFallbackUiForUnsupportedType) { |
| 109 | + const UnsupportedFieldTemplate = getTemplate<'UnsupportedFieldTemplate', T, S, F>( |
| 110 | + 'UnsupportedFieldTemplate', |
| 111 | + registry, |
| 112 | + uiOptions, |
| 113 | + ); |
| 114 | + |
| 115 | + return ( |
| 116 | + <UnsupportedFieldTemplate |
| 117 | + schema={schema} |
| 118 | + fieldPathId={fieldPathId} |
| 119 | + reason={translateString(TranslatableString.UnknownFieldType, [String(schema.type)])} |
| 120 | + registry={registry} |
| 121 | + /> |
| 122 | + ); |
| 123 | + } |
| 124 | + |
| 125 | + const FallbackFieldTemplate = getTemplate<'FallbackFieldTemplate', T, S, F>( |
| 126 | + 'FallbackFieldTemplate', |
| 127 | + registry, |
| 128 | + uiOptions, |
| 129 | + ); |
| 130 | + |
| 131 | + const { SchemaField } = fields; |
| 132 | + |
| 133 | + return ( |
| 134 | + <FallbackFieldTemplate |
| 135 | + schema={schema} |
| 136 | + registry={registry} |
| 137 | + typeSelector={ |
| 138 | + <SchemaField |
| 139 | + key={formData ? hashObject(formData) : '__empty__'} |
| 140 | + fieldPathId={typeSelectorInnerFieldPathId} |
| 141 | + name={`${name}__fallback_type`} |
| 142 | + schema={typesOptionSchema as S} |
| 143 | + formData={type as T} |
| 144 | + onChange={onTypeChange} |
| 145 | + onBlur={onBlur} |
| 146 | + onFocus={onFocus} |
| 147 | + registry={registry} |
| 148 | + hideLabel={!displayLabel} |
| 149 | + disabled={disabled} |
| 150 | + readonly={readonly} |
| 151 | + required={required} |
| 152 | + /> |
| 153 | + } |
| 154 | + schemaField={<SchemaField {...props} schema={{ type, title: translateString(TranslatableString.Value) } as S} />} |
| 155 | + /> |
| 156 | + ); |
| 157 | +} |
0 commit comments