diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 9b92f77ec6..1cf086e77a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -27,6 +27,7 @@ body: - core - daisyui - fluentui-rc + - mantine - mui - react-bootstrap - semantic-ui diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 603f640fe9..56c5c4a477 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -21,6 +21,7 @@ body: - core - daisyui - fluentui-rc + - mantine - mui - react-bootstrap - semantic-ui diff --git a/.github/ISSUE_TEMPLATE/question_issue.yml b/.github/ISSUE_TEMPLATE/question_issue.yml index e88aa52ba7..81f9a00b45 100644 --- a/.github/ISSUE_TEMPLATE/question_issue.yml +++ b/.github/ISSUE_TEMPLATE/question_issue.yml @@ -21,6 +21,7 @@ body: - core - daisyui - fluentui-rc + - mantine - mui - react-bootstrap - semantic-ui diff --git a/CHANGELOG.md b/CHANGELOG.md index 29bc735bc1..b487d92b03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ should change the heading of the (upcoming) version to include a major version b --> # 6.0.0-beta.14 +## @rjsf/mantine + +- Added new theme! + ## @rjsf/core - Added support for dynamic UI schema in array fields - the `items` property in `uiSchema` can now accept a function that returns a UI schema based on the array item's data, index, and form context ([#4706](https://github.com/rjsf-team/react-jsonschema-form/pull/4706)) diff --git a/README.md b/README.md index 608e460ace..438f0a14ef 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ - [Chakra UI v3](https://github.com/rjsf-team/react-jsonschema-form/tree/main/packages/chakra-ui) - [Daisy UI v5](https://github.com/rjsf-team/react-jsonschema-form/tree/main/packages/daisyui) - [Fluent UI v9](https://github.com/rjsf-team/react-jsonschema-form/tree/main/packages/fluentui-rc) +- [Mantine](https://github.com/rjsf-team/react-jsonschema-form/tree/main/packages/mantine) - [Material UI v7](https://github.com/rjsf-team/react-jsonschema-form/tree/main/packages/mui) - [React-Bootstrap (Bootstrap v5)](https://github.com/rjsf-team/react-jsonschema-form/tree/main/packages/react-bootstrap) - [Semantic UI v2](https://github.com/rjsf-team/react-jsonschema-form/tree/main/packages/semantic-ui) diff --git a/package-lock.json b/package-lock.json index 1290cc8e29..de5c59bd21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "packages/daisyui", "packages/docs", "packages/fluentui-rc", + "packages/mantine", "packages/mui", "packages/playground", "packages/primereact", @@ -5460,6 +5461,21 @@ "@floating-ui/utils": "^0.2.9" } }, + "node_modules/@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@floating-ui/react-dom": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", @@ -7897,6 +7913,62 @@ "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", "license": "MIT" }, + "node_modules/@mantine/core": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.2.1.tgz", + "integrity": "sha512-KxvydotyFRdrRbqULUX2G35/GddPFju9XQUv/vdDWu1ytIWZViTguc+WSj1aBd0DtfRrSaofU5ezZISEXVrPBA==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.26.28", + "clsx": "^2.1.1", + "react-number-format": "^5.4.3", + "react-remove-scroll": "^2.6.2", + "react-textarea-autosize": "8.5.9", + "type-fest": "^4.27.0" + }, + "peerDependencies": { + "@mantine/hooks": "8.2.1", + "react": "^18.x || ^19.x", + "react-dom": "^18.x || ^19.x" + } + }, + "node_modules/@mantine/core/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mantine/dates": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-8.2.1.tgz", + "integrity": "sha512-xNeI+Jw7p9UYEsLbg+QKny4NZ1O3bL6rlrtJKGqOm3HQoATpbRTrdunmY2sIOYXcPEasSCe+y2Ye0fORUcMUEA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1" + }, + "peerDependencies": { + "@mantine/core": "8.2.1", + "@mantine/hooks": "8.2.1", + "dayjs": ">=1.0.0", + "react": "^18.x || ^19.x", + "react-dom": "^18.x || ^19.x" + } + }, + "node_modules/@mantine/hooks": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.2.1.tgz", + "integrity": "sha512-gnRDk5FXCD9fa0AjlAj9otCsZL9QJzVrpYZk9KjOEoP5XR1TEE2F9/rGbajh1UVjPnD3jUlNLRJMH0YHTlA65A==", + "license": "MIT", + "peerDependencies": { + "react": "^18.x || ^19.x" + } + }, "node_modules/@mdx-js/mdx": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.0.tgz", @@ -9958,6 +10030,10 @@ "resolved": "packages/fluentui-rc", "link": true }, + "node_modules/@rjsf/mantine": { + "resolved": "packages/mantine", + "link": true + }, "node_modules/@rjsf/mui": { "resolved": "packages/mui", "link": true @@ -29628,55 +29704,6 @@ "postcss": "^8.4" } }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-load-config/node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/postcss-loader": { "version": "7.3.4", "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", @@ -32023,6 +32050,16 @@ "webpack": ">=4.41.1 || 5.x" } }, + "node_modules/react-number-format": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.4.tgz", + "integrity": "sha512-wOmoNZoOpvMminhifQYiYSTCLUDOiUbBunrMrMjA+dV52sY+vck1S4UhR6PkgnoCquvvMSeJjErXZ4qSaWCliA==", + "license": "MIT", + "peerDependencies": { + "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-popper": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", @@ -32249,6 +32286,23 @@ "react": "^18.3.1" } }, + "node_modules/react-textarea-autosize": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz", + "integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "use-composed-ref": "^1.3.0", + "use-latest": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-transform-catch-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/react-transform-catch-errors/-/react-transform-catch-errors-1.0.2.tgz", @@ -35044,6 +35098,12 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, "node_modules/tabster": { "version": "8.5.5", "resolved": "https://registry.npmjs.org/tabster/-/tabster-8.5.5.tgz", @@ -36488,6 +36548,20 @@ } } }, + "node_modules/use-composed-ref": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz", + "integrity": "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-disposable": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/use-disposable/-/use-disposable-1.0.4.tgz", @@ -36515,6 +36589,23 @@ } } }, + "node_modules/use-latest": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz", + "integrity": "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==", + "license": "MIT", + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sidecar": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", @@ -38055,6 +38146,60 @@ "react": ">=18" } }, + "packages/mantine": { + "name": "@rjsf/mantine", + "version": "6.0.0-beta.1", + "license": "Apache-2.0", + "devDependencies": { + "@mantine/core": ">=8", + "@mantine/dates": ">=8", + "@mantine/hooks": ">=8", + "@restart/hooks": "^0.6.2", + "@restart/ui": "^1.9.4", + "@rjsf/core": "^6.0.0-beta.1", + "@rjsf/snapshot-tests": "^6.0.0-beta.1", + "@rjsf/utils": "^6.0.0-beta.1", + "@rjsf/validator-ajv8": "^6.0.0-beta.1", + "ajv": "^8.17.1", + "ajv-keywords": "^5.1.0", + "eslint": "^8.56.0", + "uncontrollable": "^9.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@mantine/core": ">=8", + "@mantine/dates": ">=8", + "@mantine/hooks": ">=8", + "@rjsf/core": "^6.0.0-beta", + "@rjsf/utils": "^6.0.0-beta", + "react": ">=18" + } + }, + "packages/mantine/node_modules/@restart/hooks": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.6.2.tgz", + "integrity": "sha512-0QzYBe1raty/ULcuyTk0ZdLf+e//4n/f39hDPt5JYZW5kSbHEAgnZhLpWErCKEYSzi14oVBSbNsllnJmWlloBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "packages/mantine/node_modules/uncontrollable": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-9.0.0.tgz", + "integrity": "sha512-wPScOFmvRkHdvebf7qNPn9Wog9B1TL8Dqcx0Vecz6HYTKFKMfGKP6MJHTjiKL8kwd8KTW3YfIUke7pmRDtkcRQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": ">=16.14.0" + } + }, "packages/mui": { "name": "@rjsf/mui", "version": "6.0.0-beta.13", @@ -38093,6 +38238,9 @@ "@babel/runtime": "^7.23.9", "@chakra-ui/react": "^3.19.1", "@emotion/react": "^11.14.0", + "@mantine/core": "^8.1.3", + "@mantine/dates": "^8.1.3", + "@mantine/hooks": "^8.1.3", "@mui/material": "^7.1.0", "@rjsf/antd": "^6.0.0-beta.13", "@rjsf/chakra-ui": "^6.0.0-beta.13", @@ -38328,6 +38476,55 @@ "node": ">=14.0.0" } }, + "packages/shadcn/node_modules/tailwindcss/node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "packages/shadcn/node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "packages/snapshot-tests": { "name": "@rjsf/snapshot-tests", "version": "6.0.0-beta.13", diff --git a/package.json b/package.json index 9cdb49a9e8..931d3a925f 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "packages/daisyui", "packages/docs", "packages/fluentui-rc", + "packages/mantine", "packages/mui", "packages/playground", "packages/primereact", diff --git a/packages/docs/docs/migration-guides/v6.x upgrade guide.md b/packages/docs/docs/migration-guides/v6.x upgrade guide.md index 92d0b5240c..a2b4592193 100644 --- a/packages/docs/docs/migration-guides/v6.x upgrade guide.md +++ b/packages/docs/docs/migration-guides/v6.x upgrade guide.md @@ -2,11 +2,12 @@ ## New packages -There are 4 new packages added in RJSF version 6: +There are 5 new packages added in RJSF version 6: - `@rjsf/daisyui`: This is new theme based on the `daisyui` toolkit -- `@rjsf/react-bootstrap`: This is rename of the `bootstrap-4` theme with an upgrade to the latest version of `react-bootstrap` +- `@rjsf/mantine`: This is new theme based on the `mantine` toolkit - `@rjsf/primereact`: This is a new theme based on the `PrimeReact` toolkit +- `@rjsf/react-bootstrap`: This is rename of the `bootstrap-4` theme with an upgrade to the latest version of `react-bootstrap` - `@rjsf/shadcn`: This is new theme based on the `shadcn` toolkit ## Breaking changes diff --git a/packages/docs/docs/usage/themes.md b/packages/docs/docs/usage/themes.md index ad9629dfcb..2efd741105 100644 --- a/packages/docs/docs/usage/themes.md +++ b/packages/docs/docs/usage/themes.md @@ -11,8 +11,9 @@ meaning that you must load the Bootstrap stylesheet on the page to view the form | Chakra UI | Published | `@rjsf/chakra-ui` | | Bootstrap 3 (default) | Published | `@rjsf/core` | | fluentui-rc | Published | `@rjsf/fluentui-rc` | +| mantine | Published | `@rjsf/mantine` | | material-ui | Published | `@rjsf/mui` | -| PrimeReact | - | `@rjsf/primereact` | +| PrimeReact | Published | `@rjsf/primereact` | | react-bootstrap | Published | `@rjsf/react-bootstrap` | | Semantic UI | Published | `@rjsf/semantic-ui` | | shadcn | Published | `@rjsf/shadcn` | diff --git a/packages/mantine/.eslintrc b/packages/mantine/.eslintrc new file mode 100644 index 0000000000..726b68b788 --- /dev/null +++ b/packages/mantine/.eslintrc @@ -0,0 +1,4 @@ +{ + "extends": "../../.eslintrc", + "plugins": ["@typescript-eslint", "jsx-a11y", "react", "import"] +} diff --git a/packages/mantine/README.md b/packages/mantine/README.md new file mode 100644 index 0000000000..fec61b5e5f --- /dev/null +++ b/packages/mantine/README.md @@ -0,0 +1,153 @@ +[![Build Status][build-shield]][build-url] +[![npm][npm-shield]][npm-url] +[![npm downloads][npm-dl-shield]][npm-dl-url] +[![Contributors][contributors-shield]][contributors-url] +[![Apache 2.0 License][license-shield]][license-url] + + +
+

