Created
May 16, 2022 19:31
-
-
Save truh/ce6a1b93bdd0b7533d2d65f5c5290300 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from "react"; | |
import PropTypes from "prop-types"; | |
import IconButton from "@rjsf/core/lib/components/IconButton"; | |
import { | |
ADDITIONAL_PROPERTY_FLAG, | |
deepEquals, | |
getDisplayLabel, | |
getSchemaType, | |
isSelect, | |
mergeObjects, | |
retrieveSchema, | |
toIdSchema, | |
} from "@rjsf/core/lib/utils"; | |
import * as types from "@rjsf/core/lib/types"; | |
const NullComponent = () => null; | |
const REQUIRED_FIELD_SYMBOL = "*"; | |
const COMPONENT_TYPES = { | |
array: "ArrayField", | |
boolean: "BooleanField", | |
integer: "NumberField", | |
number: "NumberField", | |
object: "ObjectField", | |
string: "StringField", | |
null: "NullField", | |
}; | |
function getFieldComponent(schema, uiSchema, idSchema, fields) { | |
const field = uiSchema["ui:field"]; | |
if (typeof field === "function") { | |
return field; | |
} | |
if (typeof field === "string" && field in fields) { | |
return fields[field]; | |
} | |
const componentName = COMPONENT_TYPES[getSchemaType(schema)]; | |
// If the type is not defined and the schema uses 'anyOf' or 'oneOf', don't | |
// render a field and let the MultiSchemaField component handle the form display | |
if (!componentName && (schema.anyOf || schema.oneOf)) { | |
return NullComponent; | |
} | |
return componentName in fields | |
? fields[componentName] | |
: () => { | |
const { UnsupportedField } = fields; | |
return ( | |
<UnsupportedField | |
schema={schema} | |
idSchema={idSchema} | |
reason={`Unknown field type ${schema.type}`} | |
/> | |
); | |
}; | |
} | |
function Label(props) { | |
const { label, required, id } = props; | |
if (!label) { | |
return null; | |
} | |
return ( | |
<label className="control-label" htmlFor={id}> | |
{label} | |
{required && <span className="required">{REQUIRED_FIELD_SYMBOL}</span>} | |
</label> | |
); | |
} | |
function LabelInput(props) { | |
const { id, label, onChange } = props; | |
return ( | |
<input | |
className="form-control" | |
type="text" | |
id={id} | |
onBlur={(event) => onChange(event.target.value)} | |
defaultValue={label} | |
/> | |
); | |
} | |
function Help(props) { | |
const { id, help } = props; | |
if (!help) { | |
return null; | |
} | |
if (typeof help === "string") { | |
return ( | |
<p id={id} className="help-block"> | |
{help} | |
</p> | |
); | |
} | |
return ( | |
<div id={id} className="help-block"> | |
{help} | |
</div> | |
); | |
} | |
function ErrorList(props) { | |
const { errors = [] } = props; | |
if (errors.length === 0) { | |
return null; | |
} | |
return ( | |
<div> | |
<ul className="error-detail bs-callout bs-callout-info"> | |
{errors | |
.filter((elem) => !!elem) | |
.map((error, index) => { | |
return ( | |
<li className="text-danger" key={index}> | |
{error} | |
</li> | |
); | |
})} | |
</ul> | |
</div> | |
); | |
} | |
function DefaultTemplate(props) { | |
const { | |
id, | |
label, | |
children, | |
errors, | |
help, | |
description, | |
hidden, | |
required, | |
displayLabel, | |
} = props; | |
if (hidden) { | |
return <div className="hidden">{children}</div>; | |
} | |
return ( | |
<WrapIfAdditional {...props}> | |
{displayLabel && <Label label={label} required={required} id={id} />} | |
{displayLabel && description ? description : null} | |
{children} | |
{errors} | |
{help} | |
</WrapIfAdditional> | |
); | |
} | |
// eslint-disable-next-line no-undef | |
if (process.env.NODE_ENV !== "production") { | |
DefaultTemplate.propTypes = { | |
id: PropTypes.string, | |
classNames: PropTypes.string, | |
label: PropTypes.string, | |
children: PropTypes.node.isRequired, | |
errors: PropTypes.element, | |
rawErrors: PropTypes.arrayOf(PropTypes.string), | |
help: PropTypes.element, | |
rawHelp: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), | |
description: PropTypes.element, | |
rawDescription: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), | |
hidden: PropTypes.bool, | |
required: PropTypes.bool, | |
readonly: PropTypes.bool, | |
displayLabel: PropTypes.bool, | |
fields: PropTypes.object, | |
formContext: PropTypes.object, | |
}; | |
} | |
DefaultTemplate.defaultProps = { | |
hidden: false, | |
readonly: false, | |
required: false, | |
displayLabel: true, | |
}; | |
function WrapIfAdditional(props) { | |
const { | |
id, | |
classNames, | |
disabled, | |
label, | |
onKeyChange, | |
onDropPropertyClick, | |
readonly, | |
required, | |
schema, | |
} = props; | |
const keyLabel = `${label} Key`; // i18n ? | |
const additional = schema.hasOwnProperty(ADDITIONAL_PROPERTY_FLAG); | |
if (!additional) { | |
return <div className={classNames}>{props.children}</div>; | |
} | |
return ( | |
<div className={classNames}> | |
<div className="row"> | |
<div className="col-xs-5 form-additional"> | |
<div className="form-group"> | |
<Label label={keyLabel} required={required} id={`${id}-key`} /> | |
<LabelInput | |
label={label} | |
required={required} | |
id={`${id}-key`} | |
onChange={onKeyChange} | |
/> | |
</div> | |
</div> | |
<div className="form-additional form-group col-xs-5"> | |
{props.children} | |
</div> | |
<div className="col-xs-2"> | |
<IconButton | |
type="danger" | |
icon="remove" | |
className="array-item-remove btn-block" | |
tabIndex="-1" | |
style={{ border: "0" }} | |
disabled={disabled || readonly} | |
onClick={onDropPropertyClick(label)} | |
/> | |
</div> | |
</div> | |
</div> | |
); | |
} | |
function SchemaFieldRender(props) { | |
const { | |
uiSchema, | |
formData, | |
errorSchema, | |
idPrefix, | |
idSeparator, | |
name, | |
onChange, | |
onKeyChange, | |
onDropPropertyClick, | |
required, | |
registry, | |
wasPropertyKeyModified = false, | |
} = props; | |
const { rootSchema, fields, formContext } = registry; | |
const FieldTemplate = | |
uiSchema["ui:FieldTemplate"] || registry.FieldTemplate || DefaultTemplate; | |
let idSchema = props.idSchema; | |
const schema = retrieveSchema(props.schema, rootSchema, formData); | |
idSchema = mergeObjects( | |
toIdSchema(schema, null, rootSchema, formData, idPrefix, idSeparator), | |
idSchema, | |
); | |
const FieldComponent = getFieldComponent(schema, uiSchema, idSchema, fields); | |
const { DescriptionField } = fields; | |
const disabled = Boolean(props.disabled || uiSchema["ui:disabled"]); | |
const readonly = Boolean( | |
props.readonly || | |
uiSchema["ui:readonly"] || | |
props.schema.readOnly || | |
schema.readOnly, | |
); | |
const uiSchemaHideError = uiSchema["ui:hideError"]; | |
// Set hideError to the value provided in the uiSchema, otherwise stick with the prop to propagate to children | |
const hideError = | |
uiSchemaHideError === undefined | |
? props.hideError | |
: Boolean(uiSchemaHideError); | |
const autofocus = Boolean(props.autofocus || uiSchema["ui:autofocus"]); | |
if (Object.keys(schema).length === 0) { | |
return null; | |
} | |
const displayLabel = getDisplayLabel(schema, uiSchema, rootSchema); | |
const { __errors, ...fieldErrorSchema } = errorSchema; | |
// See #439: uiSchema: Don't pass consumed class names to child components | |
const field = ( | |
<FieldComponent | |
{...props} | |
idSchema={idSchema} | |
schema={schema} | |
uiSchema={{ ...uiSchema, classNames: undefined }} | |
disabled={disabled} | |
readonly={readonly} | |
hideError={hideError} | |
autofocus={autofocus} | |
errorSchema={fieldErrorSchema} | |
formContext={formContext} | |
rawErrors={__errors} | |
/> | |
); | |
const id = idSchema.$id; | |
// If this schema has a title defined, but the user has set a new key/label, retain their input. | |
let label; | |
if (wasPropertyKeyModified) { | |
label = name; | |
} else { | |
label = uiSchema["ui:title"] || props.schema.title || schema.title || name; | |
} | |
const description = | |
uiSchema["ui:description"] || | |
props.schema.description || | |
schema.description; | |
const errors = __errors; | |
const help = uiSchema["ui:help"]; | |
const hidden = uiSchema["ui:widget"] === "hidden"; | |
let classNames = ["form-group", "field", `field-${schema.type}`]; | |
if (!hideError && errors && errors.length > 0) { | |
classNames.push("field-error has-error has-danger"); | |
} | |
classNames.push(uiSchema.classNames); | |
classNames = classNames.join(" ").trim(); | |
const fieldProps = { | |
description: ( | |
<DescriptionField | |
id={id + "__description"} | |
description={description} | |
formContext={formContext} | |
/> | |
), | |
rawDescription: description, | |
help: <Help id={id + "__help"} help={help} />, | |
rawHelp: typeof help === "string" ? help : undefined, | |
errors: hideError ? undefined : <ErrorList errors={errors} />, | |
rawErrors: hideError ? undefined : errors, | |
id, | |
label, | |
hidden, | |
onChange, | |
onKeyChange, | |
onDropPropertyClick, | |
required, | |
disabled, | |
readonly, | |
hideError, | |
displayLabel, | |
classNames, | |
formContext, | |
formData, | |
fields, | |
schema, | |
uiSchema, | |
registry, | |
}; | |
const _AnyOfField = registry.fields.AnyOfField; | |
const _OneOfField = registry.fields.OneOfField; | |
return ( | |
<FieldTemplate {...fieldProps}> | |
<React.Fragment> | |
{field} | |
{/* | |
If the schema `anyOf` or 'oneOf' can be rendered as a select control, don't | |
render the selection and let `StringField` component handle | |
rendering | |
*/} | |
{FieldComponent === NullComponent && schema.anyOf && !isSelect(schema) && ( | |
// eslint-disable-next-line react/jsx-pascal-case | |
<_AnyOfField | |
disabled={disabled} | |
readonly={readonly} | |
hideError={hideError} | |
errorSchema={errorSchema} | |
formData={formData} | |
idPrefix={idPrefix} | |
idSchema={idSchema} | |
idSeparator={idSeparator} | |
onBlur={props.onBlur} | |
onChange={props.onChange} | |
onFocus={props.onFocus} | |
options={schema.anyOf.map((_schema) => | |
retrieveSchema(_schema, rootSchema, formData), | |
)} | |
baseType={schema.type} | |
registry={registry} | |
schema={schema} | |
uiSchema={uiSchema} | |
/> | |
)} | |
{FieldComponent === NullComponent && schema.oneOf && !isSelect(schema) && ( | |
// eslint-disable-next-line react/jsx-pascal-case | |
<_OneOfField | |
disabled={disabled} | |
readonly={readonly} | |
hideError={hideError} | |
errorSchema={errorSchema} | |
formData={formData} | |
idPrefix={idPrefix} | |
idSchema={idSchema} | |
idSeparator={idSeparator} | |
onBlur={props.onBlur} | |
onChange={props.onChange} | |
onFocus={props.onFocus} | |
options={schema.oneOf.map((_schema) => | |
retrieveSchema(_schema, rootSchema, formData), | |
)} | |
baseType={schema.type} | |
registry={registry} | |
schema={schema} | |
uiSchema={uiSchema} | |
/> | |
)} | |
</React.Fragment> | |
</FieldTemplate> | |
); | |
} | |
class SchemaField extends React.Component { | |
shouldComponentUpdate(nextProps, nextState) { | |
return !deepEquals(this.props, nextProps); | |
} | |
render() { | |
return SchemaFieldRender(this.props); | |
} | |
} | |
SchemaField.defaultProps = { | |
uiSchema: {}, | |
errorSchema: {}, | |
idSchema: {}, | |
disabled: false, | |
readonly: false, | |
autofocus: false, | |
hideError: false, | |
}; | |
// eslint-disable-next-line no-undef | |
if (process.env.NODE_ENV !== "production") { | |
SchemaField.propTypes = { | |
schema: PropTypes.object.isRequired, | |
uiSchema: PropTypes.object, | |
idSchema: PropTypes.object, | |
formData: PropTypes.any, | |
errorSchema: PropTypes.object, | |
registry: types.registry.isRequired, | |
}; | |
} | |
export default SchemaField; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment