UNPKG

@rjsf/core

Version:

A simple React component capable of building HTML forms out of a JSON schema.

1,498 lines (1,491 loc) 136 kB
// src/components/Form.tsx import { Component as Component5, createRef } from "react"; import { createSchemaUtils, deepEquals as deepEquals3, getChangedFields, getTemplate as getTemplate20, getUiOptions as getUiOptions12, isObject as isObject5, mergeObjects as mergeObjects2, NAME_KEY, RJSF_ADDITIONAL_PROPERTIES_FLAG, shouldRender, SUBMIT_BTN_OPTIONS_KEY, toErrorList, UI_GLOBAL_OPTIONS_KEY, UI_OPTIONS_KEY as UI_OPTIONS_KEY2, validationDataMerge, createErrorHandler, unwrapErrorHandler } from "@rjsf/utils"; import _forEach from "lodash/forEach"; import _get from "lodash/get"; import _isEmpty from "lodash/isEmpty"; import _isNil from "lodash/isNil"; import _pick from "lodash/pick"; import _toPath from "lodash/toPath"; // src/getDefaultRegistry.ts import { englishStringTranslator } from "@rjsf/utils"; // src/components/fields/ArrayField.tsx import { Component } from "react"; import { getTemplate, getWidget, getUiOptions, isFixedItems, allowAdditionalItems, isCustomWidget, optionsList, TranslatableString, ITEMS_KEY } from "@rjsf/utils"; import cloneDeep from "lodash/cloneDeep"; import get from "lodash/get"; import isObject from "lodash/isObject"; import set from "lodash/set"; import uniqueId from "lodash/uniqueId"; import { jsx } from "react/jsx-runtime"; function generateRowId() { return uniqueId("rjsf-array-item-"); } function generateKeyedFormData(formData) { return !Array.isArray(formData) ? [] : formData.map((item) => { return { key: generateRowId(), item }; }); } function keyedToPlainFormData(keyedFormData) { if (Array.isArray(keyedFormData)) { return keyedFormData.map((keyedItem) => keyedItem.item); } return []; } var ArrayField = class extends Component { /** Constructs an `ArrayField` from the `props`, generating the initial keyed data from the `formData` * * @param props - The `FieldProps` for this template */ constructor(props) { super(props); const { formData = [] } = props; const keyedFormData = generateKeyedFormData(formData); this.state = { keyedFormData, updatedKeyedFormData: false }; } /** React lifecycle method that is called when the props are about to change allowing the state to be updated. It * regenerates the keyed form data and returns it * * @param nextProps - The next set of props data * @param prevState - The previous set of state data */ static getDerivedStateFromProps(nextProps, prevState) { if (prevState.updatedKeyedFormData) { return { updatedKeyedFormData: false }; } const nextFormData = Array.isArray(nextProps.formData) ? nextProps.formData : []; const previousKeyedFormData = prevState.keyedFormData || []; const newKeyedFormData = nextFormData.length === previousKeyedFormData.length ? previousKeyedFormData.map((previousKeyedFormDatum, index) => { return { key: previousKeyedFormDatum.key, item: nextFormData[index] }; }) : generateKeyedFormData(nextFormData); return { keyedFormData: newKeyedFormData }; } /** Returns the appropriate title for an item by getting first the title from the schema.items, then falling back to * the description from the schema.items, and finally the string "Item" */ get itemTitle() { const { schema, registry } = this.props; const { translateString } = registry; return get( schema, [ITEMS_KEY, "title"], get(schema, [ITEMS_KEY, "description"], translateString(TranslatableString.ArrayItemTitle)) ); } /** Determines whether the item described in the schema is always required, which is determined by whether any item * may be null. * * @param itemSchema - The schema for the item * @return - True if the item schema type does not contain the "null" type */ isItemRequired(itemSchema) { if (Array.isArray(itemSchema.type)) { return !itemSchema.type.includes("null"); } return itemSchema.type !== "null"; } /** Determines whether more items can be added to the array. If the uiSchema indicates the array doesn't allow adding * then false is returned. Otherwise, if the schema indicates that there are a maximum number of items and the * `formData` matches that value, then false is returned, otherwise true is returned. * * @param formItems - The list of items in the form * @returns - True if the item is addable otherwise false */ canAddItem(formItems) { const { schema, uiSchema, registry } = this.props; let { addable } = getUiOptions(uiSchema, registry.globalUiOptions); if (addable !== false) { if (schema.maxItems !== void 0) { addable = formItems.length < schema.maxItems; } else { addable = true; } } return addable; } /** Returns the default form information for an item based on the schema for that item. Deals with the possibility * that the schema is fixed and allows additional items. */ _getNewFormDataRow = () => { const { schema, registry } = this.props; const { schemaUtils } = registry; let itemSchema = schema.items; if (isFixedItems(schema) && allowAdditionalItems(schema)) { itemSchema = schema.additionalItems; } return schemaUtils.getDefaultFormState(itemSchema); }; /** Callback handler for when the user clicks on the add or add at index buttons. Creates a new row of keyed form data * either at the end of the list (when index is not specified) or inserted at the `index` when it is, adding it into * the state, and then returning `onChange()` with the plain form data converted from the keyed data * * @param event - The event for the click * @param [index] - The optional index at which to add the new data */ _handleAddClick(event, index) { if (event) { event.preventDefault(); } const { onChange, errorSchema } = this.props; const { keyedFormData } = this.state; let newErrorSchema; if (errorSchema) { newErrorSchema = {}; for (const idx in errorSchema) { const i = parseInt(idx); if (index === void 0 || i < index) { set(newErrorSchema, [i], errorSchema[idx]); } else if (i >= index) { set(newErrorSchema, [i + 1], errorSchema[idx]); } } } const newKeyedFormDataRow = { key: generateRowId(), item: this._getNewFormDataRow() }; const newKeyedFormData = [...keyedFormData]; if (index !== void 0) { newKeyedFormData.splice(index, 0, newKeyedFormDataRow); } else { newKeyedFormData.push(newKeyedFormDataRow); } this.setState( { keyedFormData: newKeyedFormData, updatedKeyedFormData: true }, () => onChange(keyedToPlainFormData(newKeyedFormData), newErrorSchema) ); } /** Callback handler for when the user clicks on the add button. Creates a new row of keyed form data at the end of * the list, adding it into the state, and then returning `onChange()` with the plain form data converted from the * keyed data * * @param event - The event for the click */ onAddClick = (event) => { this._handleAddClick(event); }; /** Callback handler for when the user clicks on the add button on an existing array element. Creates a new row of * keyed form data inserted at the `index`, adding it into the state, and then returning `onChange()` with the plain * form data converted from the keyed data * * @param index - The index at which the add button is clicked */ onAddIndexClick = (index) => { return (event) => { this._handleAddClick(event, index); }; }; /** Callback handler for when the user clicks on the copy button on an existing array element. Clones the row of * keyed form data at the `index` into the next position in the state, and then returning `onChange()` with the plain * form data converted from the keyed data * * @param index - The index at which the copy button is clicked */ onCopyIndexClick = (index) => { return (event) => { if (event) { event.preventDefault(); } const { onChange, errorSchema } = this.props; const { keyedFormData } = this.state; let newErrorSchema; if (errorSchema) { newErrorSchema = {}; for (const idx in errorSchema) { const i = parseInt(idx); if (i <= index) { set(newErrorSchema, [i], errorSchema[idx]); } else if (i > index) { set(newErrorSchema, [i + 1], errorSchema[idx]); } } } const newKeyedFormDataRow = { key: generateRowId(), item: cloneDeep(keyedFormData[index].item) }; const newKeyedFormData = [...keyedFormData]; if (index !== void 0) { newKeyedFormData.splice(index + 1, 0, newKeyedFormDataRow); } else { newKeyedFormData.push(newKeyedFormDataRow); } this.setState( { keyedFormData: newKeyedFormData, updatedKeyedFormData: true }, () => onChange(keyedToPlainFormData(newKeyedFormData), newErrorSchema) ); }; }; /** Callback handler for when the user clicks on the remove button on an existing array element. Removes the row of * keyed form data at the `index` in the state, and then returning `onChange()` with the plain form data converted * from the keyed data * * @param index - The index at which the remove button is clicked */ onDropIndexClick = (index) => { return (event) => { if (event) { event.preventDefault(); } const { onChange, errorSchema } = this.props; const { keyedFormData } = this.state; let newErrorSchema; if (errorSchema) { newErrorSchema = {}; for (const idx in errorSchema) { const i = parseInt(idx); if (i < index) { set(newErrorSchema, [i], errorSchema[idx]); } else if (i > index) { set(newErrorSchema, [i - 1], errorSchema[idx]); } } } const newKeyedFormData = keyedFormData.filter((_, i) => i !== index); this.setState( { keyedFormData: newKeyedFormData, updatedKeyedFormData: true }, () => onChange(keyedToPlainFormData(newKeyedFormData), newErrorSchema) ); }; }; /** Callback handler for when the user clicks on one of the move item buttons on an existing array element. Moves the * row of keyed form data at the `index` to the `newIndex` in the state, and then returning `onChange()` with the * plain form data converted from the keyed data * * @param index - The index of the item to move * @param newIndex - The index to where the item is to be moved */ onReorderClick = (index, newIndex) => { return (event) => { if (event) { event.preventDefault(); event.currentTarget.blur(); } const { onChange, errorSchema } = this.props; let newErrorSchema; if (errorSchema) { newErrorSchema = {}; for (const idx in errorSchema) { const i = parseInt(idx); if (i == index) { set(newErrorSchema, [newIndex], errorSchema[index]); } else if (i == newIndex) { set(newErrorSchema, [index], errorSchema[newIndex]); } else { set(newErrorSchema, [idx], errorSchema[i]); } } } const { keyedFormData } = this.state; function reOrderArray() { const _newKeyedFormData = keyedFormData.slice(); _newKeyedFormData.splice(index, 1); _newKeyedFormData.splice(newIndex, 0, keyedFormData[index]); return _newKeyedFormData; } const newKeyedFormData = reOrderArray(); this.setState( { keyedFormData: newKeyedFormData }, () => onChange(keyedToPlainFormData(newKeyedFormData), newErrorSchema) ); }; }; /** Callback handler used to deal with changing the value of the data in the array at the `index`. Calls the * `onChange` callback with the updated form data * * @param index - The index of the item being changed */ onChangeForIndex = (index) => { return (value, newErrorSchema, id) => { const { formData, onChange, errorSchema } = this.props; const arrayData = Array.isArray(formData) ? formData : []; const newFormData = arrayData.map((item, i) => { const jsonValue = typeof value === "undefined" ? null : value; return index === i ? jsonValue : item; }); onChange( newFormData, errorSchema && errorSchema && { ...errorSchema, [index]: newErrorSchema }, id ); }; }; /** Callback handler used to change the value for a checkbox */ onSelectChange = (value) => { const { onChange, idSchema } = this.props; onChange(value, void 0, idSchema && idSchema.$id); }; /** Renders the `ArrayField` depending on the specific needs of the schema and uischema elements */ render() { const { schema, uiSchema, idSchema, registry } = this.props; const { schemaUtils, translateString } = registry; if (!(ITEMS_KEY in schema)) { const uiOptions = getUiOptions(uiSchema); const UnsupportedFieldTemplate = getTemplate( "UnsupportedFieldTemplate", registry, uiOptions ); return /* @__PURE__ */ jsx( UnsupportedFieldTemplate, { schema, idSchema, reason: translateString(TranslatableString.MissingItems), registry } ); } if (schemaUtils.isMultiSelect(schema)) { return this.renderMultiSelect(); } if (isCustomWidget(uiSchema)) { return this.renderCustomWidget(); } if (isFixedItems(schema)) { return this.renderFixedArray(); } if (schemaUtils.isFilesArray(schema, uiSchema)) { return this.renderFiles(); } return this.renderNormalArray(); } /** Renders a normal array without any limitations of length */ renderNormalArray() { const { schema, uiSchema = {}, errorSchema, idSchema, name, title, disabled = false, readonly = false, autofocus = false, required = false, registry, onBlur, onFocus, idPrefix, idSeparator = "_", rawErrors } = this.props; const { keyedFormData } = this.state; const fieldTitle = schema.title || title || name; const { schemaUtils, formContext } = registry; const uiOptions = getUiOptions(uiSchema); const _schemaItems = isObject(schema.items) ? schema.items : {}; const itemsSchema = schemaUtils.retrieveSchema(_schemaItems); const formData = keyedToPlainFormData(this.state.keyedFormData); const canAdd = this.canAddItem(formData); const arrayProps = { canAdd, items: keyedFormData.map((keyedItem, index) => { const { key, item } = keyedItem; const itemCast = item; const itemSchema = schemaUtils.retrieveSchema(_schemaItems, itemCast); const itemErrorSchema = errorSchema ? errorSchema[index] : void 0; const itemIdPrefix = idSchema.$id + idSeparator + index; const itemIdSchema = schemaUtils.toIdSchema(itemSchema, itemIdPrefix, itemCast, idPrefix, idSeparator); return this.renderArrayFieldItem({ key, index, name: name && `${name}-${index}`, title: fieldTitle ? `${fieldTitle}-${index + 1}` : void 0, canAdd, canMoveUp: index > 0, canMoveDown: index < formData.length - 1, itemSchema, itemIdSchema, itemErrorSchema, itemData: itemCast, itemUiSchema: uiSchema.items, autofocus: autofocus && index === 0, onBlur, onFocus, rawErrors, totalItems: keyedFormData.length }); }), className: `field field-array field-array-of-${itemsSchema.type}`, disabled, idSchema, uiSchema, onAddClick: this.onAddClick, readonly, required, schema, title: fieldTitle, formContext, formData, rawErrors, registry }; const Template = getTemplate("ArrayFieldTemplate", registry, uiOptions); return /* @__PURE__ */ jsx(Template, { ...arrayProps }); } /** Renders an array using the custom widget provided by the user in the `uiSchema` */ renderCustomWidget() { const { schema, idSchema, uiSchema, disabled = false, readonly = false, autofocus = false, required = false, hideError, placeholder, onBlur, onFocus, formData: items = [], registry, rawErrors, name } = this.props; const { widgets: widgets2, formContext, globalUiOptions, schemaUtils } = registry; const { widget, title: uiTitle, ...options } = getUiOptions(uiSchema, globalUiOptions); const Widget = getWidget(schema, widget, widgets2); const label = uiTitle ?? schema.title ?? name; const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions); return /* @__PURE__ */ jsx( Widget, { id: idSchema.$id, name, multiple: true, onChange: this.onSelectChange, onBlur, onFocus, options, schema, uiSchema, registry, value: items, disabled, readonly, hideError, required, label, hideLabel: !displayLabel, placeholder, formContext, autofocus, rawErrors } ); } /** Renders an array as a set of checkboxes */ renderMultiSelect() { const { schema, idSchema, uiSchema, formData: items = [], disabled = false, readonly = false, autofocus = false, required = false, placeholder, onBlur, onFocus, registry, rawErrors, name } = this.props; const { widgets: widgets2, schemaUtils, formContext, globalUiOptions } = registry; const itemsSchema = schemaUtils.retrieveSchema(schema.items, items); const enumOptions = optionsList(itemsSchema, uiSchema); const { widget = "select", title: uiTitle, ...options } = getUiOptions(uiSchema, globalUiOptions); const Widget = getWidget(schema, widget, widgets2); const label = uiTitle ?? schema.title ?? name; const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions); return /* @__PURE__ */ jsx( Widget, { id: idSchema.$id, name, multiple: true, onChange: this.onSelectChange, onBlur, onFocus, options: { ...options, enumOptions }, schema, uiSchema, registry, value: items, disabled, readonly, required, label, hideLabel: !displayLabel, placeholder, formContext, autofocus, rawErrors } ); } /** Renders an array of files using the `FileWidget` */ renderFiles() { const { schema, uiSchema, idSchema, name, disabled = false, readonly = false, autofocus = false, required = false, onBlur, onFocus, registry, formData: items = [], rawErrors } = this.props; const { widgets: widgets2, formContext, globalUiOptions, schemaUtils } = registry; const { widget = "files", title: uiTitle, ...options } = getUiOptions(uiSchema, globalUiOptions); const Widget = getWidget(schema, widget, widgets2); const label = uiTitle ?? schema.title ?? name; const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions); return /* @__PURE__ */ jsx( Widget, { options, id: idSchema.$id, name, multiple: true, onChange: this.onSelectChange, onBlur, onFocus, schema, uiSchema, value: items, disabled, readonly, required, registry, formContext, autofocus, rawErrors, label, hideLabel: !displayLabel } ); } /** Renders an array that has a maximum limit of items */ renderFixedArray() { const { schema, uiSchema = {}, formData = [], errorSchema, idPrefix, idSeparator = "_", idSchema, name, title, disabled = false, readonly = false, autofocus = false, required = false, registry, onBlur, onFocus, rawErrors } = this.props; const { keyedFormData } = this.state; let { formData: items = [] } = this.props; const fieldTitle = schema.title || title || name; const uiOptions = getUiOptions(uiSchema); const { schemaUtils, formContext } = registry; const _schemaItems = isObject(schema.items) ? schema.items : []; const itemSchemas = _schemaItems.map( (item, index) => schemaUtils.retrieveSchema(item, formData[index]) ); const additionalSchema = isObject(schema.additionalItems) ? schemaUtils.retrieveSchema(schema.additionalItems, formData) : null; if (!items || items.length < itemSchemas.length) { items = items || []; items = items.concat(new Array(itemSchemas.length - items.length)); } const canAdd = this.canAddItem(items) && !!additionalSchema; const arrayProps = { canAdd, className: "field field-array field-array-fixed-items", disabled, idSchema, formData, items: keyedFormData.map((keyedItem, index) => { const { key, item } = keyedItem; const itemCast = item; const additional = index >= itemSchemas.length; const itemSchema = (additional && isObject(schema.additionalItems) ? schemaUtils.retrieveSchema(schema.additionalItems, itemCast) : itemSchemas[index]) || {}; const itemIdPrefix = idSchema.$id + idSeparator + index; const itemIdSchema = schemaUtils.toIdSchema(itemSchema, itemIdPrefix, itemCast, idPrefix, idSeparator); const itemUiSchema = additional ? uiSchema.additionalItems || {} : Array.isArray(uiSchema.items) ? uiSchema.items[index] : uiSchema.items || {}; const itemErrorSchema = errorSchema ? errorSchema[index] : void 0; return this.renderArrayFieldItem({ key, index, name: name && `${name}-${index}`, title: fieldTitle ? `${fieldTitle}-${index + 1}` : void 0, canAdd, canRemove: additional, canMoveUp: index >= itemSchemas.length + 1, canMoveDown: additional && index < items.length - 1, itemSchema, itemData: itemCast, itemUiSchema, itemIdSchema, itemErrorSchema, autofocus: autofocus && index === 0, onBlur, onFocus, rawErrors, totalItems: keyedFormData.length }); }), onAddClick: this.onAddClick, readonly, required, registry, schema, uiSchema, title: fieldTitle, formContext, errorSchema, rawErrors }; const Template = getTemplate("ArrayFieldTemplate", registry, uiOptions); return /* @__PURE__ */ jsx(Template, { ...arrayProps }); } /** Renders the individual array item using a `SchemaField` along with the additional properties required to be send * back to the `ArrayFieldItemTemplate`. * * @param props - The props for the individual array item to be rendered */ renderArrayFieldItem(props) { const { key, index, name, canAdd, canRemove = true, canMoveUp, canMoveDown, itemSchema, itemData, itemUiSchema, itemIdSchema, itemErrorSchema, autofocus, onBlur, onFocus, rawErrors, totalItems, title } = props; const { disabled, hideError, idPrefix, idSeparator, readonly, uiSchema, registry, formContext } = this.props; const { fields: { ArraySchemaField, SchemaField: SchemaField2 }, globalUiOptions } = registry; const ItemSchemaField = ArraySchemaField || SchemaField2; const { orderable = true, removable = true, copyable = false } = getUiOptions(uiSchema, globalUiOptions); const has2 = { moveUp: orderable && canMoveUp, moveDown: orderable && canMoveDown, copy: copyable && canAdd, remove: removable && canRemove, toolbar: false }; has2.toolbar = Object.keys(has2).some((key2) => has2[key2]); return { children: /* @__PURE__ */ jsx( ItemSchemaField, { name, title, index, schema: itemSchema, uiSchema: itemUiSchema, formData: itemData, formContext, errorSchema: itemErrorSchema, idPrefix, idSeparator, idSchema: itemIdSchema, required: this.isItemRequired(itemSchema), onChange: this.onChangeForIndex(index), onBlur, onFocus, registry, disabled, readonly, hideError, autofocus, rawErrors } ), className: "array-item", disabled, canAdd, hasCopy: has2.copy, hasToolbar: has2.toolbar, hasMoveUp: has2.moveUp, hasMoveDown: has2.moveDown, hasRemove: has2.remove, index, totalItems, key, onAddIndexClick: this.onAddIndexClick, onCopyIndexClick: this.onCopyIndexClick, onDropIndexClick: this.onDropIndexClick, onReorderClick: this.onReorderClick, readonly, registry, schema: itemSchema, uiSchema: itemUiSchema }; } }; var ArrayField_default = ArrayField; // src/components/fields/BooleanField.tsx import { getWidget as getWidget2, getUiOptions as getUiOptions2, optionsList as optionsList2, TranslatableString as TranslatableString2 } from "@rjsf/utils"; import isObject2 from "lodash/isObject"; import { jsx as jsx2 } from "react/jsx-runtime"; function BooleanField(props) { const { schema, name, uiSchema, idSchema, formData, registry, required, disabled, readonly, hideError, autofocus, title, onChange, onFocus, onBlur, rawErrors } = props; const { title: schemaTitle } = schema; const { widgets: widgets2, formContext, translateString, globalUiOptions } = registry; const { widget = "checkbox", title: uiTitle, // Unlike the other fields, don't use `getDisplayLabel()` since it always returns false for the boolean type label: displayLabel = true, ...options } = getUiOptions2(uiSchema, globalUiOptions); const Widget = getWidget2(schema, widget, widgets2); const yes = translateString(TranslatableString2.YesLabel); const no = translateString(TranslatableString2.NoLabel); let enumOptions; const label = uiTitle ?? schemaTitle ?? title ?? name; if (Array.isArray(schema.oneOf)) { enumOptions = optionsList2( { oneOf: schema.oneOf.map((option) => { if (isObject2(option)) { return { ...option, title: option.title || (option.const === true ? yes : no) }; } return void 0; }).filter((o) => o) // cast away the error that typescript can't grok is fixed }, uiSchema ); } else { const schemaWithEnumNames = schema; const enums = schema.enum ?? [true, false]; if (!schemaWithEnumNames.enumNames && enums.length === 2 && enums.every((v) => typeof v === "boolean")) { enumOptions = [ { value: enums[0], label: enums[0] ? yes : no }, { value: enums[1], label: enums[1] ? yes : no } ]; } else { enumOptions = optionsList2( { enum: enums, // NOTE: enumNames is deprecated, but still supported for now. enumNames: schemaWithEnumNames.enumNames }, uiSchema ); } } return /* @__PURE__ */ jsx2( Widget, { options: { ...options, enumOptions }, schema, uiSchema, id: idSchema.$id, name, onChange, onFocus, onBlur, label, hideLabel: !displayLabel, value: formData, required, disabled, readonly, hideError, registry, formContext, autofocus, rawErrors } ); } var BooleanField_default = BooleanField; // src/components/fields/MultiSchemaField.tsx import { Component as Component2 } from "react"; import get2 from "lodash/get"; import isEmpty from "lodash/isEmpty"; import omit from "lodash/omit"; import { ANY_OF_KEY, deepEquals, ERRORS_KEY, getDiscriminatorFieldFromSchema, getUiOptions as getUiOptions3, getWidget as getWidget3, mergeSchemas, ONE_OF_KEY, TranslatableString as TranslatableString3 } from "@rjsf/utils"; import { jsx as jsx3, jsxs } from "react/jsx-runtime"; var AnyOfField = class extends Component2 { /** Constructs an `AnyOfField` with the given `props` to initialize the initially selected option in state * * @param props - The `FieldProps` for this template */ constructor(props) { super(props); const { formData, options, registry: { schemaUtils } } = this.props; const retrievedOptions = options.map((opt) => schemaUtils.retrieveSchema(opt, formData)); this.state = { retrievedOptions, selectedOption: this.getMatchingOption(0, formData, retrievedOptions) }; } /** React lifecycle method that is called when the props and/or state for this component is updated. It recomputes the * currently selected option based on the overall `formData` * * @param prevProps - The previous `FieldProps` for this template * @param prevState - The previous `AnyOfFieldState` for this template */ componentDidUpdate(prevProps, prevState) { const { formData, options, idSchema } = this.props; const { selectedOption } = this.state; let newState = this.state; if (!deepEquals(prevProps.options, options)) { const { registry: { schemaUtils } } = this.props; const retrievedOptions = options.map((opt) => schemaUtils.retrieveSchema(opt, formData)); newState = { selectedOption, retrievedOptions }; } if (!deepEquals(formData, prevProps.formData) && idSchema.$id === prevProps.idSchema.$id) { const { retrievedOptions } = newState; const matchingOption = this.getMatchingOption(selectedOption, formData, retrievedOptions); if (prevState && matchingOption !== selectedOption) { newState = { selectedOption: matchingOption, retrievedOptions }; } } if (newState !== this.state) { this.setState(newState); } } /** Determines the best matching option for the given `formData` and `options`. * * @param formData - The new formData * @param options - The list of options to choose from * @return - The index of the `option` that best matches the `formData` */ getMatchingOption(selectedOption, formData, options) { const { schema, registry: { schemaUtils } } = this.props; const discriminator = getDiscriminatorFieldFromSchema(schema); const option = schemaUtils.getClosestMatchingOption(formData, options, selectedOption, discriminator); return option; } /** Callback handler to remember what the currently selected option is. In addition to that the `formData` is updated * to remove properties that are not part of the newly selected option schema, and then the updated data is passed to * the `onChange` handler. * * @param option - The new option value being selected */ onOptionChange = (option) => { const { selectedOption, retrievedOptions } = this.state; const { formData, onChange, registry } = this.props; const { schemaUtils } = registry; const intOption = option !== void 0 ? parseInt(option, 10) : -1; if (intOption === selectedOption) { return; } const newOption = intOption >= 0 ? retrievedOptions[intOption] : void 0; const oldOption = selectedOption >= 0 ? retrievedOptions[selectedOption] : void 0; let newFormData = schemaUtils.sanitizeDataForNewSchema(newOption, oldOption, formData); if (newOption) { newFormData = schemaUtils.getDefaultFormState(newOption, newFormData, "excludeObjectChildren"); } this.setState({ selectedOption: intOption }, () => { onChange(newFormData, void 0, this.getFieldId()); }); }; getFieldId() { const { idSchema, schema } = this.props; return `${idSchema.$id}${schema.oneOf ? "__oneof_select" : "__anyof_select"}`; } /** Renders the `AnyOfField` selector along with a `SchemaField` for the value of the `formData` */ render() { const { name, disabled = false, errorSchema = {}, formContext, onBlur, onFocus, readonly, registry, schema, uiSchema } = this.props; const { widgets: widgets2, fields: fields2, translateString, globalUiOptions, schemaUtils } = registry; const { SchemaField: _SchemaField } = fields2; const { selectedOption, retrievedOptions } = this.state; const { widget = "select", placeholder, autofocus, autocomplete, title = schema.title, ...uiOptions } = getUiOptions3(uiSchema, globalUiOptions); const Widget = getWidget3({ type: "number" }, widget, widgets2); const rawErrors = get2(errorSchema, ERRORS_KEY, []); const fieldErrorSchema = omit(errorSchema, [ERRORS_KEY]); const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions); const option = selectedOption >= 0 ? retrievedOptions[selectedOption] || null : null; let optionSchema; if (option) { const { required } = schema; optionSchema = required ? mergeSchemas({ required }, option) : option; } let optionsUiSchema = []; if (ONE_OF_KEY in schema && uiSchema && ONE_OF_KEY in uiSchema) { if (Array.isArray(uiSchema[ONE_OF_KEY])) { optionsUiSchema = uiSchema[ONE_OF_KEY]; } else { console.warn(`uiSchema.oneOf is not an array for "${title || name}"`); } } else if (ANY_OF_KEY in schema && uiSchema && ANY_OF_KEY in uiSchema) { if (Array.isArray(uiSchema[ANY_OF_KEY])) { optionsUiSchema = uiSchema[ANY_OF_KEY]; } else { console.warn(`uiSchema.anyOf is not an array for "${title || name}"`); } } let optionUiSchema = uiSchema; if (selectedOption >= 0 && optionsUiSchema.length > selectedOption) { optionUiSchema = optionsUiSchema[selectedOption]; } const translateEnum = title ? TranslatableString3.TitleOptionPrefix : TranslatableString3.OptionPrefix; const translateParams = title ? [title] : []; const enumOptions = retrievedOptions.map((opt, index) => { const { title: uiTitle = opt.title } = getUiOptions3(optionsUiSchema[index]); return { label: uiTitle || translateString(translateEnum, translateParams.concat(String(index + 1))), value: index }; }); return /* @__PURE__ */ jsxs("div", { className: "panel panel-default panel-body", children: [ /* @__PURE__ */ jsx3("div", { className: "form-group", children: /* @__PURE__ */ jsx3( Widget, { id: this.getFieldId(), name: `${name}${schema.oneOf ? "__oneof_select" : "__anyof_select"}`, schema: { type: "number", default: 0 }, onChange: this.onOptionChange, onBlur, onFocus, disabled: disabled || isEmpty(enumOptions), multiple: false, rawErrors, errorSchema: fieldErrorSchema, value: selectedOption >= 0 ? selectedOption : void 0, options: { enumOptions, ...uiOptions }, registry, formContext, placeholder, autocomplete, autofocus, label: title ?? name, hideLabel: !displayLabel, readonly } ) }), optionSchema && optionSchema.type !== "null" && /* @__PURE__ */ jsx3(_SchemaField, { ...this.props, schema: optionSchema, uiSchema: optionUiSchema }) ] }); } }; var MultiSchemaField_default = AnyOfField; // src/components/fields/NumberField.tsx import { useState, useCallback } from "react"; import { asNumber } from "@rjsf/utils"; import { jsx as jsx4 } from "react/jsx-runtime"; var trailingCharMatcherWithPrefix = /\.([0-9]*0)*$/; var trailingCharMatcher = /[0.]0*$/; function NumberField(props) { const { registry, onChange, formData, value: initialValue } = props; const [lastValue, setLastValue] = useState(initialValue); const { StringField: StringField2 } = registry.fields; let value = formData; const handleChange = useCallback( (value2, errorSchema, id) => { setLastValue(value2); if (`${value2}`.charAt(0) === ".") { value2 = `0${value2}`; } const processed = typeof value2 === "string" && value2.match(trailingCharMatcherWithPrefix) ? asNumber(value2.replace(trailingCharMatcher, "")) : asNumber(value2); onChange(processed, errorSchema, id); }, [onChange] ); if (typeof lastValue === "string" && typeof value === "number") { const re = new RegExp(`^(${String(value).replace(".", "\\.")})?\\.?0*$`); if (lastValue.match(re)) { value = lastValue; } } return /* @__PURE__ */ jsx4(StringField2, { ...props, formData: value, onChange: handleChange }); } var NumberField_default = NumberField; // src/components/fields/ObjectField.tsx import { Component as Component3 } from "react"; import { getTemplate as getTemplate2, getUiOptions as getUiOptions4, orderProperties, TranslatableString as TranslatableString4, ADDITIONAL_PROPERTY_FLAG, PROPERTIES_KEY, REF_KEY, ANY_OF_KEY as ANY_OF_KEY2, ONE_OF_KEY as ONE_OF_KEY2 } from "@rjsf/utils"; import Markdown from "markdown-to-jsx"; import get3 from "lodash/get"; import has from "lodash/has"; import isObject3 from "lodash/isObject"; import set2 from "lodash/set"; import unset from "lodash/unset"; import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime"; var ObjectField = class extends Component3 { /** Set up the initial state */ state = { wasPropertyKeyModified: false, additionalProperties: {} }; /** Returns a flag indicating whether the `name` field is required in the object schema * * @param name - The name of the field to check for required-ness * @returns - True if the field `name` is required, false otherwise */ isRequired(name) { const { schema } = this.props; return Array.isArray(schema.required) && schema.required.indexOf(name) !== -1; } /** Returns the `onPropertyChange` handler for the `name` field. Handles the special case where a user is attempting * to clear the data for a field added as an additional property. Calls the `onChange()` handler with the updated * formData. * * @param name - The name of the property * @param addedByAdditionalProperties - Flag indicating whether this property is an additional property * @returns - The onPropertyChange callback for the `name` property */ onPropertyChange = (name, addedByAdditionalProperties = false) => { return (value, newErrorSchema, id) => { const { formData, onChange, errorSchema } = this.props; if (value === void 0 && addedByAdditionalProperties) { value = ""; } const newFormData = { ...formData, [name]: value }; onChange( newFormData, errorSchema && errorSchema && { ...errorSchema, [name]: newErrorSchema }, id ); }; }; /** Returns a callback to handle the onDropPropertyClick event for the given `key` which removes the old `key` data * and calls the `onChange` callback with it * * @param key - The key for which the drop callback is desired * @returns - The drop property click callback */ onDropPropertyClick = (key) => { return (event) => { event.preventDefault(); const { onChange, formData } = this.props; const copiedFormData = { ...formData }; unset(copiedFormData, key); onChange(copiedFormData); }; }; /** Computes the next available key name from the `preferredKey`, indexing through the already existing keys until one * that is already not assigned is found. * * @param preferredKey - The preferred name of a new key * @param [formData] - The form data in which to check if the desired key already exists * @returns - The name of the next available key from `preferredKey` */ getAvailableKey = (preferredKey, formData) => { const { uiSchema, registry } = this.props; const { duplicateKeySuffixSeparator = "-" } = getUiOptions4(uiSchema, registry.globalUiOptions); let index = 0; let newKey = preferredKey; while (has(formData, newKey)) { newKey = `${preferredKey}${duplicateKeySuffixSeparator}${++index}`; } return newKey; }; /** Returns a callback function that deals with the rename of a key for an additional property for a schema. That * callback will attempt to rename the key and move the existing data to that key, calling `onChange` when it does. * * @param oldValue - The old value of a field * @returns - The key change callback function */ onKeyChange = (oldValue) => { return (value, newErrorSchema) => { if (oldValue === value) { return; } const { formData, onChange, errorSchema } = this.props; value = this.getAvailableKey(value, formData); const newFormData = { ...formData }; const newKeys = { [oldValue]: value }; const keyValues = Object.keys(newFormData).map((key) => { const newKey = newKeys[key] || key; return { [newKey]: newFormData[key] }; }); const renamedObj = Object.assign({}, ...keyValues); this.setState({ wasPropertyKeyModified: true }); onChange( renamedObj, errorSchema && errorSchema && { ...errorSchema, [value]: newErrorSchema } ); }; }; /** Returns a default value to be used for a new additional schema property of the given `type` * * @param type - The type of the new additional schema property */ getDefaultValue(type) { const { registry: { translateString } } = this.props; switch (type) { case "array": return []; case "boolean": return false; case "null": return null; case "number": return 0; case "object": return {}; case "string": default: return translateString(TranslatableString4.NewStringDefault); } } /** Handles the adding of a new additional property on the given `schema`. Calls the `onChange` callback once the new * default data for that field has been added to the formData. * * @param schema - The schema element to which the new property is being added */ handleAddClick = (schema) => () => { if (!schema.additionalProperties) { return; } const { formData, onChange, registry } = this.props; const newFormData = { ...formData }; let type = void 0; let constValue = void 0; let defaultValue = void 0; if (isObject3(schema.additionalProperties)) { type = schema.additionalProperties.type; constValue = schema.additionalProperties.const; defaultValue = schema.additionalProperties.default; let apSchema = schema.additionalProperties; if (REF_KEY in apSchema) { const { schemaUtils } = registry; apSchema = schemaUtils.retrieveSchema({ $ref: apSchema[REF_KEY] }, formData); type = apSchema.type; constValue = apSchema.const; defaultValue = apSchema.default; } if (!type && (ANY_OF_KEY2 in apSchema || ONE_OF_KEY2 in apSchema)) { type = "object"; } } const newKey = this.getAvailableKey("newKey", newFormData); const newValue = constValue ?? defaultValue ?? this.getDefaultValue(type); set2(newFormData, newKey, newValue); onChange(newFormData); }; /** Renders the `ObjectField` from the given props */ render() { const { schema: rawSchema, uiSchema = {}, formData, errorSchema, idSchema, name, required = false, disabled, readonly, hideError, idPrefix, idSeparator, onBlur, onFocus, registry, title } = this.props; const { fields: fields2, formContext, schemaUtils, translateString, globalUiOptions } = registry; const { SchemaField: SchemaField2 } = fields2; const schema = schemaUtils.retrieveSchema(rawSchema, formData); const uiOptions = getUiOptions4(uiSchema, globalUiOptions); const { properties: schemaProperties = {} } = schema; const templateTitle = uiOptions.title ?? schema.title ?? title ?? name; const description = uiOptions.description ?? schema.description; let orderedProperties; try { const properties = Object.keys(schemaProperties); orderedProperties = orderProperties(properties, uiOptions.order); } catch (err) { return /* @__PURE__ */ jsxs2("div", { children: [ /* @__PURE__ */ jsx5("p", { className: "config-error", style: { color: "red" }, children: /* @__PURE__ */ jsx5(Markdown, { options: { disableParsingRawHTML: true }, children: translateString(TranslatableString4.InvalidObjectField, [name || "root", err.message]) }) }), /* @__PURE__ */ jsx5("pre", { children: JSON.stringify(schema) }) ] }); } const Template = getTemplate2("ObjectFieldTemplate", registry, uiOptions); const templateProps = { // getDisplayLabel() always returns false for object types, so just check the `uiOptions.label` title: uiOptions.label === false ? "" : templateTitle, description: uiOptions.label === false ? void 0 : description, properties: orderedProperties.map((name2) => { const addedByAdditionalProperties = has(schema, [PROPERTIES_KEY, name2, ADDITIONAL_PROPERTY_FLAG]); const fieldUiSchema = addedByAdditionalProperties ? uiSchema.additionalProperties : uiSchema[name2]; const hidden = getUiOptions4(fieldUiSchema).widget === "hidden"; const fieldIdSchema = get3(idSchema, [name2], {}); return { content: /* @__PURE__ */ jsx5( SchemaField2, { name: name2, required: this.isRequired(name2), schema: get3(schema, [PROPERTIES_KEY, name2], {}), uiSchema: fieldUiSchema, errorSchema: get3(errorSchema, name2), idSchema: fieldIdSchema, idPrefix, idSeparator, formData: get3(formData, name2), formContext, wasPropertyKeyModified: this.state.wasPropertyKeyModified, onKeyChange: this.onKeyChange(name2), onChange: this.onPropertyChange(name2, addedByAdditionalProperties), onBlur, onFocus, registry, disabled, readonly, hideError, onDropPropertyClick: this.onDropPropertyClick }, name2 ), name: name2, readonly, disabled, required, hidden }; }), readonly, disabled, required, idSchema, uiSchema, errorSchema, schema, formData, formContext, registry }; return /* @__PURE__ */ jsx5(Template, { ...templateProps, onAddClick: this.handleAddClick }); } }; var ObjectField_default = ObjectField; // src/components/fields/SchemaField.tsx import { useCallback as useCallback2, Component as Component4 } from "react"; import { ADDITIONAL_PROPERTY_FLAG as ADDITIONAL_PROPERTY_FLAG2, deepEquals as deepEquals2, descriptionId, getSchemaType, getTemplate as getTemplate3, getUiOptions as getUiOptions5, ID_KEY, mergeObjects, TranslatableString as TranslatableString5, UI_OPTIONS_KEY } from "@rjsf/utils"; import isObject4 from "lodash/isObject"; import omit2 from "lodash/omit"; import Markdown2 from "markdown-to-jsx"; import { Fragment, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime"; var COMPONENT_TYPES = { array: "ArrayField", boolean: "BooleanField", integer: "NumberField", number: "NumberField", object: "ObjectField", string: "StringField", null: "NullField" }; function getFieldComponent(schema, uiOptions, idSchema, registry) { const field = uiOptions.field; const { fields: fields2, translateString } = registry; if (typeof field === "function") { return field; } if (typeof field === "string" && field in fields2) { return fields2[field]; } const schemaType = getSchemaType(schema); const type = Array.isArray(schemaType) ? schemaType[0] : schemaType || ""; const schemaId = schema.$id; let componentName = COMPONENT_TYPES[type]; if (schemaId && schemaId in fields2) { componentName = schemaId; } if (!componentName && (schema.anyOf || schema.oneOf)) { return () => null; } return componentName in fields2 ? fields2[componentName] : () => { const UnsupportedFieldTemplate = getTemplate3( "UnsupportedFieldTemplate", registry, uiOptions ); return /* @__PURE__ */ jsx6( UnsupportedFieldTemplate, { schema, idSchema, reason: translateS