From a847a34cb0c22311b271a621fc9e952b6cdca6cb Mon Sep 17 00:00:00 2001 From: songwongtp <16089160+songwongtp@users.noreply.github.com> Date: Mon, 1 Apr 2024 13:24:12 +0700 Subject: [PATCH 1/7] feat: upgrade rjsf --- CHANGELOG.md | 1 + package.json | 11 +- pnpm-lock.yaml | 80 ++-- .../json-schema/form/fields/ArrayField.tsx | 374 ++++++++++++------ .../json-schema/form/fields/BooleanField.tsx | 54 ++- .../form/fields/MultiSchemaField.tsx | 352 ++++++++++------- .../json-schema/form/fields/NullField.tsx | 13 +- src/lib/components/json-schema/form/index.tsx | 104 +---- .../form/templates/ArrayFieldItemTemplate.tsx | 61 +-- .../form/templates/ArrayFieldTemplate.tsx | 48 ++- .../form/templates/BaseInputTemplate.tsx | 124 ++++-- .../templates/DescriptionFieldTemplate.tsx | 26 +- .../form/templates/FieldTemplate.tsx | 31 +- .../form/templates/ObjectFieldTemplate.tsx | 76 ++-- .../form/templates/TitleFieldTemplate.tsx | 16 +- .../templates/button-templates/AddButton.tsx | 21 +- .../button-templates/SubmitButton.tsx | 15 +- .../icon-buttons/ChakraIconButton.tsx | 23 +- .../icon-buttons/IconButton.tsx | 50 ++- .../json-schema/form/widgets/SelectWidget.tsx | 66 ++-- 20 files changed, 953 insertions(+), 593 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6a860e30..4b931256e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Improvements +- [#861](https://github.com/alleslabs/celatone-frontend/pull/861) Upgrade json schema package version - [#845](https://github.com/alleslabs/celatone-frontend/pull/845) Edit error fetching message on the contract state - [#844](https://github.com/alleslabs/celatone-frontend/pull/844) Modify wording status for rejected proposals - [#841](https://github.com/alleslabs/celatone-frontend/pull/841) Bump cosmos-kit package fixing the installed wallet issue diff --git a/package.json b/package.json index 20d3b51ab..a7de0f651 100644 --- a/package.json +++ b/package.json @@ -37,9 +37,6 @@ "storybook": "storybook dev -p 6006", "build-storybook": "storybook build" }, - "resolutions": { - "chakra-react-select": "^4.7.0" - }, "dependencies": { "@amplitude/analytics-browser": "^2.3.3", "@amplitude/analytics-types": "^2.3.0", @@ -72,10 +69,10 @@ "@initia/initia.proto": "0.1.12", "@interchain-ui/react": "1.21.19", "@monaco-editor/react": "^4.6.0", - "@rjsf/chakra-ui": "v5.0.0-beta.10", - "@rjsf/core": "v5.0.0-beta.10", - "@rjsf/utils": "v5.0.0-beta.10", - "@rjsf/validator-ajv8": "v5.0.0-beta.10", + "@rjsf/chakra-ui": "v5.18.1", + "@rjsf/core": "v5.18.1", + "@rjsf/utils": "v5.18.1", + "@rjsf/validator-ajv8": "v5.18.1", "@rx-stream/pipe": "^0.7.1", "@sentry/integrations": "7.81.1", "@sentry/nextjs": "7.81.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77f9bc715..6a8fa1246 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,9 +4,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -overrides: - chakra-react-select: ^4.7.0 - dependencies: '@amplitude/analytics-browser': specifier: ^2.3.3 @@ -102,17 +99,17 @@ dependencies: specifier: ^4.6.0 version: 4.6.0(monaco-editor@0.44.0)(react-dom@18.2.0)(react@18.2.0) '@rjsf/chakra-ui': - specifier: v5.0.0-beta.10 - version: 5.0.0-beta.10(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/icons@2.1.1)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/react@2.8.2)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.11.3)(@rjsf/core@5.0.0-beta.10)(@rjsf/utils@5.0.0-beta.10)(@types/react@18.2.48)(framer-motion@11.0.3)(react-dom@18.2.0)(react@18.2.0) + specifier: v5.18.1 + version: 5.18.1(@chakra-ui/icons@2.1.1)(@chakra-ui/react@2.8.2)(@chakra-ui/system@2.6.2)(@rjsf/core@5.18.1)(@rjsf/utils@5.18.1)(@types/react@18.2.48)(chakra-react-select@4.7.6)(framer-motion@11.0.3)(react-dom@18.2.0)(react@18.2.0) '@rjsf/core': - specifier: v5.0.0-beta.10 - version: 5.0.0-beta.10(@rjsf/utils@5.0.0-beta.10)(react@18.2.0) + specifier: v5.18.1 + version: 5.18.1(@rjsf/utils@5.18.1)(react@18.2.0) '@rjsf/utils': - specifier: v5.0.0-beta.10 - version: 5.0.0-beta.10(react@18.2.0) + specifier: v5.18.1 + version: 5.18.1(react@18.2.0) '@rjsf/validator-ajv8': - specifier: v5.0.0-beta.10 - version: 5.0.0-beta.10(@rjsf/utils@5.0.0-beta.10) + specifier: v5.18.1 + version: 5.18.1(@rjsf/utils@5.18.1) '@rx-stream/pipe': specifier: ^0.7.1 version: 0.7.1 @@ -4371,25 +4368,25 @@ packages: resolution: {integrity: sha512-O0WKDOo0yhJuugCx6trZQj5jVJ9yR0ystG2JaNAemYUWce+pmM6WUEFIibnWyEJKdrDxhm75NoSRME35FNaM/Q==} dependencies: '@floating-ui/utils': 0.2.1 + dev: false /@floating-ui/core@1.6.0: resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==} dependencies: '@floating-ui/utils': 0.2.1 - dev: false /@floating-ui/dom@1.5.4: resolution: {integrity: sha512-jByEsHIY+eEdCjnTVu+E3ephzTOzkQ8hgUfGwos+bg7NlH33Zc5uO+QHz1mrQUOgIKKDD1RtS201P9NvAfq3XQ==} dependencies: '@floating-ui/core': 1.5.3 '@floating-ui/utils': 0.2.1 + dev: false /@floating-ui/dom@1.6.3: resolution: {integrity: sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==} dependencies: '@floating-ui/core': 1.6.0 '@floating-ui/utils': 0.2.1 - dev: false /@floating-ui/react-dom@2.0.6(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-IB8aCRFxr8nFkdYZgH+Otd9EVQPJoynxeFRGTB8voPoZMRWo8XjYuCRgpI1btvuKY69XMiLnW+ym7zoBHM90Rw==} @@ -4397,7 +4394,7 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' dependencies: - '@floating-ui/dom': 1.5.4 + '@floating-ui/dom': 1.6.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: true @@ -7677,55 +7674,51 @@ packages: resolution: {integrity: sha512-l3YHBLAol6d/IKnB9LhpD0cEZWAoe3eFKUyTYWmFmCO2Q/WOckxLQAUyMZWwZV2M/m3+4vgRoaolFqaII82/TA==} dev: false - /@rjsf/chakra-ui@5.0.0-beta.10(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/icons@2.1.1)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/react@2.8.2)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.11.3)(@rjsf/core@5.0.0-beta.10)(@rjsf/utils@5.0.0-beta.10)(@types/react@18.2.48)(framer-motion@11.0.3)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-WHTOLrim5SgSR6rfxr+gu2fjIo69W0mL6dYngI9mI6Ep7xlpA9OLtE/njDDnwMczTVo4MkbCx/D1y5eqPeZyoQ==} + /@rjsf/chakra-ui@5.18.1(@chakra-ui/icons@2.1.1)(@chakra-ui/react@2.8.2)(@chakra-ui/system@2.6.2)(@rjsf/core@5.18.1)(@rjsf/utils@5.18.1)(@types/react@18.2.48)(chakra-react-select@4.7.6)(framer-motion@11.0.3)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Aut0m3g9Wu9QMEuPsiENun6XW3vUxvsaldR86hZ416JFyqAqxo8YJlIj6YsmMlo2n6+xMMCtD1Vz5utBbVuDjw==} engines: {node: '>=14'} peerDependencies: '@chakra-ui/icons': '>=1.1.1' '@chakra-ui/react': '>=1.7.3' - '@rjsf/core': ^5.0.0-beta.1 - '@rjsf/utils': ^5.0.0-beta.1 + '@chakra-ui/system': '>=1.12.1' + '@rjsf/core': ^5.18.x + '@rjsf/utils': ^5.18.x + chakra-react-select: '>=3.3.8' framer-motion: '>=5.6.0' react: ^16.14.0 || >=17 dependencies: '@chakra-ui/icons': 2.1.1(@chakra-ui/system@2.6.2)(react@18.2.0) '@chakra-ui/react': 2.8.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(@types/react@18.2.48)(framer-motion@11.0.3)(react-dom@18.2.0)(react@18.2.0) - '@rjsf/core': 5.0.0-beta.10(@rjsf/utils@5.0.0-beta.10)(react@18.2.0) - '@rjsf/utils': 5.0.0-beta.10(react@18.2.0) + '@chakra-ui/system': 2.6.2(@emotion/react@11.11.3)(@emotion/styled@11.11.0)(react@18.2.0) + '@rjsf/core': 5.18.1(@rjsf/utils@5.18.1)(react@18.2.0) + '@rjsf/utils': 5.18.1(react@18.2.0) chakra-react-select: 4.7.6(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.11.3)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) framer-motion: 11.0.3(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-select: 5.8.0(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) transitivePeerDependencies: - - '@chakra-ui/form-control' - - '@chakra-ui/icon' - - '@chakra-ui/layout' - - '@chakra-ui/media-query' - - '@chakra-ui/menu' - - '@chakra-ui/spinner' - - '@chakra-ui/system' - - '@emotion/react' - '@types/react' - react-dom dev: false - /@rjsf/core@5.0.0-beta.10(@rjsf/utils@5.0.0-beta.10)(react@18.2.0): - resolution: {integrity: sha512-L/ZqK3/3uXhRLcvfOX3uL3adiJ0i7deV/egUkwnJQvqv1bIn58vY4wfiJwqTc8seiAHtyPCvehWZtLthyVP9Rw==} + /@rjsf/core@5.18.1(@rjsf/utils@5.18.1)(react@18.2.0): + resolution: {integrity: sha512-325fOPqupCkEkH4/ufeAbaezyknCTRQx8XeK9PhtWBSwpVggJg003C2RFL7g7HdFkz546KjEBGKeJ74HXC/kVg==} engines: {node: '>=14'} peerDependencies: - '@rjsf/utils': ^5.0.0-beta.1 + '@rjsf/utils': ^5.18.x react: ^16.14.0 || >=17 dependencies: - '@rjsf/utils': 5.0.0-beta.10(react@18.2.0) + '@rjsf/utils': 5.18.1(react@18.2.0) lodash: 4.17.21 lodash-es: 4.17.21 + markdown-to-jsx: 7.4.5(react@18.2.0) nanoid: 3.3.7 prop-types: 15.8.1 react: 18.2.0 dev: false - /@rjsf/utils@5.0.0-beta.10(react@18.2.0): - resolution: {integrity: sha512-cXXD9KHILJKk80ma69RN/53hxyPNxpJvyxEVXN1C9xg/VVhjrDTevTgvsfsVshqc8xTevbxVt6P8pm8ITG5QPw==} + /@rjsf/utils@5.18.1(react@18.2.0): + resolution: {integrity: sha512-BxXd5C8gxOSDCSgfDT+XZHpBZtu4F0jJZsnMQstWJ+9QKpmTiuvbkjk3c1J4zZ3CRNgGghVH5otU5gvzVWIxpQ==} engines: {node: '>=14'} peerDependencies: react: ^16.14.0 || >=17 @@ -7738,13 +7731,13 @@ packages: react-is: 18.2.0 dev: false - /@rjsf/validator-ajv8@5.0.0-beta.10(@rjsf/utils@5.0.0-beta.10): - resolution: {integrity: sha512-ADt6zzUkZ1cdJlo1aUZgR4dSuRSCcSeHa/O1JJe0oXfljgVCWMu+IYSIGtEHBFnWL1kedBaOn60m6XTVSvyBdA==} + /@rjsf/validator-ajv8@5.18.1(@rjsf/utils@5.18.1): + resolution: {integrity: sha512-Cb4++kru+XL8q5FJDWMpongtY7zLUoNqZaHDbYdkPWLl0Q7duGMfYOIRAhPpWhaI9KAjj19kCpAjQicA8gEgaw==} engines: {node: '>=14'} peerDependencies: - '@rjsf/utils': ^5.0.0-beta.1 + '@rjsf/utils': ^5.18.x dependencies: - '@rjsf/utils': 5.0.0-beta.10(react@18.2.0) + '@rjsf/utils': 5.18.1(react@18.2.0) ajv: 8.12.0 ajv-formats: 2.1.1(ajv@8.12.0) lodash: 4.17.21 @@ -16402,6 +16395,15 @@ packages: react: 18.2.0 dev: true + /markdown-to-jsx@7.4.5(react@18.2.0): + resolution: {integrity: sha512-c8NB0H/ig+FOWssE9be0PKsYbCDhcWEkicxMnpdfUuHbFljnen4LAdgUShOyR/PgO3/qKvt9cwfQ0U/zQvZ44A==} + engines: {node: '>= 10'} + peerDependencies: + react: '>= 0.14.0' + dependencies: + react: 18.2.0 + dev: false + /match-sorter@6.3.3: resolution: {integrity: sha512-sgiXxrRijEe0SzHKGX4HouCpfHRPnqteH42UdMEW7BlWy990ZkzcvonJGv4Uu9WE7Y1f8Yocm91+4qFPCbmNww==} dependencies: @@ -18677,7 +18679,7 @@ packages: '@babel/runtime': 7.23.8 '@emotion/cache': 11.11.0 '@emotion/react': 11.11.3(@types/react@18.2.48)(react@18.2.0) - '@floating-ui/dom': 1.5.4 + '@floating-ui/dom': 1.6.3 '@types/react-transition-group': 4.4.10 memoize-one: 6.0.0 prop-types: 15.8.1 diff --git a/src/lib/components/json-schema/form/fields/ArrayField.tsx b/src/lib/components/json-schema/form/fields/ArrayField.tsx index 931db4879..2606844da 100644 --- a/src/lib/components/json-schema/form/fields/ArrayField.tsx +++ b/src/lib/components/json-schema/form/fields/ArrayField.tsx @@ -1,4 +1,5 @@ -/* eslint-disable sonarjs/no-identical-functions */ +/* eslint-disable class-methods-use-this */ +/* eslint-disable react/no-unused-class-component-methods */ /* eslint-disable sonarjs/cognitive-complexity */ /* eslint-disable no-nested-ternary */ /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -8,8 +9,10 @@ import type { ErrorSchema, Field, FieldProps, + FormContextType, IdSchema, RJSFSchema, + StrictRJSFSchema, UiSchema, } from "@rjsf/utils"; import { @@ -21,11 +24,13 @@ import { isFixedItems, ITEMS_KEY, optionsList, + TranslatableString, } from "@rjsf/utils"; +import { cloneDeep } from "lodash"; +import get from "lodash/get"; import isObject from "lodash/isObject"; import set from "lodash/set"; import { Component } from "react"; -import type React from "react"; import * as uuid from "uuid"; import { isNullFormData } from "../utils"; @@ -80,18 +85,23 @@ function keyedToPlainFormData( /** The `ArrayField` component is used to render a field in the schema that is of type `array`. It supports both normal * and fixed array, allowing user to add and remove elements from the array data. */ -class ArrayField extends Component< - FieldProps, - ArrayFieldState -> { +class ArrayField< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +> extends Component, ArrayFieldState> { /** 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: Readonly>, + static getDerivedStateFromProps< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, + >( + nextProps: Readonly>, prevState: Readonly> ) { // Don't call getDerivedStateFromProps if keyed formdata was just updated. @@ -118,27 +128,11 @@ class ArrayField extends Component< }; } - /** 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 - */ - static isItemRequired(itemSchema: RJSFSchema) { - if (Array.isArray(itemSchema.type)) { - // While we don't yet support composite/nullable jsonschema types, it's - // future-proof to check for requirement against these. - return !itemSchema.type.includes("null"); - } - // All non-null array item types are inherently required by design - return itemSchema.type !== "null"; - } - /** Constructs an `ArrayField` from the `props`, generating the initial keyed data from the `formData` * * @param props - The `FieldProps` for this template */ - constructor(props: FieldProps) { + constructor(props: FieldProps) { super(props); const { formData = [] } = props; const keyedFormData = generateKeyedFormData(formData); @@ -148,47 +142,97 @@ class ArrayField extends Component< }; } + /** 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) + ) + ); + } + /** 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 = (): T => { const { schema, registry } = this.props; const { schemaUtils } = registry; - let itemSchema = schema.items as RJSFSchema; + let itemSchema = schema.items as S; if (isFixedItems(schema) && allowAdditionalItems(schema)) { - itemSchema = schema.additionalItems as RJSFSchema; + itemSchema = schema.additionalItems as S; } - // Cast this as a T to work around schema utils being for T[] caused by the FieldProps call on the class + // Cast this as a T to work around schema utils being for T[] caused by the FieldProps call on the class return schemaUtils.getDefaultFormState(itemSchema) as unknown as T; }; - /** 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 + /** 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 */ - onAddClick = (event: MouseEvent) => { + handleAddClick = (event: MouseEvent, index?: number) => { if (event) { event.preventDefault(); } - const { onChange } = this.props; + const { onChange, errorSchema } = this.props; const { keyedFormData } = this.state; + // refs #195: revalidate to ensure properly reindexing errors + let newErrorSchema: ErrorSchema; + if (errorSchema) { + newErrorSchema = {}; + Object.keys(errorSchema).forEach((_, idx) => { + if (index === undefined || idx < index) { + set(newErrorSchema, [idx], errorSchema[idx]); + } else if (idx >= index) { + set(newErrorSchema, [idx + 1], errorSchema[idx]); + } + }); + } + const newKeyedFormDataRow: KeyedFormDataType = { key: generateRowId(), item: this.getNewFormDataRow(), }; - const newKeyedFormData = [...keyedFormData, newKeyedFormDataRow]; + const newKeyedFormData = [...keyedFormData]; + if (index !== undefined) { + newKeyedFormData.splice(index, 0, newKeyedFormDataRow); + } else { + newKeyedFormData.push(newKeyedFormDataRow); + } this.setState( { keyedFormData: newKeyedFormData, updatedKeyedFormData: true, }, - () => onChange(keyedToPlainFormData(newKeyedFormData)) + () => + onChange( + keyedToPlainFormData(newKeyedFormData), + newErrorSchema as ErrorSchema + ) ); }; + /** 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: MouseEvent) => { + 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 @@ -196,25 +240,58 @@ class ArrayField extends Component< * @param index - The index at which the add button is clicked */ onAddIndexClick = (index: number) => { + return (event: MouseEvent) => { + 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: number) => { return (event: MouseEvent) => { if (event) { event.preventDefault(); } - const { onChange } = this.props; + + const { onChange, errorSchema } = this.props; const { keyedFormData } = this.state; + // refs #195: revalidate to ensure properly reindexing errors + let newErrorSchema: ErrorSchema; + if (errorSchema) { + newErrorSchema = {}; + Object.keys(errorSchema).forEach((_, idx) => { + if (idx <= index) { + set(newErrorSchema, [idx], errorSchema[idx]); + } else { + set(newErrorSchema, [idx + 1], errorSchema[idx]); + } + }); + } + const newKeyedFormDataRow: KeyedFormDataType = { key: generateRowId(), - item: this.getNewFormDataRow(), + item: cloneDeep(keyedFormData[index].item), }; const newKeyedFormData = [...keyedFormData]; - newKeyedFormData.splice(index, 0, newKeyedFormDataRow); - + if (index !== undefined) { + newKeyedFormData.splice(index + 1, 0, newKeyedFormDataRow); + } else { + newKeyedFormData.push(newKeyedFormDataRow); + } this.setState( { keyedFormData: newKeyedFormData, updatedKeyedFormData: true, }, - () => onChange(keyedToPlainFormData(newKeyedFormData)) + () => + onChange( + keyedToPlainFormData(newKeyedFormData), + newErrorSchema as ErrorSchema + ) ); }; }; @@ -318,7 +395,7 @@ class ArrayField extends Component< * @param index - The index of the item being changed */ onChangeForIndex = (index: number) => { - return (value: any, newErrorSchema?: ErrorSchema) => { + return (value: any, newErrorSchema?: ErrorSchema, id?: string) => { const { formData, onChange, errorSchema } = this.props; const arrayData = Array.isArray(formData) ? formData : []; const newFormData = arrayData.map((item: T, i: number) => { @@ -332,15 +409,16 @@ class ArrayField extends Component< errorSchema && { ...errorSchema, [index]: newErrorSchema, - } + }, + id ); }; }; /** Callback handler used to change the value for a checkbox */ onSelectChange = (value: any) => { - const { onChange } = this.props; - onChange(value); + const { onChange, idSchema } = this.props; + onChange(value, undefined, idSchema && idSchema.$id); }; /** Determines whether more items can be added to the array. If the uiSchema indicates the array doesn't allow adding @@ -351,8 +429,11 @@ class ArrayField extends Component< * @returns - True if the item is addable otherwise false */ canAddItem(formItems: any[]) { - const { schema, uiSchema } = this.props; - let { addable } = getUiOptions(uiSchema); + const { schema, uiSchema, registry } = this.props; + let { addable } = getUiOptions( + uiSchema, + registry.globalUiOptions + ); if (addable !== false) { // if ui:options.addable was not explicitly set to false, we can add // another item if we have not exceeded maxItems yet @@ -365,6 +446,22 @@ class ArrayField extends Component< return addable; } + /** 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: S) { + if (Array.isArray(itemSchema.type)) { + // While we don't yet support composite/nullable jsonschema types, it's + // future-proof to check for requirement against these. + return !itemSchema.type.includes("null"); + } + // All non-null array item types are inherently required by design + return itemSchema.type !== "null"; + } + /** Renders a normal array without any limitations of length */ renderNormalArray() { @@ -374,6 +471,7 @@ class ArrayField extends Component< errorSchema, idSchema, name, + title: titleProp, disabled = false, readonly = false, autofocus = false, @@ -389,17 +487,17 @@ class ArrayField extends Component< } = this.props; const { keyedFormData } = this.state; - - const title = schema.title === undefined ? name : schema.title; + const fieldTitle = schema.title || titleProp || name; const { schemaUtils, formContext } = registry; - const uiOptions = getUiOptions(uiSchema); - const schemaItems = isObject(schema.items) - ? (schema.items as RJSFSchema) - : {}; - const itemsSchema = schemaUtils.retrieveSchema(schemaItems); + const uiOptions = getUiOptions(uiSchema); + const schemaItems: S = isObject(schema.items) + ? (schema.items as S) + : ({} as S); + const itemsSchema: S = schemaUtils.retrieveSchema(schemaItems); const formData = keyedToPlainFormData(keyedFormData); - const arrayProps: ArrayFieldTemplateProps = { - canAdd: this.canAddItem(formData), + const canAdd = this.canAddItem(formData); + const arrayProps: ArrayFieldTemplateProps = { + canAdd, items: keyedFormData.map((keyedItem, index) => { const { key, item } = keyedItem; // While we are actually dealing with a single item of type T, the types require a T[], so cast @@ -420,6 +518,8 @@ class ArrayField extends Component< key, index, name: name && `${name}-${index}`, + title: fieldTitle ? `${fieldTitle}-${index + 1}` : undefined, + canAdd, canMoveUp: index > 0, canMoveDown: index < formData.length - 1, itemSchema, @@ -431,6 +531,7 @@ class ArrayField extends Component< onBlur, onFocus, rawErrors, + totalItems: keyedFormData.length, }); }), className: `field field-array field-array-of-${itemsSchema.type}`, @@ -441,14 +542,14 @@ class ArrayField extends Component< readonly, required, schema, - title, + title: fieldTitle, formContext, formData: rawFormData, rawErrors, registry, }; - const Template = getTemplate<"ArrayFieldTemplate", T[], F>( + const Template = getTemplate<"ArrayFieldTemplate", T[], S, F>( "ArrayFieldTemplate", registry, uiOptions @@ -505,14 +606,23 @@ class ArrayField extends Component< rawErrors, name, } = this.props; - const { widgets, formContext } = registry; - const title = schema.title || name; - - const { widget, ...options } = getUiOptions(uiSchema); - const Widget = getWidget(schema, widget, widgets); + const { widgets, formContext, globalUiOptions, schemaUtils } = registry; + const { + widget, + title: uiTitle, + ...options + } = getUiOptions(uiSchema, globalUiOptions); + const Widget = getWidget(schema, widget, widgets); + const label = uiTitle ?? schema.title ?? name; + const displayLabel = schemaUtils.getDisplayLabel( + schema, + uiSchema, + globalUiOptions + ); return ( extends Component< readonly={readonly} hideError={hideError} required={required} - label={title} + label={label} + hideLabel={!displayLabel} placeholder={placeholder} formContext={formContext} autofocus={autofocus} @@ -554,18 +665,25 @@ class ArrayField extends Component< rawErrors, name, } = this.props; - const { widgets, schemaUtils, formContext } = registry; - const itemsSchema = schemaUtils.retrieveSchema( - schema.items as RJSFSchema, - items - ); - const title = schema.title || name; + const { widgets, schemaUtils, formContext, globalUiOptions } = registry; + const itemsSchema = schemaUtils.retrieveSchema(schema.items as S, items); const enumOptions = optionsList(itemsSchema); - const { widget = "select", ...options } = getUiOptions(uiSchema); - const Widget = getWidget(schema, widget, widgets); + const { + widget = "select", + title: uiTitle, + ...options + } = getUiOptions(uiSchema, globalUiOptions); + const Widget = getWidget(schema, widget, widgets); + const label = uiTitle ?? schema.title ?? name; + const displayLabel = schemaUtils.getDisplayLabel( + schema, + uiSchema, + globalUiOptions + ); return ( extends Component< disabled={disabled} readonly={readonly} required={required} - label={title} + label={label} + hideLabel={!displayLabel} placeholder={placeholder} formContext={formContext} autofocus={autofocus} @@ -605,21 +724,30 @@ class ArrayField extends Component< formData: items = [], rawErrors, } = this.props; - const title = schema.title || name; - const { widgets, formContext } = registry; - const { widget = "files", ...options } = getUiOptions(uiSchema); - const Widget = getWidget(schema, widget, widgets); + const { widgets, formContext, globalUiOptions, schemaUtils } = registry; + const { + widget = "files", + title: uiTitle, + ...options + } = getUiOptions(uiSchema, globalUiOptions); + const Widget = getWidget(schema, widget, widgets); + const label = uiTitle ?? schema.title ?? name; + const displayLabel = schemaUtils.getDisplayLabel( + schema, + uiSchema, + globalUiOptions + ); return ( extends Component< formContext={formContext} autofocus={autofocus} rawErrors={rawErrors} - label="" + label={label} + hideLabel={!displayLabel} /> ); } @@ -645,6 +774,7 @@ class ArrayField extends Component< idSeparator = "_", idSchema, name, + title, disabled = false, readonly = false, autofocus = false, @@ -656,17 +786,17 @@ class ArrayField extends Component< } = this.props; const { keyedFormData } = this.state; let { formData: items = [] } = this.props; - const title = schema.title || name; - const uiOptions = getUiOptions(uiSchema); + const fieldTitle = schema.title || title || name; + const uiOptions = getUiOptions(uiSchema); const { schemaUtils, formContext } = registry; - const schemaItems = isObject(schema.items) - ? (schema.items as RJSFSchema[]) - : []; - const itemSchemas = schemaItems.map((item: RJSFSchema, index: number) => + const schemaItems: S[] = isObject(schema.items) + ? (schema.items as S[]) + : ([] as S[]); + const itemSchemas = schemaItems.map((item: S, index: number) => schemaUtils.retrieveSchema(item, formData[index] as unknown as T[]) ); const additionalSchema = isObject(schema.additionalItems) - ? schemaUtils.retrieveSchema(schema.additionalItems, formData) + ? schemaUtils.retrieveSchema(schema.additionalItems as S, formData) : null; if (!items || items.length < itemSchemas.length) { @@ -676,8 +806,9 @@ class ArrayField extends Component< } // These are the props passed into the render function - const arrayProps: ArrayFieldTemplateProps = { - canAdd: this.canAddItem(items) && !!additionalSchema, + const canAdd = this.canAddItem(items) && !!additionalSchema; + const arrayProps: ArrayFieldTemplateProps = { + canAdd, className: "field field-array field-array-fixed-items", disabled, idSchema, @@ -688,9 +819,9 @@ class ArrayField extends Component< const itemCast = item as unknown as T[]; const additional = index >= itemSchemas.length; const itemSchema = - additional && isObject(schema.additionalItems) - ? schemaUtils.retrieveSchema(schema.additionalItems, itemCast) - : itemSchemas[index]; + (additional && isObject(schema.additionalItems) + ? schemaUtils.retrieveSchema(schema.additionalItems as S, itemCast) + : itemSchemas[index]) || {}; const itemIdPrefix = idSchema.$id + idSeparator + index; const itemIdSchema = schemaUtils.toIdSchema( itemSchema, @@ -712,6 +843,8 @@ class ArrayField extends Component< key, index, name: name && `${name}-${index}`, + title: fieldTitle ? `${fieldTitle}-${index + 1}` : undefined, + canAdd, canRemove: additional, canMoveUp: index >= itemSchemas.length + 1, canMoveDown: additional && index < items.length - 1, @@ -724,6 +857,7 @@ class ArrayField extends Component< onBlur, onFocus, rawErrors, + totalItems: keyedFormData.length, }); }), onAddClick: this.onAddClick, @@ -732,12 +866,13 @@ class ArrayField extends Component< registry, schema, uiSchema, - title, + title: fieldTitle, formContext, + errorSchema, rawErrors, }; - const Template = getTemplate<"ArrayFieldTemplate", T[], F>( + const Template = getTemplate<"ArrayFieldTemplate", T[], S, F>( "ArrayFieldTemplate", registry, uiOptions @@ -754,26 +889,30 @@ class ArrayField extends Component< key: string; index: number; name: string; + title: string | undefined; + canAdd: boolean; canRemove?: boolean; - canMoveUp?: boolean; - canMoveDown?: boolean; - itemSchema: RJSFSchema; + canMoveUp: boolean; + canMoveDown: boolean; + itemSchema: S; itemData: T[]; - itemUiSchema: UiSchema; + itemUiSchema: UiSchema; itemIdSchema: IdSchema; itemErrorSchema?: ErrorSchema; autofocus?: boolean; - onBlur: FieldProps["onBlur"]; - onFocus: FieldProps["onFocus"]; + onBlur: FieldProps["onBlur"]; + onFocus: FieldProps["onFocus"]; rawErrors?: string[]; + totalItems: number; }) { const { key, index, name, + canAdd, canRemove = true, - canMoveUp = true, - canMoveDown = true, + canMoveUp, + canMoveDown, itemSchema, itemData, itemUiSchema, @@ -783,6 +922,8 @@ class ArrayField extends Component< onBlur, onFocus, rawErrors, + totalItems, + title, } = props; const { disabled, @@ -796,14 +937,18 @@ class ArrayField extends Component< } = this.props; const { fields: { ArraySchemaField, SchemaField }, + globalUiOptions, } = registry; const ItemSchemaField = ArraySchemaField || SchemaField; - const { orderable = true, removable = true } = getUiOptions( - uiSchema - ); + const { + orderable = true, + removable = true, + copyable = false, + } = getUiOptions(uiSchema, globalUiOptions); const has: { [key: string]: boolean } = { moveUp: orderable && canMoveUp, moveDown: orderable && canMoveDown, + copy: copyable && canAdd, remove: removable && canRemove, toolbar: false, }; @@ -813,6 +958,7 @@ class ArrayField extends Component< children: ( extends Component< idPrefix={idPrefix} idSeparator={idSeparator} idSchema={itemIdSchema} - required={ArrayField.isItemRequired(itemSchema)} + required={this.isItemRequired(itemSchema)} onChange={this.onChangeForIndex(index)} onBlur={onBlur} onFocus={onFocus} @@ -836,17 +982,22 @@ class ArrayField extends Component< ), className: "array-item", disabled, + canAdd, + hasCopy: has.copy, hasToolbar: has.toolbar, hasMoveUp: has.moveUp, hasMoveDown: has.moveDown, hasRemove: has.remove, index, + totalItems, key, onAddIndexClick: this.onAddIndexClick, + onCopyIndexClick: this.onCopyIndexClick, onDropIndexClick: this.onDropIndexClick, onReorderClick: this.onReorderClick, readonly, registry, + schema: itemSchema, uiSchema: itemUiSchema, }; } @@ -855,12 +1006,13 @@ class ArrayField extends Component< */ render() { const { schema, uiSchema, idSchema, registry } = this.props; - const { schemaUtils } = registry; + const { schemaUtils, translateString } = registry; if (!(ITEMS_KEY in schema)) { - const uiOptions = getUiOptions(uiSchema); + const uiOptions = getUiOptions(uiSchema); const UnsupportedFieldTemplate = getTemplate< "UnsupportedFieldTemplate", T[], + S, F >("UnsupportedFieldTemplate", registry, uiOptions); @@ -868,7 +1020,7 @@ class ArrayField extends Component< ); @@ -877,7 +1029,7 @@ class ArrayField extends Component< // If array has enum or uniqueItems set to true, call renderMultiSelect() to render the default multiselect widget or a custom widget, if specified. return this.renderMultiSelect(); } - if (isCustomWidget(uiSchema)) { + if (isCustomWidget(uiSchema)) { return this.renderCustomWidget(); } if (isFixedItems(schema)) { diff --git a/src/lib/components/json-schema/form/fields/BooleanField.tsx b/src/lib/components/json-schema/form/fields/BooleanField.tsx index dda047042..c34bae897 100644 --- a/src/lib/components/json-schema/form/fields/BooleanField.tsx +++ b/src/lib/components/json-schema/form/fields/BooleanField.tsx @@ -3,8 +3,9 @@ import type { EnumOptionsType, FieldProps, + FormContextType, RJSFSchema, - RJSFSchemaDefinition, + StrictRJSFSchema, } from "@rjsf/utils"; import { getUiOptions, getWidget, optionsList } from "@rjsf/utils"; import isObject from "lodash/isObject"; @@ -14,7 +15,11 @@ import isObject from "lodash/isObject"; * * @param props - The `FieldProps` for this template */ -function BooleanField(props: FieldProps) { +function BooleanField< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: FieldProps) { const { schema, name, @@ -25,23 +30,32 @@ function BooleanField(props: FieldProps) { required, disabled, readonly, + hideError, autofocus, + title, onChange, onFocus, onBlur, rawErrors, } = props; - const { title } = schema; - const { widgets, formContext } = registry; - const { widget = "select", ...options } = getUiOptions(uiSchema); - const Widget = getWidget(schema, widget, widgets); + const { title: schemaTitle } = schema; + const { widgets, formContext, globalUiOptions } = registry; + const { + widget = "select", + title: uiTitle, + // Unlike the other fields, don't use `getDisplayLabel()` since it always returns false for the boolean type + label: displayLabel = true, + ...options + } = getUiOptions(uiSchema, globalUiOptions); - let enumOptions: EnumOptionsType[] | undefined; + const Widget = getWidget(schema, widget, widgets); + let enumOptions: EnumOptionsType[] | undefined; + const label = uiTitle ?? schemaTitle ?? title ?? name; if (Array.isArray(schema.oneOf)) { - enumOptions = optionsList({ + enumOptions = optionsList({ oneOf: schema.oneOf - .map((option: RJSFSchemaDefinition) => { + .map((option) => { if (isObject(option)) { return { ...option, @@ -50,17 +64,16 @@ function BooleanField(props: FieldProps) { } return undefined; }) - .filter((o) => o) as RJSFSchemaDefinition[], // cast away the error that typescript can't grok is fixed - }); + .filter((o: any) => o) as S[], // cast away the error that typescript can't grok is fixed + } as unknown as S); } else { // We deprecated enumNames in v5. It's intentionally omitted from RSJFSchema type, so we need to cast here. - const schemaWithEnumNames = schema as RJSFSchema & { enumNames?: string[] }; - const enums = schema.enum ?? [false, true]; + const schemaWithEnumNames = schema as S & { enumNames?: string[] }; + const enums = schema.enum ?? [true, false]; if ( !schemaWithEnumNames.enumNames && - enums && enums.length === 2 && - enums.every((v) => typeof v === "boolean") + enums.every((v: any) => typeof v === "boolean") ) { enumOptions = [ { @@ -73,11 +86,11 @@ function BooleanField(props: FieldProps) { }, ]; } else { - enumOptions = optionsList({ + enumOptions = optionsList({ enum: enums, // NOTE: enumNames is deprecated, but still supported for now. enumNames: schemaWithEnumNames.enumNames, - } as RJSFSchema); + } as unknown as S); } } @@ -90,15 +103,18 @@ function BooleanField(props: FieldProps) { placeholder={readonly ? undefined : "Select boolean option"} schema={schema} uiSchema={uiSchema} - id={idSchema && idSchema.$id} + id={idSchema.$id} + name={name} onChange={onChange} onFocus={onFocus} onBlur={onBlur} - label={title === undefined ? name : title} + label={label} + hideLabel={!displayLabel} value={formData} required={required} disabled={disabled} readonly={readonly} + hideError={hideError} registry={registry} formContext={formContext} autofocus={autofocus} diff --git a/src/lib/components/json-schema/form/fields/MultiSchemaField.tsx b/src/lib/components/json-schema/form/fields/MultiSchemaField.tsx index 3e9384077..6674d0aaf 100644 --- a/src/lib/components/json-schema/form/fields/MultiSchemaField.tsx +++ b/src/lib/components/json-schema/form/fields/MultiSchemaField.tsx @@ -1,86 +1,112 @@ /* eslint-disable */ -import type { FieldProps, RJSFSchema } from "@rjsf/utils"; +import type { + FieldProps, + FormContextType, + RJSFSchema, + StrictRJSFSchema, + UiSchema, +} from "@rjsf/utils"; import { - ERRORS_KEY, + ANY_OF_KEY, deepEquals, + ERRORS_KEY, + getDiscriminatorFieldFromSchema, getUiOptions, getWidget, - guessType, + mergeSchemas, + ONE_OF_KEY, + TranslatableString, } from "@rjsf/utils"; import get from "lodash/get"; import isEmpty from "lodash/isEmpty"; import omit from "lodash/omit"; -import unset from "lodash/unset"; import { Component } from "react"; -import { getMatchingOptionFixed } from "../utils"; /** Type used for the state of the `AnyOfField` component */ -type AnyOfFieldState = { +type AnyOfFieldState = { /** The currently selected option */ - selectedOptionIndex: number; + selectedOption: number; + /** The option schemas after retrieving all $refs */ + retrievedOptions: S[]; }; -/** - * Replacement from react-jsonschema-form. Ensures fields are named correctly - * The `AnyOfField` component is used to render a field in the schema that is an `anyOf`, `allOf` - * or `oneOf`. It tracks the currently selected option and cleans up any irrelevant data in - * `formData`. +/** The `AnyOfField` component is used to render a field in the schema that is an `anyOf`, `allOf` or `oneOf`. It tracks + * the currently selected option and cleans up any irrelevant data in `formData`. * * @param props - The `FieldProps` for this template */ -class MultiSchemaField extends Component< - FieldProps, - AnyOfFieldState -> { +class MultiSchemaField< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +> extends Component, AnyOfFieldState> { /** Constructs an `AnyOfField` with the given `props` to initialize the initially selected option in state * * @param props - The `FieldProps` for this template */ - constructor(props: FieldProps) { + constructor(props: FieldProps) { super(props); - const { formData, options } = this.props; + const { + formData, + options, + registry: { schemaUtils }, + } = this.props; + // cache the retrieved options in state in case they have $refs to save doing it later + const retrievedOptions = options.map((opt: S) => + schemaUtils.retrieveSchema(opt, formData) + ); this.state = { - selectedOptionIndex: this.getMatchingOption(0, formData, options), + retrievedOptions, + selectedOption: this.getMatchingOption(0, formData, retrievedOptions), }; } - /** - * React lifecycle methos 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` + /** 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: Readonly>, + prevProps: Readonly>, prevState: Readonly ) { const { formData, options, idSchema } = this.props; - const { selectedOptionIndex } = this.state; + const { selectedOption } = this.state; + let newState = this.state; + if (!deepEquals(prevProps.options, options)) { + const { + registry: { schemaUtils }, + } = this.props; + // re-cache the retrieved options in state in case they have $refs to save doing it later + const retrievedOptions = options.map((opt: S) => + schemaUtils.retrieveSchema(opt, formData) + ); + newState = { selectedOption, retrievedOptions }; + } if ( !deepEquals(formData, prevProps.formData) && idSchema.$id === prevProps.idSchema.$id ) { + const { retrievedOptions } = newState; const matchingOption = this.getMatchingOption( - selectedOptionIndex, + selectedOption, formData, - options + retrievedOptions ); - if (!prevState || matchingOption === selectedOptionIndex) { - return; + if (prevState && matchingOption !== selectedOption) { + newState = { selectedOption: matchingOption, retrievedOptions }; } - - this.setState({ - selectedOptionIndex: matchingOption, - }); + } + if (newState !== this.state) { + this.setState(newState); } } - /** - * Determines the best matching option for the given `formData` and `options`. + /** 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 @@ -88,108 +114,85 @@ class MultiSchemaField extends Component< */ getMatchingOption( selectedOption: number, - formData: T, - options: RJSFSchema[] + formData: T | undefined, + options: S[] ) { const { - registry: { schemaUtils, rootSchema }, + schema, + registry: { schemaUtils }, } = this.props; - const option = getMatchingOptionFixed( - schemaUtils.getValidator(), + const discriminator = getDiscriminatorFieldFromSchema(schema); + return schemaUtils.getClosestMatchingOption( formData, options, - rootSchema + selectedOption, + discriminator ); - if (option !== -1) { - return option; - } - // If the form data matches none of the options, use the currently selected - // option, assuming it's available; otherwise use the first option - return selectedOption || 0; } - /** - * 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. + /** 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 - + * @param option - The new option value being selected */ - onOptionChange = (selectedOption: any) => { - const selectedOptionIndex = parseInt(selectedOption, 10); - // console.log("I HAVE SELECTED", selectedOptionIndex) - const { formData, onChange, options, registry } = this.props; + onOptionChange = (option: string) => { + const { selectedOption, retrievedOptions } = this.state; + const { formData, onChange, registry } = this.props; const { schemaUtils } = registry; - const newOption = schemaUtils.retrieveSchema( - options[selectedOptionIndex], + const intOption = option !== undefined ? parseInt(option, 10) : -1; + if (intOption === selectedOption) { + return; + } + const newOption = intOption >= 0 ? retrievedOptions[intOption] : undefined; + const oldOption = + selectedOption >= 0 ? retrievedOptions[selectedOption] : undefined; + + let newFormData = schemaUtils.sanitizeDataForNewSchema( + newOption, + oldOption, formData ); - - // If the new option is of type object and the current data is an object, - // discard properties added using the old option. - let newFormData: T | undefined; - if ( - guessType(formData) === "object" && - (newOption.type === "object" || newOption.properties) - ) { - newFormData = { ...formData }; - - const optionsToDiscard = options.slice(); - // Remove the newly selected option from the list of options to discard - optionsToDiscard.splice(selectedOptionIndex, 1); - // console.log("OPTIONS TO DISCARD", optionsToDiscard) - - // Discard any data added using other options - for (const option of optionsToDiscard) { - if (option.properties) { - for (const key in option.properties) { - if (key in newFormData) { - // console.log("UNSET", newFormData, key) - unset(newFormData, key); - } - } - } - } + if (newFormData && newOption) { + // Call getDefaultFormState to make sure defaults are populated on change. Pass "excludeObjectChildren" + // so that only the root objects themselves are created without adding undefined children properties + newFormData = schemaUtils.getDefaultFormState( + newOption, + newFormData, + "excludeObjectChildren" + ) as T; } - // Call getDefaultFormState to make sure defaults are populated on change. - const defaultFormState = schemaUtils.getDefaultFormState( - options[selectedOptionIndex], - newFormData - ) as T; - // console.log("DEFAULT FORM STATE", defaultFormState) - onChange(defaultFormState); + onChange(newFormData, undefined, this.getFieldId()); - this.setState({ - selectedOptionIndex, - }); + this.setState({ selectedOption: intOption }); }; - /** - * Renders the `AnyOfField` selector along with a `SchemaField` for the value of the `formData` + 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, - baseType, errorSchema = {}, formContext, - idSchema, onBlur, onFocus, - options, registry, - uiSchema, schema, - required, - readonly, + uiSchema, } = this.props; - const { widgets, fields, schemaUtils } = registry; - const { SchemaField } = fields; - const { selectedOptionIndex } = this.state; + const { widgets, fields, translateString, globalUiOptions, schemaUtils } = + registry; + const { SchemaField: _SchemaField } = fields; + const { selectedOption, retrievedOptions } = this.state; const { widget = "select", placeholder, @@ -197,65 +200,104 @@ class MultiSchemaField extends Component< autocomplete, title = schema.title, ...uiOptions - } = getUiOptions(uiSchema); - const Widget = getWidget({ type: "number" }, widget, widgets); + } = getUiOptions(uiSchema, globalUiOptions); + const Widget = getWidget({ type: "number" }, widget, widgets); const rawErrors = get(errorSchema, ERRORS_KEY, []); const fieldErrorSchema = omit(errorSchema, [ERRORS_KEY]); - const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema); + const displayLabel = schemaUtils.getDisplayLabel( + schema, + uiSchema, + globalUiOptions + ); - const selectedOption = options[selectedOptionIndex] || null; - let optionSchema; - if (selectedOption) { - // If the subschema doesn't declare a type, infer the type from the - // parent schema - optionSchema = selectedOption.type - ? selectedOption - : { ...selectedOption, type: baseType }; - } + const option = + selectedOption >= 0 ? retrievedOptions[selectedOption] || null : null; + let optionSchema: S | undefined | null; - // if the second option's type is "null" it's probably a rust optional. - // This means that the nonnull value should use the name as the option name - const enumOptions = options.map((option: RJSFSchema, index: number) => { - let optionTitle = option.title; + if (option) { + // merge top level required field + const { required } = schema; + // Merge in all the non-oneOf/anyOf properties and also skip the special ADDITIONAL_PROPERTY_FLAG property + optionSchema = required + ? (mergeSchemas({ required }, option) as S) + : option; + } - // Rust optional case - if (option.required?.length && option.required.length > 0) { - // there is one option available, so we can use its title - if (option.required.length === 1) { - optionTitle = option.required[0]; - } + // First we will check to see if there is an anyOf/oneOf override for the UI schema + let optionsUiSchema: UiSchema[] = []; + 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}"`); } - if (option.type === "string" && option.enum?.length === 1) { - optionTitle = option.enum[0]?.toString(); - } - // Rust optional case - else if (option.type === "null") { - optionTitle = `${name} as null`; - } - // Here we do the second part of the above, because the non optional - else if ( - options.length === 2 && - options.some((opt: RJSFSchema) => opt.type === "null") - ) { - optionTitle = 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}"`); } + } + // Then we pick the one that matches the selected option index, if one exists otherwise default to the main uiSchema + let optionUiSchema = uiSchema; + if (selectedOption >= 0 && optionsUiSchema.length > selectedOption) { + optionUiSchema = optionsUiSchema[selectedOption]; + } + + const translateEnum: TranslatableString = title + ? TranslatableString.TitleOptionPrefix + : TranslatableString.OptionPrefix; + const translateParams = title ? [title] : []; + const enumOptions = retrievedOptions.map( + (option: RJSFSchema, index: number) => { + // Also see if there is an override title in the uiSchema for each option, otherwise use the title from the option + const { title: uiTitle } = getUiOptions( + optionsUiSchema[index] + ); + + let optionTitle = uiTitle ?? option.title; + + // Rust optional case + if (option.required?.length && option.required.length > 0) { + // there is one option available, so we can use its title + if (option.required.length === 1) { + optionTitle = option.required[0]; + } + } + if (option.type === "string" && option.enum?.length === 1) { + optionTitle = option.enum[0]?.toString(); + } + // Rust optional case + else if (option.type === "null") { + optionTitle = `${name} as null`; + } + // Here we do the second part of the above, because the non optional + else if ( + retrievedOptions.length === 2 && + retrievedOptions.some((opt: RJSFSchema) => opt.type === "null") + ) { + optionTitle = name; + } - return { - label: optionTitle || `Option ${index + 1}`, - value: index, - }; - }); + return { + label: + optionTitle || + translateString( + translateEnum, + translateParams.concat(String(index + 1)) + ), + value: index, + }; + } + ); return (
extends Component< multiple={false} rawErrors={rawErrors} errorSchema={fieldErrorSchema} - value={selectedOptionIndex >= 0 ? selectedOptionIndex : undefined} + value={selectedOption >= 0 ? selectedOption : undefined} options={{ enumOptions, ...uiOptions }} registry={registry} formContext={formContext} @@ -274,8 +316,12 @@ class MultiSchemaField extends Component< hideLabel={!displayLabel} />
- {selectedOption !== null && ( - + {optionSchema && ( + <_SchemaField + {...this.props} + schema={optionSchema} + uiSchema={optionUiSchema} + /> )}
); diff --git a/src/lib/components/json-schema/form/fields/NullField.tsx b/src/lib/components/json-schema/form/fields/NullField.tsx index 3a2a08207..85767c62d 100644 --- a/src/lib/components/json-schema/form/fields/NullField.tsx +++ b/src/lib/components/json-schema/form/fields/NullField.tsx @@ -1,13 +1,22 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Text } from "@chakra-ui/react"; -import type { FieldProps } from "@rjsf/utils"; +import type { + FieldProps, + FormContextType, + RJSFSchema, + StrictRJSFSchema, +} from "@rjsf/utils"; /** The `NullField` component is used to render a field in the schema is null. It also ensures that the `formData` is * also set to null if it has no value. * * @param props - The `FieldProps` for this template */ -function NullField(props: FieldProps) { +function NullField< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: FieldProps) { const { idSchema } = props; return idSchema.$id === "root" ? ( ) { - const actualOneOfOptions = Object.keys(value); +function fixSchema(schema: RJSFSchema) { + if (schema.type === "object" && isUndefined(schema.properties)) + schema.properties = {}; - // check if there are TOO many options selected in the value - if (actualOneOfOptions.length === 2) { - console.log("Deleting", actualOneOfOptions[0]); - delete value[actualOneOfOptions[0]]; - } else if (actualOneOfOptions.length > 2) { - console.warn("Unexpected number of oneOf options", actualOneOfOptions); - // TODO: throw an error? - } -} - -/** - * A helper method to fix a bug in the rjsf library. - * The bug is that `oneOf` properties will ALWAYS contain the first option. - * @param formData - * @param collapsedSchema - the schema that has been collapsed via `createSchemaUtils` - */ -function fixOneOfKeys( - formData: Record, - collapsedSchema: RJSFSchema -) { - // if the entry is supposed to be a oneof *itself*, then check that it only has one key - if (collapsedSchema.oneOf) { - deleteExtraneousOneOfKeys(formData); - - // Now recursively check the keys (though there should only be one) - for (const [key, value] of Object.entries(formData)) { - if (typeof value === "object") { - // Find the schema for the key('s value) - const valueSchema = collapsedSchema.oneOf - .filter((i): i is RJSFSchema => typeof i === "object") - .find((i) => i.required?.length === 1 && i.required[0] === key); - - if (!valueSchema) return; - fixOneOfKeys(value as Record, valueSchema); - } - } - return; - } - - // iterate through each entry in the formData to check if it's oneOf - for (const [key, value] of Object.entries(formData)) { - // skip non-objects - if (!value || typeof value !== "object") continue; - - const valueSchema = collapsedSchema.properties?.[key]; - // Skip those without a valid schema - if (!valueSchema || typeof valueSchema === "boolean") continue; - - // if the entry is supposed to be a oneof, then check that it only has one key - if (valueSchema.oneOf) { - // console.log("Found oneOf", key, value); - deleteExtraneousOneOfKeys(value as Record); - } - - // Since we know that the value is an object, we can recurse - fixOneOfKeys(value as Record, valueSchema); - } + Object.values(schema).forEach((value) => { + if (typeof value === "object") fixSchema(value); + }); } export interface JsonSchemaFormProps @@ -119,37 +64,17 @@ export const JsonSchemaForm: FC = ({ }) => { const [formData, setFormData] = useState(initialFormData); - const collapsedSchema = useMemo( - () => createSchemaUtils(v8Validator, schema).retrieveSchema(schema), - [schema] - ); - - const fixOneOfKeysCallback = useCallback( - (data: Record) => fixOneOfKeys(data, collapsedSchema), - [collapsedSchema] - ); - const onSubmit = (values: Record) => { - fixOneOfKeysCallback(values); console.log("onSubmit", values); - propsOnSubmit?.(values); }; const onChange = useCallback( (data: JsonDataType, errors: RJSFValidationError[]) => { - if (data) { - const values: JsonDataType = structuredClone(data); - if (typeof values === "object") - fixOneOfKeysCallback(values as Record); - - if (!isEqual(formData, values)) { - setFormData(values); - propsOnChange?.(values, errors); - } - } + setFormData(data); + propsOnChange?.(data, errors); }, - [fixOneOfKeysCallback, formData, propsOnChange] + [formData, propsOnChange] ); useEffect(() => { @@ -160,6 +85,7 @@ export const JsonSchemaForm: FC = ({ propsOnChange?.(initialFormData, errors); }, [JSON.stringify(initialFormData)]); + fixSchema(schema); return (
= ({ onSubmit(values); }} onError={() => console.error("errors")} + experimental_defaultFormStateBehavior={{ + // Assign value to formData when only default is set + emptyObjectFields: "skipEmptyDefaults", + }} /> ); }; diff --git a/src/lib/components/json-schema/form/templates/ArrayFieldItemTemplate.tsx b/src/lib/components/json-schema/form/templates/ArrayFieldItemTemplate.tsx index d693b9c06..744c9ae99 100644 --- a/src/lib/components/json-schema/form/templates/ArrayFieldItemTemplate.tsx +++ b/src/lib/components/json-schema/form/templates/ArrayFieldItemTemplate.tsx @@ -1,45 +1,43 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Box, ButtonGroup, HStack } from "@chakra-ui/react"; -import type { ArrayFieldTemplateItemType } from "@rjsf/utils"; -import { useMemo } from "react"; +import type { + ArrayFieldTemplateItemType, + FormContextType, + RJSFSchema, + StrictRJSFSchema, +} from "@rjsf/utils"; -export default function ArrayFieldItemTemplate( - props: ArrayFieldTemplateItemType -) { +/** The `ArrayFieldItemTemplate` component is the template used to render an items of an array. + * + * @param props - The `ArrayFieldTemplateItemType` props for the component + */ +export default function ArrayFieldItemTemplate< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: ArrayFieldTemplateItemType) { const { children, + className, disabled, hasToolbar, hasMoveDown, hasMoveUp, hasRemove, + hasCopy, index, + onCopyIndexClick, onDropIndexClick, onReorderClick, readonly, - uiSchema, registry, + uiSchema, } = props; - const { MoveDownButton, MoveUpButton, RemoveButton } = + const { CopyButton, MoveDownButton, MoveUpButton, RemoveButton } = registry.templates.ButtonTemplates; - const onRemoveClick = useMemo( - () => onDropIndexClick(index), - [index, onDropIndexClick] - ); - - const onArrowUpClick = useMemo( - () => onReorderClick(index, index - 1), - [index, onReorderClick] - ); - - const onArrowDownClick = useMemo( - () => onReorderClick(index, index + 1), - [index, onReorderClick] - ); - return ( - + ( {(hasMoveUp || hasMoveDown) && ( )} {(hasMoveUp || hasMoveDown) && ( + )} + {hasCopy && ( + )} {hasRemove && ( )} diff --git a/src/lib/components/json-schema/form/templates/ArrayFieldTemplate.tsx b/src/lib/components/json-schema/form/templates/ArrayFieldTemplate.tsx index e885af863..967b119f6 100644 --- a/src/lib/components/json-schema/form/templates/ArrayFieldTemplate.tsx +++ b/src/lib/components/json-schema/form/templates/ArrayFieldTemplate.tsx @@ -1,8 +1,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Box, Flex, Grid, GridItem, Text } from "@chakra-ui/react"; +import { Flex, Grid, GridItem, Text } from "@chakra-ui/react"; import type { ArrayFieldTemplateItemType, ArrayFieldTemplateProps, + FormContextType, + RJSFSchema, + StrictRJSFSchema, } from "@rjsf/utils"; import { getTemplate, getUiOptions } from "@rjsf/utils"; @@ -10,59 +13,69 @@ import { isNullFormData } from "../utils"; import { FieldTypeTag } from "./FieldTypeTag"; -export default function ArrayFieldTemplate( - props: ArrayFieldTemplateProps -) { +/** The `ArrayFieldTemplate` component is the template used to render all items in an array. + * + * @param props - The `ArrayFieldTemplateItemType` props for the component + */ +export default function ArrayFieldTemplate< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: ArrayFieldTemplateProps) { const { formData, canAdd, + className, disabled, idSchema, uiSchema, items, onAddClick, - readonly = false, + readonly, registry, required, schema, title, } = props; - const uiOptions = getUiOptions(uiSchema); + const uiOptions = getUiOptions(uiSchema); const ArrayFieldDescriptionTemplate = getTemplate< "ArrayFieldDescriptionTemplate", T, + S, F >("ArrayFieldDescriptionTemplate", registry, uiOptions); - const ArrayFieldItemTemplate = getTemplate<"ArrayFieldItemTemplate", T, F>( + const ArrayFieldItemTemplate = getTemplate<"ArrayFieldItemTemplate", T, S, F>( "ArrayFieldItemTemplate", registry, uiOptions ); - const ArrayFieldTitleTemplate = getTemplate<"ArrayFieldTitleTemplate", T, F>( + const ArrayFieldTitleTemplate = getTemplate< "ArrayFieldTitleTemplate", - registry, - uiOptions - ); + T, + S, + F + >("ArrayFieldTitleTemplate", registry, uiOptions); // Button templates are not overridden in the uiSchema const { ButtonTemplates: { AddButton }, } = registry.templates; return ( - +
- @@ -99,7 +112,7 @@ export default function ArrayFieldTemplate( ) : ( items.map( - ({ key, ...itemProps }: ArrayFieldTemplateItemType) => ( + ({ key, ...itemProps }: ArrayFieldTemplateItemType) => ( @@ -113,11 +126,12 @@ export default function ArrayFieldTemplate( onClick={onAddClick} disabled={disabled || readonly} uiSchema={uiSchema} + registry={registry} /> )} )} - +
); } diff --git a/src/lib/components/json-schema/form/templates/BaseInputTemplate.tsx b/src/lib/components/json-schema/form/templates/BaseInputTemplate.tsx index 4a8606b2d..2426211bc 100644 --- a/src/lib/components/json-schema/form/templates/BaseInputTemplate.tsx +++ b/src/lib/components/json-schema/form/templates/BaseInputTemplate.tsx @@ -1,3 +1,4 @@ +/* eslint-disable complexity */ /* eslint-disable sonarjs/cognitive-complexity */ /* eslint-disable react/destructuring-assignment */ /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -12,8 +13,20 @@ import { Text, } from "@chakra-ui/react"; // import { fromBase64, fromUtf8, toBase64, toUtf8 } from "@cosmjs/encoding"; -import type { WidgetProps } from "@rjsf/utils"; -import { getInputProps, getTemplate, getUiOptions } from "@rjsf/utils"; +import type { + BaseInputTemplateProps, + FormContextType, + RJSFSchema, + StrictRJSFSchema, +} from "@rjsf/utils"; +import { + ariaDescribedByIds, + descriptionId, + examplesId, + getInputProps, + getTemplate, +} from "@rjsf/utils"; +import { useCallback } from "react"; import type { ChangeEvent, FocusEvent } from "react"; import { isSchemaTypeString } from "../utils"; @@ -76,48 +89,84 @@ const getBaseInputPlaceholder = ( return `${readonly ? "" : "Left blank to send as "}null`; }; -const BaseInputTemplate = (props: WidgetProps) => { +/** The `BaseInputTemplate` is the template to use to render the basic `` component for the `core` theme. + * It is used as the template for rendering many of the based widgets that differ by `type` and callbacks only. + * It can be customized/overridden for other themes or individual implementations as needed. + * + * @param props - The `WidgetProps` for this template + */ +export default function BaseInputTemplate< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: BaseInputTemplateProps) { const { id, - type, + name, // remove this from ...rest value, label, - schema, - uiSchema, - onChange, + readonly, + disabled, + autofocus, onBlur, onFocus, + onChange, + onChangeOverride, options, required = false, - readonly, - rawErrors, - autofocus, - // placeholder, - disabled, - // formContext, + schema, + uiSchema, + formContext, registry, + rawErrors, + type, + hideLabel, // remove this from ...rest + hideError, // remove this from ...rest + ...rest } = props; - const inputProps = getInputProps(schema, type, options); - - const uiOptions = getUiOptions(uiSchema); const DescriptionFieldTemplate = getTemplate< "DescriptionFieldTemplate", T, + S, F - >("DescriptionFieldTemplate", registry, uiOptions); + >("DescriptionFieldTemplate", registry, options); const { schemaUtils } = registry; const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema) && (!!label || !!schema.title); - const handleOnChange = ({ target }: ChangeEvent) => - onChange(target.value === "" ? options.emptyValue : target.value); - const handleOnBlur = ({ target }: FocusEvent) => - onBlur(id, target.value); - const handleOnFocus = ({ target }: FocusEvent) => - onFocus(id, target.value); + // Note: since React 15.2.0 we can't forward unknown element attributes, so we + // exclude the "options" and "schema" ones here. + if (!id) { + // console.log("No id for", props); + throw new Error(`no id for props ${JSON.stringify(props)}`); + } + const inputProps = { + ...rest, + ...getInputProps(schema, type, options), + }; + let inputValue; + if (inputProps.type === "number" || inputProps.type === "integer") { + inputValue = value || value === 0 ? value : ""; + } else { + inputValue = value == null ? "" : value; + } + + const handleOnChange = useCallback( + ({ target }: ChangeEvent) => + onChange(value === "" ? options.emptyValue : target.value), + [onChange, options.emptyValue, value] + ); + const handleOnBlur = useCallback( + ({ target }: FocusEvent) => onBlur(id, target.value), + [onBlur, id] + ); + const handleOnFocus = useCallback( + ({ target }: FocusEvent) => onFocus(id, target.value), + [onFocus, id] + ); // const rightAddon = renderRightAddOn( // value, // label, @@ -127,9 +176,9 @@ const BaseInputTemplate = (props: WidgetProps) => { // ); const isStringType = isSchemaTypeString(schema.type); - return ( (props: WidgetProps) => { (props: WidgetProps) => { // )?.[1] } {...inputProps} - list={schema.examples ? `examples_${id}` : undefined} + list={schema.examples ? examplesId(id) : undefined} + onChange={handleOnChange} + onBlur={handleOnBlur} + onFocus={handleOnFocus} + aria-describedby={ariaDescribedByIds(id, !!schema.examples)} _disabled={{ color: "text.main", cursor: "not-allowed", @@ -188,8 +238,9 @@ const BaseInputTemplate = (props: WidgetProps) => { /> {/* {rightAddon && {rightAddon}} */} - {!readonly && isStringType && ( + {isStringType && ( { @@ -201,8 +252,8 @@ const BaseInputTemplate = (props: WidgetProps) => { )} - {Array.isArray(schema.examples) ? ( - + {Array.isArray(schema.examples) && ( + (id)}> {(schema.examples as string[]) .concat( schema.default && !schema.examples.includes(schema.default) @@ -215,18 +266,17 @@ const BaseInputTemplate = (props: WidgetProps) => { ); })} - ) : null} + )} {!!schema.description && ( (id)} description={schema.description} + schema={schema} registry={registry} /> )} ); -}; - -export default BaseInputTemplate; +} diff --git a/src/lib/components/json-schema/form/templates/DescriptionFieldTemplate.tsx b/src/lib/components/json-schema/form/templates/DescriptionFieldTemplate.tsx index 744a4f834..c9e11aa23 100644 --- a/src/lib/components/json-schema/form/templates/DescriptionFieldTemplate.tsx +++ b/src/lib/components/json-schema/form/templates/DescriptionFieldTemplate.tsx @@ -1,15 +1,21 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Text } from "@chakra-ui/react"; -import type { DescriptionFieldProps } from "@rjsf/utils"; +import type { + DescriptionFieldProps, + FormContextType, + RJSFSchema, + StrictRJSFSchema, +} from "@rjsf/utils"; -/** - * Description Field for the jsonschema forms. - * @param description - * @param id +/** The `DescriptionField` is the template to use to render the description of a field + * + * @param props - The `DescriptionFieldProps` for this component */ -const DescriptionFieldTemplate = ( - props: DescriptionFieldProps -) => { +export default function DescriptionField< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: DescriptionFieldProps) { const { id, description } = props; if (!description) { return null; @@ -32,6 +38,4 @@ const DescriptionFieldTemplate = ( {description} ); -}; - -export default DescriptionFieldTemplate; +} diff --git a/src/lib/components/json-schema/form/templates/FieldTemplate.tsx b/src/lib/components/json-schema/form/templates/FieldTemplate.tsx index 8399b4713..eaaefc48e 100644 --- a/src/lib/components/json-schema/form/templates/FieldTemplate.tsx +++ b/src/lib/components/json-schema/form/templates/FieldTemplate.tsx @@ -1,11 +1,23 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { FormControl } from "@chakra-ui/react"; -import type { FieldTemplateProps } from "@rjsf/utils"; +import type { + FieldTemplateProps, + FormContextType, + RJSFSchema, + StrictRJSFSchema, +} from "@rjsf/utils"; import { getTemplate, getUiOptions } from "@rjsf/utils"; -export default function FieldTemplate( - props: FieldTemplateProps -) { +/** The `FieldTemplate` component is the template used by `SchemaField` to render any field. It renders the field + * content, (label, description, children, errors and help) inside of a `WrapIfAdditional` component. + * + * @param props - The `FieldTemplateProps` for this component + */ +export default function FieldTemplate< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: FieldTemplateProps) { const { id, children, @@ -24,16 +36,17 @@ export default function FieldTemplate( schema, uiSchema, } = props; - const uiOptions = getUiOptions(uiSchema); + const uiOptions = getUiOptions(uiSchema); const WrapIfAdditionalTemplate = getTemplate< "WrapIfAdditionalTemplate", T, + S, F >("WrapIfAdditionalTemplate", registry, uiOptions); - - return hidden ? ( -
{children}
- ) : ( + if (hidden) { + return
{children}
; + } + return ( ( - props: ObjectFieldTemplateProps -) => { +/** The `ObjectFieldTemplate` is the template to use to render all the inner properties of an object along with the + * title and description if available. If the object is expandable, then an `AddButton` is also rendered after all + * the properties. + * + * @param props - The `ObjectFieldTemplateProps` for this component + */ +export default function ObjectFieldTemplate< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: ObjectFieldTemplateProps) { const { description, - properties, disabled, - readonly, - uiSchema, - idSchema, - schema, formData, + idSchema, onAddClick, + properties, + readonly, registry, + schema, + uiSchema, } = props; - const uiOptions = getUiOptions(uiSchema); - const DescriptionFieldTemplate = getTemplate<"DescriptionFieldTemplate">( + const options = getUiOptions(uiSchema); + const DescriptionFieldTemplate = getTemplate< "DescriptionFieldTemplate", - registry, - uiOptions - ); + T, + S, + F + >("DescriptionFieldTemplate", registry, options); // Button templates are not overridden in the uiSchema const { ButtonTemplates: { AddButton }, } = registry.templates; - return ( - <> - {(uiOptions.description || description) && ( +
+ {description && ( (idSchema)} + description={description} + schema={schema} + uiSchema={uiSchema} registry={registry} /> )} @@ -42,7 +63,11 @@ const ObjectFieldTemplate = ( {properties.length > 0 ? ( properties.map((element, index) => element.hidden ? ( - element.content + + {element.content} + ) : ( ( object with no properties )} - {canExpand(schema, uiSchema, formData) && ( + {canExpand(schema, uiSchema, formData) && ( )} - +
); -}; - -export default ObjectFieldTemplate; +} diff --git a/src/lib/components/json-schema/form/templates/TitleFieldTemplate.tsx b/src/lib/components/json-schema/form/templates/TitleFieldTemplate.tsx index 7b033586e..4d0d6e409 100644 --- a/src/lib/components/json-schema/form/templates/TitleFieldTemplate.tsx +++ b/src/lib/components/json-schema/form/templates/TitleFieldTemplate.tsx @@ -1,13 +1,21 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Text } from "@chakra-ui/react"; -import type { TitleFieldProps } from "@rjsf/utils"; +import type { + FormContextType, + RJSFSchema, + StrictRJSFSchema, + TitleFieldProps, +} from "@rjsf/utils"; + /** The `TitleField` is the template to use to render the title of a field * * @param props - The `TitleFieldProps` for this component */ -export default function TitleFieldTemplate( - props: TitleFieldProps -) { +export default function TitleField< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: TitleFieldProps) { const { id, title } = props; return ( diff --git a/src/lib/components/json-schema/form/templates/button-templates/AddButton.tsx b/src/lib/components/json-schema/form/templates/button-templates/AddButton.tsx index a6107dc6b..cef8ffb24 100644 --- a/src/lib/components/json-schema/form/templates/button-templates/AddButton.tsx +++ b/src/lib/components/json-schema/form/templates/button-templates/AddButton.tsx @@ -1,13 +1,24 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Button } from "@chakra-ui/react"; -import type { IconButtonProps } from "@rjsf/utils"; +import { TranslatableString } from "@rjsf/utils"; +import type { + FormContextType, + IconButtonProps, + RJSFSchema, + StrictRJSFSchema, +} from "@rjsf/utils"; -export default function AddButton({ - uiSchema, - ...props -}: IconButtonProps) { +export default function AddButton< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: IconButtonProps) { + const { + registry: { translateString }, + } = props; return (