Skip to content

Commit

Permalink
Update utils based on core refactor and update package.jsons (rjsf-te…
Browse files Browse the repository at this point in the history
…am#2903)

* Update utils based on core refactor and update package.jsons
- Updated the main `package.json` to bump typescript
- Updated the utils `package.json` to bump everything but react
- Updated `SchemaUtilsType` to add the `getValidator()` and `doesSchemaUtilsDiffer()` functions
  - Updated `createSchemaUtils()` to implement the new functions and the tests to validate them
- Also updated other types to deal with issues found during core refactor
  - Changed `FormValidation` to `ErrorSchema` as needed
  - Updated the `Registry` type to remove `definitions` as it is never used and added the `xxxTemplate` props
  - Updated many interfaces to make previously required props to be optional
  - Switched to using the `React.ComponentType` which incorporates the `FunctionComponent` and `ClassComponent` both
- Fixed a bug in `getDefaultFormState()` by making the `array` defaults use effectively the same logic as it did in `core` but refactored to a function for type safety
  - Updated the tests to add one that verifies the bug is fixed

* - rollback typescript to previous version due to `fluent-ui` issue

* - Fixed bug in `getSchemaType()` related to incorrectly defaulting to `string` when no type exists

* - Made `uiSchema` optional in `canExpand()`, `getSubmitButtonOptions()` and `getUiOptions()`
- Updated the required-ness of a smattering of props in interfaces

* - Made all callbacks be required again

* - Made label required again

* - More updates to make `uiSchema` optional

* - Added new `processSelectValue()` utility, refactored from core's `SelectWidget`, with full tests
- Also updated the `UIOptionsType` to add optional `title` and `description` props, typed to `string` to eliminate the need to type cast them

* - Added documentation for the `[key: string]` prop

* - Responded to reviewer feedback
  • Loading branch information
heath-freenome committed Aug 27, 2022
1 parent 188e038 commit 704799f
Show file tree
Hide file tree
Showing 25 changed files with 16,545 additions and 14,571 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-jsonschema-form",
"version": "2.0.0-alpha.1",
"version": "4.2.0",
"private": true,
"description": "monorepo for react-jsonschema-form and its themes",
"scripts": {
Expand Down
30,625 changes: 16,178 additions & 14,447 deletions packages/utils/package-lock.json

Large diffs are not rendered by default.

35 changes: 17 additions & 18 deletions packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,40 +21,39 @@
"react": ">=16 || >=17"
},
"dependencies": {
"json-schema-merge-allof": "^0.6.0",
"json-schema-merge-allof": "^0.8.1",
"jsonpointer": "^5.0.0",
"lodash": "^4.17.15",
"lodash-es": "^4.17.15",
"react-is": "^16.9.0"
"react-is": "^18.2.0"
},
"devDependencies": {
"@babel/core": "^7.16.0",
"@babel/plugin-proposal-class-properties": "^7.16.0",
"@babel/plugin-transform-modules-commonjs": "^7.16.0",
"@babel/plugin-transform-react-jsx": "^7.16.0",
"@babel/preset-env": "^7.16.4",
"@babel/preset-react": "^7.16.0",
"@babel/core": "^7.18.6",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-transform-modules-commonjs": "^7.18.6",
"@babel/plugin-transform-react-jsx": "^7.18.6",
"@babel/preset-env": "^7.18.6",
"@babel/preset-react": "^7.18.6",
"@types/jest": "^25.2.3",
"@types/jest-expect-message": "^1.0.3",
"@types/jest-expect-message": "^1.0.4",
"@types/json-schema": "^7.0.9",
"@types/json-schema-merge-allof": "^0.6.1",
"@types/lodash": "^4.14.182",
"@types/react": "^16.14.25",
"@types/react-is": "^16.7.2",
"@types/react-is": "^17.0.3",
"@types/react-test-renderer": "^16.9.5",
"@types/sinon": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^5.25.0",
"@typescript-eslint/parser": "^5.25.0",
"babel-jest": "^28.1.0",
"babel-preset-jest": "^28.0.2",
"eslint": "^8.15.0",
"@typescript-eslint/eslint-plugin": "^5.30.3",
"@typescript-eslint/parser": "^5.30.3",
"babel-jest": "^28.1.2",
"babel-preset-jest": "^28.1.1",
"eslint": "^8.18.0",
"eslint-plugin-import": "^2.26.0",
"jest": "^28.1.0",
"jest": "^28.1.2",
"jest-expect-message": "^1.0.2",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-test-renderer": "^16.14.0",
"rimraf": "^2.6.3",
"rimraf": "^3.0.2",
"tsdx": "^0.14.1"
},
"publishConfig": {
Expand Down
8 changes: 4 additions & 4 deletions packages/utils/src/canExpand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import getUiOptions from './getUiOptions';
* the `formData` object doesn't already have `schema.maxProperties` elements.
*
* @param schema - The schema for the field that is being checked
* @param uiSchema - The uiSchema for the field
* @param formData - The formData for the field
* @param [uiSchema={}] - The uiSchema for the field
* @param [formData] - The formData for the field
* @returns - True if the schema element has additionalProperties, is expandable, and not at the maxProperties limit
*/
export default function canExpand<T = any, F = any>(schema: RJSFSchema, uiSchema: UiSchema<T, F>, formData: T) {
export default function canExpand<T = any, F = any>(schema: RJSFSchema, uiSchema: UiSchema<T, F> = {}, formData?: T) {
if (!schema.additionalProperties) {
return false;
}
Expand All @@ -20,7 +20,7 @@ export default function canExpand<T = any, F = any>(schema: RJSFSchema, uiSchema
}
// if ui:options.expandable was not explicitly set to false, we can add
// another property if we have not exceeded maxProperties yet
if (schema.maxProperties !== undefined) {
if (schema.maxProperties !== undefined && formData) {
return Object.keys(formData).length < schema.maxProperties;
}
return true;
Expand Down
32 changes: 28 additions & 4 deletions packages/utils/src/createSchemaUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import deepEquals from './deepEquals';
import { IdSchema, PathSchema, RJSFSchema, SchemaUtilsType, UiSchema, ValidatorType } from './types';
import {
getDefaultFormState,
Expand Down Expand Up @@ -30,6 +31,29 @@ class SchemaUtils<T = any> implements SchemaUtilsType<T> {
this.validator = validator;
}

/** Returns the `ValidatorType` in the `SchemaUtilsType`
*
* @returns - The `ValidatorType`
*/
getValidator() {
return this.validator;
}

/** Determines whether either the `validator` and `rootSchema` differ from the ones associated with this instance of
* the `SchemaUtilsType`. If either `validator` or `rootSchema` are falsy, then return false to prevent the creation
* of a new `SchemaUtilsType` with incomplete properties.
*
* @param validator - An implementation of the `ValidatorType` interface that will be compared against the current one
* @param rootSchema - The root schema that will be compared against the current one
* @returns - True if the `SchemaUtilsType` differs from the given `validator` or `rootSchema`
*/
doesSchemaUtilsDiffer(validator: ValidatorType, rootSchema: RJSFSchema): boolean {
if (!validator || !rootSchema) {
return false;
}
return this.validator !== validator || !deepEquals(this.rootSchema, rootSchema);
}

/** Returns the superset of `formData` that includes the given set updated to include any missing fields that have
* computed to have defaults provided in the `schema`.
*
Expand All @@ -46,10 +70,10 @@ class SchemaUtils<T = any> implements SchemaUtilsType<T> {
* should be displayed in a UI.
*
* @param schema - The schema for which the display label flag is desired
* @param uiSchema - The UI schema from which to derive potentially displayable information
* @param [uiSchema] - The UI schema from which to derive potentially displayable information
* @returns - True if the label should be displayed or false if it should not
*/
getDisplayLabel<F = any>(schema: RJSFSchema, uiSchema: UiSchema<T, F>) {
getDisplayLabel<F = any>(schema: RJSFSchema, uiSchema?: UiSchema<T, F>) {
return getDisplayLabel<T, F>(this.validator, schema, uiSchema, this.rootSchema);
}

Expand All @@ -66,10 +90,10 @@ class SchemaUtils<T = any> implements SchemaUtilsType<T> {
/** Checks to see if the `schema` and `uiSchema` combination represents an array of files
*
* @param schema - The schema for which check for array of files flag is desired
* @param uiSchema - The UI schema from which to check the widget
* @param [uiSchema] - The UI schema from which to check the widget
* @returns - True if schema/uiSchema contains an array of files, otherwise false
*/
isFilesArray<F = any>(schema: RJSFSchema, uiSchema: UiSchema<T, F>) {
isFilesArray<F = any>(schema: RJSFSchema, uiSchema?: UiSchema<T, F>) {
return isFilesArray<T, F>(this.validator, schema, uiSchema, this.rootSchema);
}

Expand Down
6 changes: 3 additions & 3 deletions packages/utils/src/getSchemaType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import { RJSFSchema } from './types';
* - type is an array with a length of 2 and one type is 'null': Returns the other type
*
* @param schema - The schema for which to get the type
* @returns - The type of the schema, defaulting to `string` if not available
* @returns - The type of the schema
*/
export default function getSchemaType(schema: RJSFSchema): string | string[] {
export default function getSchemaType(schema: RJSFSchema): string | string[] | undefined {
let { type } = schema;

if (!type && schema.const) {
Expand All @@ -31,5 +31,5 @@ export default function getSchemaType(schema: RJSFSchema): string | string[] {
type = type.find(type => type !== 'null');
}

return type || 'string';
return type;
}
4 changes: 2 additions & 2 deletions packages/utils/src/getSubmitButtonOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ export const DEFAULT_OPTIONS = {

/** Extracts any `ui:submitButtonOptions` from the `uiSchema` and merges them onto the `DEFAULT_OPTIONS`
*
* @param uiSchema - the UI Schema from which to extract submit button props
* @param [uiSchema={}] - the UI Schema from which to extract submit button props
* @returns - The merging of the `DEFAULT_OPTIONS` with any custom ones
*/
export default function getSubmitButtonOptions<T = any, F = any>(uiSchema: UiSchema<T, F>) {
export default function getSubmitButtonOptions<T = any, F = any>(uiSchema: UiSchema<T, F> = {}) {
const uiOptions = getUiOptions<T, F>(uiSchema);
if (uiOptions && uiOptions[SUBMIT_BTN_OPTIONS_KEY]) {
const options = uiOptions[SUBMIT_BTN_OPTIONS_KEY] as UISchemaSubmitButtonOptions;
Expand Down
4 changes: 2 additions & 2 deletions packages/utils/src/getUiOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { UIOptionsType, UiSchema } from './types';
/** Get all passed options from ui:options, and ui:<optionName>, returning them in an object with the `ui:`
* stripped off.
*
* @param uiSchema - The UI Schema from which to get any `ui:xxx` options
* @param [uiSchema={}] - The UI Schema from which to get any `ui:xxx` options
* @returns - An object containing all of the `ui:xxx` options with the stripped off
*/
export default function getUiOptions<T = any, F = any>(uiSchema: UiSchema<T, F>): UIOptionsType {
export default function getUiOptions<T = any, F = any>(uiSchema: UiSchema<T, F> = {}): UIOptionsType {
return Object.keys(uiSchema)
.filter(key => key.indexOf('ui:') === 0)
.reduce((options, key) => {
Expand Down
2 changes: 2 additions & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import optionsList from './optionsList';
import orderProperties from './orderProperties';
import pad from './pad';
import parseDateString from './parseDateString';
import processSelectValue from './processSelectValue';
import rangeSpec from './rangeSpec';
import schemaRequiresTrueValue from './schemaRequiresTrueValue';
import shouldRender from './shouldRender';
Expand Down Expand Up @@ -61,6 +62,7 @@ export {
orderProperties,
pad,
parseDateString,
processSelectValue,
rangeSpec,
schemaRequiresTrueValue,
shouldRender,
Expand Down
42 changes: 42 additions & 0 deletions packages/utils/src/processSelectValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import get from 'lodash/get';

import { RJSFSchema } from './types';
import asNumber from './asNumber';
import guessType from './guessType';

const nums = new Set<any>(['number', 'integer']);

/** Returns the real value for a select widget due to a silly limitation in the DOM which causes option change event
* values to always be retrieved as strings.
*
* @param schema - The schema to used to determine the value's true type
* @param [value] - The value to convert
*/
export default function processSelectValue(schema: RJSFSchema, value?: any) {
const { enum: schemaEnum, type, items } = schema;
if (value === '') {
return undefined;
}
if (type === 'array' && items && nums.has(get(items, 'type'))) {
return value.map(asNumber);
}
if (type === 'boolean') {
return value === 'true';
}
if (nums.has(type)) {
return asNumber(value);
}

// If type is undefined, but an enum is present, try and infer the type from
// the enum values
if (Array.isArray(schemaEnum)) {
if (schemaEnum.every((x: any) => nums.has(guessType(x)))) {
return asNumber(value);
}
if (schemaEnum.every((x: any) => guessType(x) === 'boolean')) {
return value === 'true';
}
}

return value;
}
51 changes: 36 additions & 15 deletions packages/utils/src/schema/getDefaultFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,43 @@ import { GenericObjectType, RJSFSchema, ValidatorType } from '../types';
import isMultiSelect from './isMultiSelect';
import retrieveSchema, { resolveDependencies } from './retrieveSchema';

/** Given a `schema` will return an inner schema that represents either an element in a `schema.items` array (when
* provided a valid `idx`), `schema.items` if it is not an array, `schema.additionalItems` when it is an object or
* an empty schema if no previous condition passes.
/** Enum that indicates how `schema.additionalItems` should be handled by the `getInnerSchemaForArrayItem()` function.
*/
export enum AdditionalItemsHandling {
Ignore,
Invert,
Fallback,
}

/** Given a `schema` will return an inner schema that for an array item. This is computed differently based on the
* `additionalItems` enum and the value of `idx`. There are four possible returns:
* 1. If `idx` is >= 0, then if `schema.items` is an array the `idx`th element of the array is returned if it is a valid
* index and not a boolean, otherwise it falls through to 3.
* 2. If `schema.items` is not an array AND truthy and not a boolean, then `schema.items` is returned since it actually
* is a schema, otherwise it falls through to 3.
* 3. If `additionalItems` is not `AdditionalItemsHandling.Ignore` and `schema.additionalItems` is an object, then
* `schema.additionalItems` is returned since it actually is a schema, otherwise it falls through to 4.
* 4. {} is returned representing an empty schema
*
* @param schema - The schema from which to get the particular item
* @param [additionalItems=AdditionalItemsHandling.Ignore] - How do we want to handle additional items?
* @param [idx=-1] - Index, if non-negative, will be used to return the idx-th element in a `schema.items` array
* @returns - The best fit schema object from the `schema`
* @returns - The best fit schema object from the `schema` given the `additionalItems` and `idx` modifiers
*/
export function getSchemaItem(schema: RJSFSchema, idx = -1) {
if (Array.isArray(schema.items) && idx >= 0 && idx < schema.items.length) {
return schema.items[idx] as RJSFSchema;
}
if (schema.items && !Array.isArray(schema.items)) {
return schema.items as RJSFSchema;
export function getInnerSchemaForArrayItem(
schema: RJSFSchema, additionalItems: AdditionalItemsHandling = AdditionalItemsHandling.Ignore, idx = -1
): RJSFSchema {
if (idx >= 0) {
if (Array.isArray(schema.items) && idx < schema.items.length) {
const item = schema.items[idx];
if (typeof item !== 'boolean') {
return item;
}
}
} else if (schema.items && !Array.isArray(schema.items) && typeof schema.items !== 'boolean') {
return schema.items;
}
if (isObject(schema.additionalItems)) {
if (additionalItems !== AdditionalItemsHandling.Ignore && isObject(schema.additionalItems)) {
return schema.additionalItems as RJSFSchema;
}
return {};
Expand Down Expand Up @@ -140,7 +161,7 @@ export function computeDefaults<T = any>(
// Inject defaults into existing array defaults
if (Array.isArray(defaults)) {
defaults = defaults.map((item, idx) => {
const schemaItem = getSchemaItem(schema, idx);
const schemaItem: RJSFSchema = getInnerSchemaForArrayItem(schema, AdditionalItemsHandling.Fallback, idx);
return computeDefaults<T>(
validator,
schemaItem,
Expand All @@ -152,10 +173,11 @@ export function computeDefaults<T = any>(

// Deeply inject defaults into already existing form data
if (Array.isArray(rawFormData)) {
const schemaItem: RJSFSchema = getInnerSchemaForArrayItem(schema);
defaults = rawFormData.map((item: T, idx: number) => {
return computeDefaults<T>(
validator,
getSchemaItem(schema, idx),
schemaItem,
get(defaults, [idx]),
rootSchema,
item
Expand All @@ -168,14 +190,13 @@ export function computeDefaults<T = any>(
if (schema.minItems > defaultsLength) {
const defaultEntries: T[] = (defaults || []) as T[];
// populate the array with the defaults
const fillerSchema: RJSFSchema = getSchemaItem(schema);
const fillerSchema: RJSFSchema = getInnerSchemaForArrayItem(schema, AdditionalItemsHandling.Invert);
const fillerDefault = fillerSchema.default;
const fillerEntries: T[] = fill(
new Array(schema.minItems - defaultsLength),
computeDefaults<any>(validator, fillerSchema, fillerDefault, rootSchema)
) as T[];
// then fill up the rest with either the item default or empty, up to minItems

return defaultEntries.concat(fillerEntries);
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/utils/src/schema/getDisplayLabel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import isMultiSelect from './isMultiSelect';
*
* @param validator - An implementation of the `ValidatorType` interface that will be used when necessary
* @param schema - The schema for which the display label flag is desired
* @param uiSchema - The UI schema from which to derive potentially displayable information
* @param [uiSchema={}] - The UI schema from which to derive potentially displayable information
* @param [rootSchema] - The root schema, used to primarily to look up `$ref`s
* @returns - True if the label should be displayed or false if it should not
*/
export default function getDisplayLabel<T = any, F = any>(
validator: ValidatorType, schema: RJSFSchema, uiSchema: UiSchema<T, F>, rootSchema?: RJSFSchema
validator: ValidatorType, schema: RJSFSchema, uiSchema: UiSchema<T, F> = {}, rootSchema?: RJSFSchema
): boolean {
const uiOptions = getUiOptions<T, F>(uiSchema);
const { label = true } = uiOptions;
Expand Down
4 changes: 2 additions & 2 deletions packages/utils/src/schema/isFilesArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import retrieveSchema from './retrieveSchema';
*
* @param validator - An implementation of the `ValidatorType` interface that will be used when necessary
* @param schema - The schema for which check for array of files flag is desired
* @param uiSchema - The UI schema from which to check the widget
* @param [uiSchema={}] - The UI schema from which to check the widget
* @param [rootSchema] - The root schema, used to primarily to look up `$ref`s
* @returns - True if schema/uiSchema contains an array of files, otherwise false
*/
export default function isFilesArray<T = any, F = any>(
validator: ValidatorType, schema: RJSFSchema, uiSchema: UiSchema<T, F>, rootSchema?: RJSFSchema
validator: ValidatorType, schema: RJSFSchema, uiSchema: UiSchema<T, F> = {}, rootSchema?: RJSFSchema
) {
if (uiSchema[UI_WIDGET_KEY] === 'files') {
return true;
Expand Down

0 comments on commit 704799f

Please sign in to comment.