+ + Logo + + +

rjsf-mantine

+ +

+ Mantine theme, fields and widgets for react-jsonschema-form. +
+ Explore the docs » +
+
+ View Playground + · + Report Bug + · + Request Feature +

+

+ + + +## Table of Contents + +- [Table of Contents](#table-of-contents) +- [About The Project](#about-the-project) + - [Built With](#built-with) +- [Getting Started](#getting-started) + - [Prerequisites](#prerequisites) + - [Installation](#installation) +- [Usage](#usage) +- [Optional Mantine Theme properties](#optional-mantine-theme-properties) + - [Mantine Widget Optional Properties](#mantine-widget-optional-properties) +- [Roadmap](#roadmap) +- [Contributing](#contributing) +- [Contact](#contact) + + + +## About The Project + +`Mantine` theme, fields and widgets for `react-jsonschema-form`. + +### Built With + +- [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form/) +- [Mantine](https://mantine.dev/) + + + +## Getting Started + +- See the [getting started guide](https://mantine.dev/getting-started/) on Mantine documentation. + +### Prerequisites + +- `@mantine/core >= 8` +- `@mantine/hooks >= 8` +- `@mantine/dates >= 8` +- `dayjs >= 1.8.0` +- `@rjsf/core >= 6.0.0` + +```sh +npm install @mantine/core @mantine/hooks @mantine/dates dayjs @rjsf/core +``` + +### Installation + +```sh +npm install @rjsf/mantine +``` + + + +## Usage + +```javascript +import Form from '@rjsf/mantine'; +``` + +or + +```javascript +import { withTheme } from '@rjsf/core'; +import { Theme as MantineTheme } from '@rjsf/mantine'; + +// Make modifications to the theme with your own fields and widgets + +const Form = withTheme(MantineTheme); +``` + +## Optional Mantine Theme properties + +- To pass additional properties to widgets, see this [guide](https://rjsf-team.github.io/react-jsonschema-form/docs/usage/objects#additional-properties). + +#### Mantine Widget Optional Properties + +- [Mantine props for CheckboxWidget](https://mantine.dev/core/checkbox/?t=props) +- [Mantine props for ColorWidget](https://mantine.dev/core/color-input/?t=props) +- [Mantine props for DateWidget](https://mantine.dev/dates/date-input/?t=props) +- [Mantine props for DateTimeWidget](https://mantine.dev/dates/date-input/?t=props) +- [Mantine props for PasswordWidget](https://mantine.dev/core/password-input/?t=props) +- [Mantine props for RadioWidget](https://mantine.dev/core/radio/?t=props) +- [Mantine props for RangeWidget](https://mantine.dev/core/slider/?t=props) +- [Mantine props for SelectWidget](https://mantine.dev/core/select/?t=props) +- [Mantine props for UpDownWidget](https://mantine.dev/core/number-input/?t=props) +- [Mantine props for TextWidget](https://mantine.dev/core/text-input/?t=props) +- [Mantine props for TextAreaWidget](https://mantine.dev/core/textarea/?t=props) +- [Mantine props for TimeWidget](https://mantine.dev/dates/time-input/?t=props) + + + +## Roadmap + +See the [open issues](https://github.com/rjsf-team/react-jsonschema-form/issues) for a list of proposed features (and known issues). + + + +## Contributing + +Read our [contributors' guide](https://rjsf-team.github.io/react-jsonschema-form/docs/contributing/) to get started. + + + +## Contact + +rjsf team: [https://github.com/orgs/rjsf-team/people](https://github.com/orgs/rjsf-team/people) + +GitHub repository: [https://github.com/rjsf-team/react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form) + + + + +[build-shield]: https://github.com/rjsf-team/react-jsonschema-form/workflows/CI/badge.svg +[build-url]: https://github.com/rjsf-team/react-jsonschema-form/actions +[contributors-shield]: https://img.shields.io/github/contributors/rjsf-team/react-jsonschema-form.svg +[contributors-url]: https://github.com/rjsf-team/react-jsonschema-form/graphs/contributors +[license-shield]: https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square +[license-url]: https://choosealicense.com/licenses/apache-2.0/ +[npm-shield]: https://img.shields.io/npm/v/@rjsf/mantine/latest.svg?style=flat-square +[npm-url]: https://www.npmjs.com/package/@rjsf/mantine +[npm-dl-shield]: https://img.shields.io/npm/dm/@rjsf/mantine.svg?style=flat-square +[npm-dl-url]: https://www.npmjs.com/package/@rjsf/mantine +[product-screenshot]: https://raw.githubusercontent.com/rjsf-team/react-jsonschema-form/59a8206e148474bea854bbb004f624143fbcbac8/packages/mantine/screenshot.png diff --git a/packages/mantine/babel.config.json b/packages/mantine/babel.config.json new file mode 100644 index 0000000000..ac08da0a4a --- /dev/null +++ b/packages/mantine/babel.config.json @@ -0,0 +1,3 @@ +{ + "extends": "../../babel.config.json" +} diff --git a/packages/mantine/jest.config.json b/packages/mantine/jest.config.json new file mode 100644 index 0000000000..4c7e4e5d80 --- /dev/null +++ b/packages/mantine/jest.config.json @@ -0,0 +1,10 @@ +{ + "rootDir": "./", + "snapshotSerializers": ["/test/cleanSnapshotSerializer.ts"], + "verbose": true, + "testEnvironment": "jsdom", + "testEnvironmentOptions": { + "browsers": ["chrome", "firefox", "safari"] + }, + "transformIgnorePatterns": ["/node_modules/(?!nanoid)"] +} diff --git a/packages/mantine/logo.png b/packages/mantine/logo.png new file mode 100644 index 0000000000..1da1cc6141 Binary files /dev/null and b/packages/mantine/logo.png differ diff --git a/packages/mantine/package.json b/packages/mantine/package.json new file mode 100644 index 0000000000..dcda12ebbe --- /dev/null +++ b/packages/mantine/package.json @@ -0,0 +1,112 @@ +{ + "name": "@rjsf/mantine", + "version": "6.0.0-beta.1", + "main": "dist/index.js", + "module": "lib/index.js", + "typings": "lib/index.d.ts", + "type": "module", + "description": "Mantine theme, fields and widgets for react-jsonschema-form", + "exports": { + ".": { + "types": "./lib/index.d.ts", + "require": "./dist/index.js", + "import": "./lib/index.js" + }, + "./lib": { + "types": "./lib/index.d.ts", + "require": "./dist/index.js", + "import": "./lib/index.js" + }, + "./lib/*.js": { + "types": "./lib/*.d.ts", + "require": "./dist/*.js", + "import": "./lib/*.js" + }, + "./dist": { + "types": "./lib/index.d.ts", + "require": "./dist/index.js", + "import": "./lib/index.js" + }, + "./dist/*.js": { + "types": "./lib/*.d.ts", + "require": "./dist/*.js", + "import": "./lib/*.js" + } + }, + "files": [ + "dist", + "lib", + "src" + ], + "engineStrict": false, + "engines": { + "node": ">=20" + }, + "scripts": { + "build:ts": "tsc -b tsconfig.build.json && tsc-alias -p tsconfig.build.json", + "build:cjs": "esbuild ./src/index.ts --bundle --outfile=dist/index.js --sourcemap --packages=external --format=cjs", + "build:esm": "esbuild ./src/index.ts --bundle --outfile=dist/mantine.esm.js --sourcemap --packages=external --format=esm", + "build:umd": "rollup dist/mantine.esm.js --format=umd --file=dist/mantine.umd.js --name=@rjsf/mantine", + "build": "npm run build:ts && npm run build:cjs && npm run build:esm && npm run build:umd", + "cs-check": "prettier -l \"{src,test}/**/*.ts?(x)\"", + "cs-format": "prettier \"{src,test}/**/*.ts?(x)\" --write", + "lint": "eslint src test", + "precommit": "lint-staged", + "test": "jest", + "test:update": "jest --u" + }, + "lint-staged": { + "{src,test}/**/*.ts?(x)": [ + "eslint --fix" + ] + }, + "peerDependencies": { + "@rjsf/core": "^6.0.0-beta", + "@rjsf/utils": "^6.0.0-beta", + "react": ">=18", + "@mantine/core": ">=8", + "@mantine/hooks": ">=8", + "@mantine/dates": ">=8" + }, + "devDependencies": { + "@rjsf/core": "^6.0.0-beta.1", + "@rjsf/snapshot-tests": "^6.0.0-beta.1", + "@rjsf/utils": "^6.0.0-beta.1", + "@rjsf/validator-ajv8": "^6.0.0-beta.1", + "eslint": "^8.56.0", + "@mantine/core": ">=8", + "@mantine/hooks": ">=8", + "@mantine/dates": ">=8", + "@restart/hooks": "^0.6.2", + "@restart/ui": "^1.9.4", + "ajv": "^8.17.1", + "ajv-keywords": "^5.1.0", + "uncontrollable": "^9.0.0" + }, + "publishConfig": { + "access": "public" + }, + "author": "Farhad Zare ", + "contributors": [ + "Heath Chiavettone (): ComponentType> { + return withTheme(generateTheme()); +} + +export default generateForm(); diff --git a/packages/mantine/src/Theme/index.ts b/packages/mantine/src/Theme/index.ts new file mode 100644 index 0000000000..2118ff3b52 --- /dev/null +++ b/packages/mantine/src/Theme/index.ts @@ -0,0 +1,18 @@ +import { ThemeProps } from '@rjsf/core'; +import { FormContextType, RJSFSchema, StrictRJSFSchema } from '@rjsf/utils'; + +import { generateTemplates } from '../templates'; +import { generateWidgets } from '../widgets'; + +export function generateTheme< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(): ThemeProps { + return { + templates: generateTemplates(), + widgets: generateWidgets(), + }; +} + +export default generateTheme(); diff --git a/packages/mantine/src/index.ts b/packages/mantine/src/index.ts new file mode 100644 index 0000000000..7cdb4fbd94 --- /dev/null +++ b/packages/mantine/src/index.ts @@ -0,0 +1,8 @@ +import Form, { generateForm } from './Form'; +import Templates, { generateTemplates } from './templates'; +import Theme, { generateTheme } from './Theme'; +import Widgets, { generateWidgets } from './widgets'; + +export { Form, Templates, Theme, Widgets, generateForm, generateTemplates, generateTheme, generateWidgets }; + +export default Form; diff --git a/packages/mantine/src/templates/ArrayFieldItemTemplate.tsx b/packages/mantine/src/templates/ArrayFieldItemTemplate.tsx new file mode 100644 index 0000000000..dac78a9499 --- /dev/null +++ b/packages/mantine/src/templates/ArrayFieldItemTemplate.tsx @@ -0,0 +1,40 @@ +import { + ArrayFieldItemTemplateType, + FormContextType, + getTemplate, + getUiOptions, + RJSFSchema, + StrictRJSFSchema, +} from '@rjsf/utils'; +import { Box, Flex, Group } from '@mantine/core'; + +/** The `ArrayFieldItemTemplate` component is the template used to render an items of an array. + * + * @param props - The `ArrayFieldItemTemplateType` props for the component + */ +export default function ArrayFieldItemTemplate< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: ArrayFieldItemTemplateType) { + const { buttonsProps, className, hasToolbar, index, uiSchema, registry, children } = props; + const uiOptions = getUiOptions(uiSchema); + const ArrayFieldItemButtonsTemplate = getTemplate<'ArrayFieldItemButtonsTemplate', T, S, F>( + 'ArrayFieldItemButtonsTemplate', + registry, + uiOptions, + ); + + return ( + + + {children} + {hasToolbar && ( + + + + )} + + + ); +} diff --git a/packages/mantine/src/templates/ArrayFieldTemplate.tsx b/packages/mantine/src/templates/ArrayFieldTemplate.tsx new file mode 100644 index 0000000000..cd159b333a --- /dev/null +++ b/packages/mantine/src/templates/ArrayFieldTemplate.tsx @@ -0,0 +1,103 @@ +import { + getTemplate, + getUiOptions, + ArrayFieldTemplateProps, + ArrayFieldItemTemplateType, + buttonId, + FormContextType, + RJSFSchema, + StrictRJSFSchema, +} from '@rjsf/utils'; +import { Fieldset, Box, Group } from '@mantine/core'; + +/** The `ArrayFieldTemplate` component is the template used to render all items in an array. + * + * @param props - The `ArrayFieldItemTemplateType` props for the component + */ +export default function ArrayFieldTemplate< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: ArrayFieldTemplateProps) { + const { + canAdd, + className, + disabled, + idSchema, + items, + onAddClick, + readonly, + required, + schema, + uiSchema, + title, + registry, + } = props; + + const uiOptions = getUiOptions(uiSchema); + const ArrayFieldDescriptionTemplate = getTemplate<'ArrayFieldDescriptionTemplate', T, S, F>( + 'ArrayFieldDescriptionTemplate', + registry, + uiOptions, + ); + const ArrayFieldItemTemplate = getTemplate<'ArrayFieldItemTemplate', T, S, F>( + 'ArrayFieldItemTemplate', + registry, + uiOptions, + ); + const ArrayFieldTitleTemplate = getTemplate<'ArrayFieldTitleTemplate', T, S, F>( + 'ArrayFieldTitleTemplate', + registry, + uiOptions, + ); + // Button templates are not overridden in the uiSchema + const { + ButtonTemplates: { AddButton }, + } = registry.templates; + + const legend = (uiOptions.title || title) && ( + + ); + + return ( +
+ {(uiOptions.description || schema.description) && ( + + )} + + + {items && + items.map(({ key, ...itemProps }: ArrayFieldItemTemplateType) => ( + + ))} + + + {canAdd && ( + + (idSchema, 'add')} + className='rjsf-array-item-add' + disabled={disabled || readonly} + onClick={onAddClick} + uiSchema={uiSchema} + registry={registry} + iconType='md' + /> + + )} +
+ ); +} diff --git a/packages/mantine/src/templates/ArrayFieldTitleTemplate.tsx b/packages/mantine/src/templates/ArrayFieldTitleTemplate.tsx new file mode 100644 index 0000000000..29ea42cc25 --- /dev/null +++ b/packages/mantine/src/templates/ArrayFieldTitleTemplate.tsx @@ -0,0 +1,33 @@ +import { + getUiOptions, + titleId, + ArrayFieldTitleProps, + FormContextType, + RJSFSchema, + StrictRJSFSchema, +} from '@rjsf/utils'; +import { Title } from '@mantine/core'; + +/** The `ArrayFieldTitleTemplate` component renders a `TitleFieldTemplate` with an `id` derived from + * the `idSchema`. + * + * @param props - The `ArrayFieldTitleProps` for the component + */ +export default function ArrayFieldTitleTemplate< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: ArrayFieldTitleProps) { + const { idSchema, title, uiSchema, registry } = props; + + const options = getUiOptions(uiSchema, registry.globalUiOptions); + const { label: displayLabel = true } = options; + if (!title || !displayLabel) { + return null; + } + return ( + (idSchema)} order={4} fw='normal'> + {title} + + ); +} diff --git a/packages/mantine/src/templates/BaseInputTemplate.tsx b/packages/mantine/src/templates/BaseInputTemplate.tsx new file mode 100644 index 0000000000..24e26425d5 --- /dev/null +++ b/packages/mantine/src/templates/BaseInputTemplate.tsx @@ -0,0 +1,134 @@ +import { ChangeEvent, FocusEvent, useCallback } from 'react'; +import { + ariaDescribedByIds, + BaseInputTemplateProps, + examplesId, + getInputProps, + labelValue, + FormContextType, + RJSFSchema, + StrictRJSFSchema, +} from '@rjsf/utils'; +import { TextInput, NumberInput } from '@mantine/core'; + +import { cleanupOptions } from '../utils'; + +/** 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, + schema, + value, + placeholder, + required, + disabled, + readonly, + autofocus, + label, + hideLabel, + onChange, + onChangeOverride, + onBlur, + onFocus, + options, + rawErrors, + children, + } = props; + + const inputProps = getInputProps(schema, type, options, false); + const themeProps = cleanupOptions(options); + + const handleNumberChange = useCallback((value: number | string) => onChange(value), [onChange]); + + const handleChange = useCallback( + (e: ChangeEvent) => { + const handler = onChangeOverride ? onChangeOverride : onChange; + const value = e.target.value === '' ? (options.emptyValue ?? '') : e.target.value; + handler(value); + }, + [onChange, onChangeOverride, options], + ); + + const handleBlur = useCallback( + (e: FocusEvent) => { + onBlur(id, e.target && e.target.value); + }, + [onBlur, id], + ); + + const handleFocus = useCallback( + (e: FocusEvent) => { + onFocus(id, e.target && e.target.value); + }, + [onFocus, id], + ); + + const input = + inputProps.type === 'number' || inputProps.type === 'integer' ? ( + 0 ? rawErrors.join('\n') : undefined} + list={schema.examples ? examplesId(id) : undefined} + {...inputProps} + {...themeProps} + step={typeof inputProps.step === 'number' ? inputProps.step : 1} + type='text' + value={value} + aria-describedby={ariaDescribedByIds(id, !!schema.examples)} + /> + ) : ( + 0 ? rawErrors.join('\n') : undefined} + list={schema.examples ? examplesId(id) : undefined} + {...inputProps} + {...themeProps} + value={value} + aria-describedby={ariaDescribedByIds(id, !!schema.examples)} + /> + ); + + return ( + <> + {input} + {children} + {Array.isArray(schema.examples) && ( + (id)}> + {(schema.examples as string[]) + .concat(schema.default && !schema.examples.includes(schema.default) ? ([schema.default] as string[]) : []) + .map((example) => { + return + )} + + ); +} diff --git a/packages/mantine/src/templates/ButtonTemplates/AddButton.tsx b/packages/mantine/src/templates/ButtonTemplates/AddButton.tsx new file mode 100644 index 0000000000..9f824359c6 --- /dev/null +++ b/packages/mantine/src/templates/ButtonTemplates/AddButton.tsx @@ -0,0 +1,17 @@ +import { FormContextType, IconButtonProps, RJSFSchema, StrictRJSFSchema, TranslatableString } from '@rjsf/utils'; + +import IconButton from './IconButton'; +import { Plus } from '../icons'; + +/** The `AddButton` renders a button that represent the `Add` action on a form + */ +export default function AddButton( + props: IconButtonProps, +) { + const { + registry: { translateString }, + } = props; + return ( + } /> + ); +} diff --git a/packages/mantine/src/templates/ButtonTemplates/IconButton.tsx b/packages/mantine/src/templates/ButtonTemplates/IconButton.tsx new file mode 100644 index 0000000000..600ac2c82f --- /dev/null +++ b/packages/mantine/src/templates/ButtonTemplates/IconButton.tsx @@ -0,0 +1,87 @@ +import { MouseEventHandler } from 'react'; +import { ActionIcon, ActionIconProps } from '@mantine/core'; +import { FormContextType, IconButtonProps, RJSFSchema, StrictRJSFSchema, TranslatableString } from '@rjsf/utils'; + +import { Copy, ChevronDown, ChevronUp, X } from '../icons'; + +export type MantineIconButtonProps< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +> = IconButtonProps & Omit; + +export default function IconButton( + props: MantineIconButtonProps, +) { + const { icon, iconType = 'sm', color, onClick, uiSchema, registry, ...otherProps } = props; + return ( + & MouseEventHandler} + {...otherProps} + > + {icon} + + ); +} + +export function CopyButton( + props: MantineIconButtonProps, +) { + const { + registry: { translateString }, + } = props; + return ( + } /> + ); +} + +export function MoveDownButton( + props: MantineIconButtonProps, +) { + const { + registry: { translateString }, + } = props; + return ( + } + /> + ); +} + +export function MoveUpButton( + props: MantineIconButtonProps, +) { + const { + registry: { translateString }, + } = props; + return ( + } + /> + ); +} + +export function RemoveButton( + props: MantineIconButtonProps, +) { + const { + registry: { translateString }, + } = props; + return ( + } + /> + ); +} diff --git a/packages/mantine/src/templates/ButtonTemplates/SubmitButton.tsx b/packages/mantine/src/templates/ButtonTemplates/SubmitButton.tsx new file mode 100644 index 0000000000..a36a69b1f5 --- /dev/null +++ b/packages/mantine/src/templates/ButtonTemplates/SubmitButton.tsx @@ -0,0 +1,20 @@ +import { Button } from '@mantine/core'; +import { getSubmitButtonOptions, FormContextType, RJSFSchema, StrictRJSFSchema, SubmitButtonProps } from '@rjsf/utils'; + +/** The `SubmitButton` renders a button that represent the `Submit` action on a form + */ +export default function SubmitButton< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>({ uiSchema }: SubmitButtonProps) { + const { submitText, norender, props: submitButtonProps = {} } = getSubmitButtonOptions(uiSchema); + if (norender) { + return null; + } + return ( + + ); +} diff --git a/packages/mantine/src/templates/ButtonTemplates/index.ts b/packages/mantine/src/templates/ButtonTemplates/index.ts new file mode 100644 index 0000000000..9f1c389a31 --- /dev/null +++ b/packages/mantine/src/templates/ButtonTemplates/index.ts @@ -0,0 +1,21 @@ +import { FormContextType, RJSFSchema, StrictRJSFSchema, TemplatesType } from '@rjsf/utils'; +import SubmitButton from './SubmitButton'; +import AddButton from './AddButton'; +import { CopyButton, MoveDownButton, MoveUpButton, RemoveButton } from './IconButton'; + +function buttonTemplates< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(): TemplatesType['ButtonTemplates'] { + return { + SubmitButton, + AddButton, + CopyButton, + MoveDownButton, + MoveUpButton, + RemoveButton, + }; +} + +export default buttonTemplates; diff --git a/packages/mantine/src/templates/DescriptionField.tsx b/packages/mantine/src/templates/DescriptionField.tsx new file mode 100644 index 0000000000..12875aad6c --- /dev/null +++ b/packages/mantine/src/templates/DescriptionField.tsx @@ -0,0 +1,24 @@ +import { DescriptionFieldProps, FormContextType, RJSFSchema, StrictRJSFSchema } from '@rjsf/utils'; +import { RichDescription } from '@rjsf/core'; +import { Text } from '@mantine/core'; + +/** The `DescriptionField` is the template to use to render the description of a field + * + * @param props - The `DescriptionFieldProps` for this component + */ +export default function DescriptionField< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: DescriptionFieldProps) { + const { id, description, registry, uiSchema } = props; + if (description) { + return ( + + + + ); + } + + return null; +} diff --git a/packages/mantine/src/templates/ErrorList.tsx b/packages/mantine/src/templates/ErrorList.tsx new file mode 100644 index 0000000000..507396c547 --- /dev/null +++ b/packages/mantine/src/templates/ErrorList.tsx @@ -0,0 +1,36 @@ +import { ErrorListProps, FormContextType, RJSFSchema, StrictRJSFSchema, TranslatableString } from '@rjsf/utils'; +import { Alert, Title, List } from '@mantine/core'; + +import { ExclamationCircle } from './icons'; + +/** The `ErrorList` component is the template that renders the all the errors associated with the fields in the `Form` + * + * @param props - The `ErrorListProps` for this component + */ +export default function ErrorList({ + errors, + registry, +}: ErrorListProps) { + const { translateString } = registry; + + return ( + + {translateString(TranslatableString.ErrorsLabel)} + + } + icon={} + > + + {errors.map((error, index) => ( + + {error.stack} + + ))} + + + ); +} diff --git a/packages/mantine/src/templates/FieldErrorTemplate.tsx b/packages/mantine/src/templates/FieldErrorTemplate.tsx new file mode 100644 index 0000000000..fde5ae2023 --- /dev/null +++ b/packages/mantine/src/templates/FieldErrorTemplate.tsx @@ -0,0 +1,27 @@ +import { errorId, FieldErrorProps, FormContextType, RJSFSchema, StrictRJSFSchema } from '@rjsf/utils'; +import { Box, List } from '@mantine/core'; + +/** The `FieldErrorTemplate` component renders the errors local to the particular field + * + * @param props - The `FieldErrorProps` for the errors being rendered + */ +export default function FieldErrorTemplate< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>({ errors, idSchema }: FieldErrorProps) { + if (!errors || !errors.length) { + return null; + } + // In mantine, errors are handled directly in each component, so there is no need to render a separate error template. + const id = errorId(idSchema); + return ( + + + {errors.map((error, index) => ( + {error} + ))} + + + ); +} diff --git a/packages/mantine/src/templates/FieldHelpTemplate.tsx b/packages/mantine/src/templates/FieldHelpTemplate.tsx new file mode 100644 index 0000000000..a2f249ee6c --- /dev/null +++ b/packages/mantine/src/templates/FieldHelpTemplate.tsx @@ -0,0 +1,22 @@ +import { helpId, FieldHelpProps, FormContextType, RJSFSchema, StrictRJSFSchema } from '@rjsf/utils'; +import { Text } from '@mantine/core'; + +/** The `FieldHelpTemplate` component renders any help desired for a field + * + * @param props - The `FieldHelpProps` to be rendered + */ +export default function FieldHelpTemplate< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: FieldHelpProps) { + const { idSchema, help } = props; + + const id = helpId(idSchema); + + return !help ? null : ( + + {help} + + ); +} diff --git a/packages/mantine/src/templates/FieldTemplate.tsx b/packages/mantine/src/templates/FieldTemplate.tsx new file mode 100644 index 0000000000..ac86ffa8de --- /dev/null +++ b/packages/mantine/src/templates/FieldTemplate.tsx @@ -0,0 +1,66 @@ +import { Box } from '@mantine/core'; +import { + FieldTemplateProps, + FormContextType, + RJSFSchema, + StrictRJSFSchema, + getTemplate, + getUiOptions, +} from '@rjsf/utils'; + +/** 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 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, + classNames, + style, + label, + errors, + help, + displayLabel, + description, + rawDescription, + hidden, + schema, + uiSchema, + registry, + children, + ...otherProps + } = props; + + const uiOptions = getUiOptions(uiSchema); + const WrapIfAdditionalTemplate = getTemplate<'WrapIfAdditionalTemplate', T, S, F>( + 'WrapIfAdditionalTemplate', + registry, + uiOptions, + ); + + if (hidden) { + return {children}; + } + + return ( + + {children} + {errors} + {help} + + ); +} diff --git a/packages/mantine/src/templates/GridTemplate.tsx b/packages/mantine/src/templates/GridTemplate.tsx new file mode 100644 index 0000000000..1528ef79fd --- /dev/null +++ b/packages/mantine/src/templates/GridTemplate.tsx @@ -0,0 +1,30 @@ +import { Container, Grid } from '@mantine/core'; +import { GridTemplateProps } from '@rjsf/utils'; + +/** Renders a `GridTemplate` for mantine, which is expecting the column sizing information coming in via the + * extra props provided by the caller, which are spread directly on the `Grid`/`Grid.Col`. + * + * @param props - The GridTemplateProps, including the extra props containing the Mantine grid positioning details + */ +export default function GridTemplate(props: GridTemplateProps) { + const { children, column, fluid = true, ...rest } = props; + + if (column) { + return {children}; + } + + // Grid with fluid container + if (fluid) { + return ( + + {children} + + ); + } + // Grid without container + return ( + + {children} + + ); +} diff --git a/packages/mantine/src/templates/MultiSchemaFieldTemplate.tsx b/packages/mantine/src/templates/MultiSchemaFieldTemplate.tsx new file mode 100644 index 0000000000..7a763fba29 --- /dev/null +++ b/packages/mantine/src/templates/MultiSchemaFieldTemplate.tsx @@ -0,0 +1,15 @@ +import { Stack } from '@mantine/core'; +import { FormContextType, MultiSchemaFieldTemplateProps, RJSFSchema, StrictRJSFSchema } from '@rjsf/utils'; + +export default function MultiSchemaFieldTemplate< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>({ selector, optionSchemaField }: MultiSchemaFieldTemplateProps) { + return ( + + {selector} + {optionSchemaField} + + ); +} diff --git a/packages/mantine/src/templates/ObjectFieldTemplate.tsx b/packages/mantine/src/templates/ObjectFieldTemplate.tsx new file mode 100644 index 0000000000..08f8db9f31 --- /dev/null +++ b/packages/mantine/src/templates/ObjectFieldTemplate.tsx @@ -0,0 +1,104 @@ +import { Box, Container, Group, MantineSpacing, SimpleGrid } from '@mantine/core'; +import { + buttonId, + canExpand, + descriptionId, + FormContextType, + getTemplate, + getUiOptions, + ObjectFieldTemplatePropertyType, + ObjectFieldTemplateProps, + RJSFSchema, + StrictRJSFSchema, + titleId, +} from '@rjsf/utils'; + +/** 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 { + title, + description, + disabled, + properties, + onAddClick, + readonly, + required, + schema, + uiSchema, + idSchema, + formData, + registry, + } = props; + const uiOptions = getUiOptions(uiSchema); + const TitleFieldTemplate = getTemplate<'TitleFieldTemplate', T, S, F>('TitleFieldTemplate', registry, uiOptions); + const DescriptionFieldTemplate = getTemplate<'DescriptionFieldTemplate', T, S, F>( + 'DescriptionFieldTemplate', + registry, + uiOptions, + ); + // Button templates are not overridden in the uiSchema + const { + ButtonTemplates: { AddButton }, + } = registry.templates; + const gridCols = (typeof uiOptions?.gridCols === 'number' && uiOptions?.gridCols) || undefined; + const gridSpacing = uiOptions?.gridSpacing; + const gridVerticalSpacing = uiOptions?.gridVerticalSpacing; + + return ( + + {title && ( + (idSchema)} + title={title} + required={required} + schema={schema} + uiSchema={uiSchema} + registry={registry} + /> + )} + {description && ( + (idSchema)} + description={description} + schema={schema} + uiSchema={uiSchema} + registry={registry} + /> + )} + + {properties + .filter((e) => !e.hidden) + .map((element: ObjectFieldTemplatePropertyType) => ( + {element.content} + ))} + + + {canExpand(schema, uiSchema, formData) && ( + + (idSchema, 'add')} + disabled={disabled || readonly} + onClick={onAddClick(schema)} + className='rjsf-object-property-expand' + uiSchema={uiSchema} + registry={registry} + /> + + )} + + ); +} diff --git a/packages/mantine/src/templates/TitleField.tsx b/packages/mantine/src/templates/TitleField.tsx new file mode 100644 index 0000000000..0b7b9fb9c7 --- /dev/null +++ b/packages/mantine/src/templates/TitleField.tsx @@ -0,0 +1,17 @@ +import { FormContextType, TitleFieldProps, RJSFSchema, StrictRJSFSchema } from '@rjsf/utils'; +import { Title } from '@mantine/core'; + +/** The `TitleField` is the template to use to render the title of a field + * + * @param props - The `TitleFieldProps` for this component + */ +export default function TitleField( + props: TitleFieldProps, +) { + const { id, title } = props; + return title ? ( + + {title} + + ) : null; +} diff --git a/packages/mantine/src/templates/WrapIfAdditionalTemplate.tsx b/packages/mantine/src/templates/WrapIfAdditionalTemplate.tsx new file mode 100644 index 0000000000..b69ef7ebf9 --- /dev/null +++ b/packages/mantine/src/templates/WrapIfAdditionalTemplate.tsx @@ -0,0 +1,99 @@ +import { FocusEvent, useCallback } from 'react'; +import { + ADDITIONAL_PROPERTY_FLAG, + UI_OPTIONS_KEY, + buttonId, + FormContextType, + RJSFSchema, + StrictRJSFSchema, + TranslatableString, + WrapIfAdditionalTemplateProps, +} from '@rjsf/utils'; +import { Flex, Grid, TextInput } from '@mantine/core'; + +/** The `WrapIfAdditional` component is used by the `FieldTemplate` to rename, or remove properties that are + * part of an `additionalProperties` part of a schema. + * + * @param props - The `WrapIfAdditionalProps` for this component + */ +export default function WrapIfAdditionalTemplate< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: WrapIfAdditionalTemplateProps) { + const { + id, + classNames, + style, + label, + required, + readonly, + disabled, + schema, + uiSchema, + onKeyChange, + onDropPropertyClick, + registry, + children, + } = props; + const { templates, translateString } = registry; + // Button templates are not overridden in the uiSchema + const { RemoveButton } = templates.ButtonTemplates; + const keyLabel = translateString(TranslatableString.KeyLabel, [label]); + const additional = ADDITIONAL_PROPERTY_FLAG in schema; + + const handleBlur = useCallback( + ({ target }: FocusEvent) => onKeyChange(target && target.value), + [onKeyChange], + ); + + if (!additional) { + return ( +
+ {children} +
+ ); + } + + // The `block` prop is not part of the `IconButtonProps` defined in the template, so put it into the uiSchema instead + const uiOptions = uiSchema ? uiSchema[UI_OPTIONS_KEY] : {}; + const buttonUiOptions = { + ...uiSchema, + [UI_OPTIONS_KEY]: { ...uiOptions, block: true }, + }; + + return ( +
+ + + +
+ +
+
+ + {children} + +
+ (id, 'remove')} + iconType='sm' + className='rjsf-array-item-remove' + disabled={disabled || readonly} + onClick={onDropPropertyClick(label)} + uiSchema={buttonUiOptions} + registry={registry} + /> +
+
+ ); +} diff --git a/packages/mantine/src/templates/icons.tsx b/packages/mantine/src/templates/icons.tsx new file mode 100644 index 0000000000..092c9a7392 --- /dev/null +++ b/packages/mantine/src/templates/icons.tsx @@ -0,0 +1,141 @@ +import { ComponentPropsWithoutRef } from 'react'; + +interface IconProps extends ComponentPropsWithoutRef<'svg'> { + size?: number | string; +} + +export function Plus({ size, style, ...others }: IconProps) { + return ( + + + + + + ); +} + +export function Copy({ size, style, ...others }: IconProps) { + return ( + + + + + + ); +} + +export function ChevronDown({ size, style, ...others }: IconProps) { + return ( + + + + + ); +} + +export function ChevronUp({ size, style, ...others }: IconProps) { + return ( + + + + + ); +} + +export function X({ size, style, ...others }: IconProps) { + return ( + + + + + + ); +} + +export function ExclamationCircle({ size, style, ...others }: IconProps) { + return ( + + + + + + ); +} diff --git a/packages/mantine/src/templates/index.ts b/packages/mantine/src/templates/index.ts new file mode 100644 index 0000000000..2208bcb7f2 --- /dev/null +++ b/packages/mantine/src/templates/index.ts @@ -0,0 +1,43 @@ +import { FormContextType, RJSFSchema, StrictRJSFSchema, TemplatesType } from '@rjsf/utils'; + +import ArrayFieldItemTemplate from './ArrayFieldItemTemplate'; +import ArrayFieldTemplate from './ArrayFieldTemplate'; +import ArrayFieldTitleTemplate from './ArrayFieldTitleTemplate'; +import BaseInputTemplate from './BaseInputTemplate'; +import DescriptionField from './DescriptionField'; +import ErrorList from './ErrorList'; +import ButtonTemplates from './ButtonTemplates'; +import FieldErrorTemplate from './FieldErrorTemplate'; +import FieldTemplate from './FieldTemplate'; +import FieldHelpTemplate from './FieldHelpTemplate'; +import GridTemplate from './GridTemplate'; +import ObjectFieldTemplate from './ObjectFieldTemplate'; +import TitleField from './TitleField'; +import WrapIfAdditionalTemplate from './WrapIfAdditionalTemplate'; +import MultiSchemaFieldTemplate from './MultiSchemaFieldTemplate'; + +export function generateTemplates< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(): Partial> { + return { + ArrayFieldItemTemplate, + ArrayFieldTemplate, + ArrayFieldTitleTemplate, + BaseInputTemplate, + ButtonTemplates: ButtonTemplates(), + DescriptionFieldTemplate: DescriptionField, + ErrorListTemplate: ErrorList, + FieldErrorTemplate, + FieldTemplate, + FieldHelpTemplate, + GridTemplate, + ObjectFieldTemplate, + TitleFieldTemplate: TitleField, + WrapIfAdditionalTemplate, + MultiSchemaFieldTemplate, + }; +} + +export default generateTemplates(); diff --git a/packages/mantine/src/tsconfig.json b/packages/mantine/src/tsconfig.json new file mode 100644 index 0000000000..01834bf1ab --- /dev/null +++ b/packages/mantine/src/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": [ + "./" + ], + "compilerOptions": { + "rootDir": "./", + "outDir": "../lib", + "baseUrl": "../", + "jsx": "react-jsx" + }, + "references": [ + { + "path": "../../core" + }, + { + "path": "../../utils" + }, + { + "path": "../../validator-ajv8" + } + ] +} diff --git a/packages/mantine/src/utils.ts b/packages/mantine/src/utils.ts new file mode 100644 index 0000000000..5190cd21fa --- /dev/null +++ b/packages/mantine/src/utils.ts @@ -0,0 +1,37 @@ +import { UIOptionsType } from '@rjsf/utils'; + +const uiOptionsKeys: Array = [ + 'emptyValue', + 'classNames', + 'title', + 'help', + 'autocomplete', + 'disabled', + 'enumDisabled', + 'hideError', + 'readonly', + 'order', + 'filePreview', + 'inline', + 'inputType', + 'submitButtonOptions', + 'widget', + 'enumNames', + 'addable', + 'copyable', + 'orderable', + 'removable', + 'duplicateKeySuffixSeparator', + 'enumOptions', + 'enableMarkdownInDescription', +]; + +export function cleanupOptions(options: T): Omit { + const result = {} as T; + for (const key in options) { + if (!uiOptionsKeys.includes(key as keyof UIOptionsType)) { + result[key] = options[key]; + } + } + return result as Omit; +} diff --git a/packages/mantine/src/widgets/CheckboxWidget.tsx b/packages/mantine/src/widgets/CheckboxWidget.tsx new file mode 100644 index 0000000000..926e3c0cad --- /dev/null +++ b/packages/mantine/src/widgets/CheckboxWidget.tsx @@ -0,0 +1,110 @@ +import { ReactElement, ChangeEvent, FocusEvent, useCallback } from 'react'; +import { + descriptionId, + getTemplate, + StrictRJSFSchema, + RJSFSchema, + FormContextType, + WidgetProps, + labelValue, + ariaDescribedByIds, +} from '@rjsf/utils'; +import { Checkbox } from '@mantine/core'; + +import { cleanupOptions } from '../utils'; + +/** The `CheckBoxWidget` is a widget for rendering boolean properties. + * It is typically used to represent a boolean. + * + * @param props - The `WidgetProps` for this component + */ +export default function CheckboxWidget< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: WidgetProps): ReactElement { + const { + id, + name, + value = false, + required, + disabled, + readonly, + autofocus, + label, + hideLabel, + schema, + rawErrors, + options, + onChange, + onBlur, + onFocus, + registry, + uiSchema, + } = props; + + const themeProps = cleanupOptions(options); + + const DescriptionFieldTemplate = getTemplate<'DescriptionFieldTemplate', T, S, F>( + 'DescriptionFieldTemplate', + registry, + options, + ); + + const handleCheckboxChange = useCallback( + (e: ChangeEvent) => { + if (!disabled && !readonly && onChange) { + onChange(e.currentTarget.checked); + } + }, + [onChange, disabled, readonly], + ); + + const handleBlur = useCallback( + ({ target }: FocusEvent) => { + if (onBlur) { + onBlur(id, target.checked); + } + }, + [onBlur, id], + ); + + const handleFocus = useCallback( + ({ target }: FocusEvent) => { + if (onFocus) { + onFocus(id, target.checked); + } + }, + [onFocus, id], + ); + + const description = options.description || schema.description; + return ( + <> + {!hideLabel && !!description && ( + (id)} + description={description} + schema={schema} + uiSchema={uiSchema} + registry={registry} + /> + )} + 0 ? rawErrors.join('\n') : undefined} + aria-describedby={ariaDescribedByIds(id)} + {...themeProps} + /> + + ); +} diff --git a/packages/mantine/src/widgets/CheckboxesWidget.tsx b/packages/mantine/src/widgets/CheckboxesWidget.tsx new file mode 100644 index 0000000000..98da224d82 --- /dev/null +++ b/packages/mantine/src/widgets/CheckboxesWidget.tsx @@ -0,0 +1,112 @@ +import { FocusEvent, useCallback } from 'react'; +import { + ariaDescribedByIds, + enumOptionsValueForIndex, + enumOptionsIndexForValue, + optionId, + titleId, + FormContextType, + WidgetProps, + RJSFSchema, + StrictRJSFSchema, +} from '@rjsf/utils'; +import { Checkbox, Flex, Input } from '@mantine/core'; + +import { cleanupOptions } from '../utils'; + +/** The `CheckboxesWidget` is a widget for rendering checkbox groups. + * It is typically used to represent an array of enums. + * + * @param props - The `WidgetProps` for this component + */ +export default function CheckboxesWidget< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: WidgetProps) { + const { + id, + value, + required, + disabled, + readonly, + autofocus, + label, + hideLabel, + rawErrors, + options, + onChange, + onBlur, + onFocus, + } = props; + + const { enumOptions, enumDisabled, inline, emptyValue } = options; + const themeProps = cleanupOptions(options); + + const handleChange = useCallback( + (nextValue: any) => { + if (!disabled && !readonly && onChange) { + onChange(enumOptionsValueForIndex(nextValue, enumOptions, emptyValue)); + } + }, + [onChange, disabled, readonly, enumOptions, emptyValue], + ); + + const handleBlur = useCallback( + ({ target }: FocusEvent) => { + if (onBlur) { + onBlur(id, enumOptionsValueForIndex(target.value, enumOptions, emptyValue)); + } + }, + [onBlur, id, enumOptions, emptyValue], + ); + + const handleFocus = useCallback( + ({ target }: FocusEvent) => { + if (onFocus) { + onFocus(id, enumOptionsValueForIndex(target.value, enumOptions, emptyValue)); + } + }, + [onFocus, id, enumOptions, emptyValue], + ); + + const selectedIndexes = enumOptionsIndexForValue(value, enumOptions, true) as string[]; + + return Array.isArray(enumOptions) && enumOptions.length > 0 ? ( + <> + {!hideLabel && !!label && ( + (id)} required={required}> + {label} + + )} + 0 ? rawErrors.join('\n') : undefined} + aria-describedby={ariaDescribedByIds(id)} + {...themeProps} + > + {Array.isArray(enumOptions) ? ( + + {enumOptions.map((option, i) => ( + + ))} + + ) : null} + + + ) : null; +} diff --git a/packages/mantine/src/widgets/ColorWidget.tsx b/packages/mantine/src/widgets/ColorWidget.tsx new file mode 100644 index 0000000000..bc84962469 --- /dev/null +++ b/packages/mantine/src/widgets/ColorWidget.tsx @@ -0,0 +1,85 @@ +import { FocusEvent, useCallback } from 'react'; +import { + FormContextType, + RJSFSchema, + StrictRJSFSchema, + WidgetProps, + labelValue, + ariaDescribedByIds, +} from '@rjsf/utils'; +import { ColorInput } from '@mantine/core'; + +import { cleanupOptions } from '../utils'; + +/** The `ColorWidget` component uses the `ColorInput` from Mantine, allowing users to pick a color. + * + * @param props - The `WidgetProps` for this component + */ +export default function ColorWidget( + props: WidgetProps, +) { + const { + id, + name, + value, + placeholder, + required, + disabled, + readonly, + autofocus, + label, + hideLabel, + rawErrors, + options, + onChange, + onBlur, + onFocus, + } = props; + + const themeProps = cleanupOptions(options); + + const handleChange = useCallback( + (nextValue: string) => { + onChange(nextValue); + }, + [onChange], + ); + + const handleBlur = useCallback( + ({ target }: FocusEvent) => { + if (onBlur) { + onBlur(id, target && target.value); + } + }, + [onBlur, id], + ); + + const handleFocus = useCallback( + ({ target }: FocusEvent) => { + if (onFocus) { + onFocus(id, target && target.value); + } + }, + [onFocus, id], + ); + + return ( + 0 ? rawErrors.join('\n') : undefined} + {...themeProps} + aria-describedby={ariaDescribedByIds(id)} + popoverProps={{ withinPortal: false }} + /> + ); +} diff --git a/packages/mantine/src/widgets/DateTime/AltDateTimeWidget.tsx b/packages/mantine/src/widgets/DateTime/AltDateTimeWidget.tsx new file mode 100644 index 0000000000..d5ae70f418 --- /dev/null +++ b/packages/mantine/src/widgets/DateTime/AltDateTimeWidget.tsx @@ -0,0 +1,22 @@ +import { FormContextType, RJSFSchema, StrictRJSFSchema, WidgetProps } from '@rjsf/utils'; + +import _AltDateWidget from './AltDateWidget'; + +/** The `AltDateTimeWidget` is an alternative widget for rendering datetime properties. + * It uses the AltDateWidget for rendering, with the `time` prop set to true by default. + * + * @param props - The `WidgetProps` for this component + */ +export default function AltDateTimeWidget< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: WidgetProps) { + const { AltDateWidget } = props.registry.widgets; + return ; +} + +AltDateTimeWidget.defaultProps = { + ..._AltDateWidget?.defaultProps, + showTime: true, +}; diff --git a/packages/mantine/src/widgets/DateTime/AltDateWidget.tsx b/packages/mantine/src/widgets/DateTime/AltDateWidget.tsx new file mode 100644 index 0000000000..0c2e75e43c --- /dev/null +++ b/packages/mantine/src/widgets/DateTime/AltDateWidget.tsx @@ -0,0 +1,138 @@ +import { useCallback, useEffect, useState } from 'react'; +import { + ariaDescribedByIds, + dateRangeOptions, + parseDateString, + toDateString, + getDateElementProps, + titleId, + DateObject, + type DateElementFormat, + FormContextType, + RJSFSchema, + StrictRJSFSchema, + TranslatableString, + WidgetProps, +} from '@rjsf/utils'; +import { Flex, Box, Group, Button, Select, Input } from '@mantine/core'; + +function readyForChange(state: DateObject) { + return Object.values(state).every((value) => value !== -1); +} + +/** The `AltDateWidget` is an alternative widget for rendering date properties. + * @param props - The `WidgetProps` for this component + */ +export default function AltDateWidget< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: WidgetProps) { + const { + id, + value, + required, + disabled, + readonly, + label, + hideLabel, + rawErrors, + options, + onChange, + showTime = false, + registry, + } = props; + + const { translateString } = registry; + + const [lastValue, setLastValue] = useState(value); + const [state, setState] = useState(parseDateString(value, showTime)); + + useEffect(() => { + const stateValue = toDateString(state, showTime); + if (lastValue !== value) { + // We got a new value in the props + setLastValue(value); + setState(parseDateString(value, showTime)); + } else if (readyForChange(state) && stateValue !== value) { + // Selected date is ready to be submitted + onChange(stateValue); + setLastValue(stateValue); + } + }, [showTime, value, onChange, state, lastValue]); + + const handleChange = useCallback((property: keyof DateObject, nextValue: any) => { + setState((prev) => ({ ...prev, [property]: typeof nextValue === 'undefined' ? -1 : nextValue })); + }, []); + + const handleSetNow = useCallback(() => { + if (!disabled && !readonly) { + const nextState = parseDateString(new Date().toJSON(), showTime); + onChange(toDateString(nextState, showTime)); + } + }, [disabled, readonly, showTime, onChange]); + + const handleClear = useCallback(() => { + if (!disabled && !readonly) { + onChange(''); + } + }, [disabled, readonly, onChange]); + + return ( + <> + {!hideLabel && !!label && ( + (id)} required={required}> + {label} + + )} + + {getDateElementProps( + state, + showTime, + options.yearsRange as [number, number] | undefined, + options.format as DateElementFormat | undefined, + ).map((elemProps, i) => { + const elemId = id + '_' + elemProps.type; + return ( + +