├── .babelrc
├── .coveralls.yml
├── .editorconfig
├── .eslintrc
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── playground
├── app
│ ├── App.js
│ ├── conf
│ │ ├── embedded.js
│ │ ├── index.js
│ │ ├── issue.59.js
│ │ └── simpleSum.js
│ └── index.js
├── index.html
└── index.prod.html
├── src
├── actions
│ ├── index.js
│ ├── remove.js
│ ├── require.js
│ ├── uiAppend.js
│ ├── uiOverride.js
│ ├── uiReplace.js
│ └── validateAction.js
├── applyRules.js
├── index.js
├── rulesRunner.js
└── utils.js
├── test
├── .eslintrc
├── actions
│ ├── remove.fromArray.test.js
│ ├── remove.nested.test.js
│ ├── remove.test.js
│ ├── require.nested.test.js
│ ├── require.test.js
│ ├── uiAppend.test.js
│ ├── uiAppend
│ │ ├── nested
│ │ │ ├── rules.json
│ │ │ ├── schema.json
│ │ │ └── uiSchema.json
│ │ └── uiAppend.nested.test.js
│ ├── uiOverride.test.js
│ ├── uiReplace.test.js
│ ├── validateAction.nested.test.js
│ ├── validateAction.test.js
│ ├── validateField.test.js
│ └── validation.test.js
├── applyRules.render.test.js
├── applyRules.test.js
├── applyRules.validation.test.js
├── calculatedField.test.js
├── issues
│ ├── 34.test.js
│ ├── 35.test.js
│ ├── 38.test.js
│ ├── 43.test.js
│ ├── 44.test.js
│ ├── 46.test.js
│ ├── 47.test.js
│ ├── 53.test.js
│ ├── 59.test.js
│ └── 61.test.js
├── rulesRuneer.test.js
├── runRules.normRules.test.js
├── utils.findRelUiSchema.test.js
├── utils.js
└── utils.test.js
├── webpack.config.dist.js
├── webpack.config.js
├── webpack.config.prod.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react", "env"],
3 | "plugins": [
4 | "transform-object-rest-spread",
5 | "transform-class-properties"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | repo_token: apkhWgDS9UbAI8V6ibKL9JmZEsgBzU6nC
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | indent_style = space
2 | indent_size = 2
3 | charset = utf-8
4 | insert_final_newline = true
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "rules": {
4 | "react/jsx-uses-react": 2,
5 | "react/jsx-uses-vars": 2,
6 | "react/react-in-jsx-scope": 2,
7 | "react/jsx-tag-spacing": 0,
8 | "curly": [2],
9 | "linebreak-style": [2, "unix"],
10 | "semi": [2, "always"],
11 | "comma-dangle": [0],
12 | "no-unused-vars": [2, {
13 | "vars": "all",
14 | "args": "none",
15 | "ignoreRestSiblings": true
16 | }],
17 | "no-console": [0],
18 | "object-curly-spacing": [2, "always"],
19 | "keyword-spacing": ["error"],
20 | "jest/no-disabled-tests": "warn",
21 | "jest/no-focused-tests": "error",
22 | "jest/no-identical-title": "error",
23 | "jest/valid-expect": "error"
24 | },
25 | "env": {
26 | "es6": true,
27 | "browser": true,
28 | "node": true,
29 | "jest/globals": true
30 | },
31 | "extends": "eslint:recommended",
32 | "parserOptions": {
33 | "ecmaVersion": 6,
34 | "sourceType": "module",
35 | "ecmaFeatures": {
36 | "jsx": true
37 | }
38 | },
39 | "plugins": [
40 | "react",
41 | "jest"
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | coverage
2 | npm-debug.log
3 | node_modules
4 | build
5 | dist
6 | lib
7 |
8 |
9 | # Numerous always-ignore extensions
10 | *.diff
11 | *.err
12 | *.orig
13 | *.log
14 | *.rej
15 | *.swo
16 | *.swp
17 | *.vi
18 | *~
19 | *.sass-cache
20 |
21 | # OS or Editor folders
22 | .DS_Store
23 | .cache
24 | .project
25 | .settings
26 | .tmproj
27 | nbproject
28 | Thumbs.db
29 |
30 | # NPM packages folder.
31 | node_modules/
32 |
33 | # Brunch output folder.
34 | public/
35 | .idea/
36 | json-schema-playing.iml
37 |
38 | *.iml
39 |
40 | *.tgz
41 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language:
3 | - node_js
4 | node_js:
5 | - "8"
6 | env:
7 | - ACTION="run lint"
8 | - ACTION="run cs-check"
9 | - ACTION="run dist"
10 | - ACTION="test && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js"
11 | script:
12 | - npm $ACTION
13 | - npm test && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/RxNT/react-jsonschema-form-conditionals)
2 | [](https://coveralls.io/github/RxNT/react-jsonschema-form-conditionals?branch=master)
3 | [](https://badge.fury.io/js/react-jsonschema-form-conditionals)
4 | # Form with conditionals
5 |
6 | This project extends [react-jsonschema-form](https://github.com/mozilla-services/react-jsonschema-form) with
7 | conditional logic, which allow to have more complicated logic expressed and controlled with JSON schema.
8 | This is primarily useful for complicated schemas with extended business logic,
9 | which are suspect to changes and need to be manageable and changeable without modifying running application.
10 |
11 | If you need simple rule logic, that does not change a lot, you can use original [mozilla project](https://github.com/mozilla-services/react-jsonschema-form),
12 | by following examples like https://jsfiddle.net/69z2wepo/68259/
13 |
14 | The project is done to be fully compatible with mozilla,
15 | without imposing additional limitations.
16 |
17 | ## Features
18 |
19 | - Support for [Json Rules Engine](https://github.com/CacheControl/json-rules-engine) and [json-rules-engine-simplified](https://github.com/RxNT/json-rules-engine-simplified)
20 | - Extensible action mechanism
21 | - Configuration over coding
22 | - Lightweight and extensible
23 |
24 | ## Installation
25 |
26 | Install `react-jsonschema-form-conditionals` by running:
27 |
28 | ```bash
29 | npm install --s react-jsonschema-form-conditionals
30 | ```
31 |
32 | ## Usage
33 |
34 | The simplest example of using `react-jsonschema-form-conditionals`
35 |
36 | ```jsx
37 | import applyRules from 'react-jsonschema-form-conditionals';
38 | import Engine from 'json-rules-engine-simplified';
39 | import Form from "react-jsonschema-form";
40 |
41 | ...
42 |
43 | const rules = [{
44 | ...
45 | }];
46 |
47 | let FormWithConditionals = applyRules(schema, uiSchema, rules, Engine)(Form);
48 |
49 | ReactDOM.render(
50 | ,
51 | document.querySelector('#app')
52 | );
53 | ```
54 |
55 | To show case uses for this library we'll be using simple registration schema example
56 |
57 | ```jsx
58 |
59 | import applyRules from 'react-jsonschema-form-conditionals';
60 | import Form from "react-jsonschema-form";
61 |
62 | let schema = {
63 | definitions: {
64 | hobby: {
65 | type: "object",
66 | properties: {
67 | name: { type: "string" },
68 | durationInMonth: { "type": "integer" },
69 | }
70 | }
71 | },
72 | title: "A registration form",
73 | description: "A simple form example.",
74 | type: "object",
75 | required: [
76 | "firstName",
77 | "lastName"
78 | ],
79 | properties: {
80 | firstName: {
81 | type: "string",
82 | title: "First name"
83 | },
84 | lastName: {
85 | type: "string",
86 | title: "Last name"
87 | },
88 | age: {
89 | type: "integer",
90 | title: "Age",
91 | },
92 | bio: {
93 | type: "string",
94 | title: "Bio",
95 | },
96 | country: {
97 | type: "string",
98 | title: "Country"
99 | },
100 | state: {
101 | type: "string",
102 | title: "State"
103 | },
104 | zip: {
105 | type: "string",
106 | title: "ZIP"
107 | },
108 | password: {
109 | type: "string",
110 | title: "Password",
111 | minLength: 3
112 | },
113 | telephone: {
114 | type: "string",
115 | title: "Telephone",
116 | minLength: 10
117 | },
118 | hobbies: {
119 | type: "array",
120 | items: { "$ref": "#/definitions/hobby" }
121 | }
122 | }
123 | }
124 |
125 | let rules = [{
126 | ...
127 | }]
128 |
129 | let FormWithConditionals = applyRules(schema, uiSchema, rules, Engine)(Form);
130 |
131 | render((
132 |
133 | ), document.getElementById("app"));
134 | ```
135 |
136 | Conditionals functionality is build using 2 things
137 | - Rules engine ([Json Rules Engine](https://github.com/CacheControl/json-rules-engine) or [Simplified Json Rules Engine](https://github.com/RxNT/json-rules-engine-simplified))
138 | - Schema action mechanism
139 |
140 | Rules engine responsibility is to trigger events, action mechanism
141 | performs needed actions on the requests.
142 |
143 | ## Rules engine
144 |
145 | Project supports 2 rules engines out of the box:
146 | - [Json Rules Engine](https://github.com/CacheControl/json-rules-engine)
147 | - [Simplified Json Rules Engine](https://github.com/RxNT/json-rules-engine-simplified)
148 |
149 | In order to use either of those, you need to specify `Engine` in `applyRules` configuration.
150 |
151 | For example:
152 |
153 | To use [Simplified Json Rules Engine](https://github.com/RxNT/json-rules-engine-simplified), you can do following:
154 | ```js
155 |
156 | import applyRules from 'react-jsonschema-form-conditionals';
157 | import Form from "react-jsonschema-form";
158 |
159 | import Engine from 'json-rules-engine-simplified';
160 |
161 | ...
162 |
163 | let FormWithConditionals = applyRules(schema, uiSchema, rules, Engine)(Form);
164 |
165 | ReactDOM.render(
166 | ,
167 | document.querySelector('#app')
168 | );
169 | ```
170 |
171 | To use [Json Rules Engine](https://github.com/RxNT/json-rules-engine-simplified), is almost the same:
172 |
173 | ```js
174 |
175 | import applyRules from 'react-jsonschema-form-conditionals';
176 | import Engine from 'json-rules-engine';
177 | import Form from "react-jsonschema-form";
178 |
179 | // ...
180 |
181 | let FormWithConditionals = applyRules(schema, uiSchema, rules, Engine)(Form);
182 |
183 | ReactDOM.render(
184 | ,
185 | document.querySelector('#app')
186 | );
187 | ```
188 |
189 | ### Extending rules engine
190 |
191 | If non of the provided engines satisfies, your needs, you can
192 | implement your own `Engine` which should
193 | comply to following:
194 |
195 | ```js
196 | class Engine {
197 | constructor(rules, schema) {
198 | }
199 | addRule = (rule) => {
200 | }
201 | run = (formData) => {
202 | return Promise[Event]
203 | }
204 | }
205 | ```
206 |
207 | Original `rules` and `schema` is used as a parameter for a factory call,
208 | in order to be able to have additional functionality, such as rules to schema compliance validation,
209 | like it's done in Simplified Json Rules Engine](https://github.com/RxNT/json-rules-engine-simplified)
210 |
211 | ## Schema action mechanism
212 |
213 | Rules engine emits events, which are expected to have a `type` and `params` field,
214 | `type` is used to distinguish action that is needed, `params` are used as input for that action:
215 |
216 | ```json
217 | {
218 | "type": "remove",
219 | "params": {
220 | "field": "name"
221 | }
222 | }
223 | ```
224 |
225 | By default action mechanism defines a supported set of rules, which you can extend as needed:
226 |
227 | - `remove` removes a field or set of fields from `schema` and `uiSchema`
228 | - `require` makes a field or set of fields required
229 |
230 | ### Remove action
231 |
232 | If you want to remove a field, your configuration should look like this:
233 |
234 | ```json
235 | {
236 | "conditions": { },
237 | "event": {
238 | "type": "remove",
239 | "params": {
240 | "field": "password"
241 | }
242 | }
243 | }
244 | ```
245 | When `condition` is met, `password` will be removed from both `schema` and `uiSchema`.
246 |
247 | In case you want to remove multiple fields `name`, `password`, rule should look like this:
248 |
249 | ```json
250 | {
251 | "conditions": { },
252 | "event": {
253 | "type": "remove",
254 | "params": {
255 | "field": [ "name", "password" ]
256 | }
257 | }
258 | }
259 | ```
260 |
261 | To remove nested schema properties, use json dot notation. e.g. For schema object:
262 |
263 | ```json
264 | {
265 | "type": "object",
266 | "properties": {
267 | "someParentWrapper": {
268 | "type": "object",
269 | "properties": {
270 | "booleanValA": {
271 | "type": "boolean",
272 | "title": "Some boolean input"
273 | },
274 | "booleanValB": {
275 | "type": "boolean",
276 | "title": "Another boolean input"
277 | }
278 | }
279 | }
280 | }
281 | }
282 |
283 | ```
284 |
285 | You can remove the nested booleanValA or booleanValB like so:
286 |
287 | ```json
288 | {
289 | "conditions": { },
290 | "event": {
291 | "type": "remove",
292 | "params": {
293 | "field": "someParentWrapper.booleanValA"
294 | }
295 | }
296 | }
297 | ```
298 |
299 | ### Require action
300 |
301 | The same convention goes for `require` action
302 |
303 | For a single field:
304 |
305 | ```json
306 | {
307 | "conditions": { },
308 | "event": {
309 | "type": "require",
310 | "params": {
311 | "field": "password"
312 | }
313 | }
314 | }
315 | ```
316 |
317 | For multiple fields:
318 |
319 | ```json
320 | {
321 | "conditions": { },
322 | "event": {
323 | "type": "require",
324 | "params": {
325 | "field": [ "name", "password"]
326 | }
327 | }
328 | }
329 | ```
330 |
331 | ## UiSchema actions
332 |
333 | API defines a set of actions, that you can take on `uiSchema`, they cover most of the
334 |
335 | - `uiAppend` appends `uiSchema` specified in params with an original `uiSchema`
336 | - `uiOverride` replaces field in original `uiSchema` with fields in `params`, keeping unrelated entries
337 | - `uiRepalce` replaces whole `uiSchema` with a conf schema
338 |
339 | To show case, let's take a simple `schema`
340 |
341 | ```json
342 | {
343 | "properties": {
344 | "lastName": { "type": "string" },
345 | "firstName": { "type": "string" },
346 | "nickName": { "type": "string"}
347 | }
348 | }
349 | ```
350 |
351 | and `uiSchema`
352 |
353 | ```json
354 | {
355 | "ui:order": ["firstName"],
356 | "lastName": {
357 | "classNames": "col-md-1",
358 | },
359 | "firstName": {
360 | "ui:disabled": false,
361 | "num": 23
362 | },
363 | "nickName": {
364 | "classNames": "col-md-12"
365 | }
366 | }
367 | ```
368 | with event `params` something like this
369 | ```json
370 | {
371 | "ui:order": [ "lastName" ],
372 | "lastName": {
373 | "classNames": "has-error"
374 | },
375 | "firstName" : {
376 | "classNames": "col-md-6",
377 | "ui:disabled": true,
378 | "num": 22
379 | }
380 | }
381 | ```
382 |
383 | And look at different results depend on the choosen action.
384 |
385 | ### uiAppend
386 |
387 | UiAppend can handle `arrays` and `string`, with fallback to `uiOverride` behavior for all other fields.
388 |
389 | So the expected result `uiSchema` will be:
390 | ```json
391 | {
392 | "ui:order": ["firstName", "lastName"],
393 | "lastName": {
394 | "classNames": "col-md-1 has-error"
395 | },
396 | "firstName": {
397 | "classNames": "col-md-6",
398 | "ui:disabled": true,
399 | "num": 22
400 | },
401 | "nickName": {
402 | "classNames": "col-md-12"
403 | }
404 | }
405 | ```
406 |
407 | In this case it
408 | - added `lastName` to `ui:order` array,
409 | - appended `has-error` to `classNames` in `lastName` field
410 | - added `classNames` and enabled `firstName`
411 | - as for the `num` in `firstName` it just overrode it
412 |
413 | This is useful for example if you want to add some additional markup in your code, without touching layout that you've defined.
414 |
415 | ### uiOverride
416 |
417 | `uiOverride` behaves similar to append, but instead of appending it completely replaces overlapping values
418 |
419 | So the expected result `uiSchema` will be:
420 | ```json
421 | {
422 | "ui:order": [ "lastName" ],
423 | "lastName": {
424 | "classNames": "has-error"
425 | },
426 | "firstName": {
427 | "classNames": "col-md-6",
428 | "ui:disabled": true,
429 | "num": 22
430 | },
431 | "nickName": {
432 | "classNames": "col-md-12"
433 | }
434 | }
435 | ```
436 |
437 | In this case it
438 | - `ui:order` was replaced with configured value
439 | - `className` for the `lastName` was replaced with `has-error`
440 | - added `classNames` and enabled `firstName`
441 | - as for the `num` in `firstName` it just overrode it
442 |
443 | ### uiReplace
444 |
445 | `uiReplace` just replaces all fields in `uiSchema` with `params` fields, leaving unrelated fields untouched.
446 |
447 | So the result `uiSchema` will be
448 | ```json
449 | {
450 | "ui:order": [ "lastName" ],
451 | "lastName": {
452 | "classNames": "has-error"
453 | },
454 | "firstName" : {
455 | "classNames": "col-md-6",
456 | "ui:disabled": true,
457 | "num": 22
458 | },
459 | "nickName": {
460 | "classNames": "col-md-12"
461 | }
462 | }
463 | ```
464 |
465 | ## Extension mechanism
466 |
467 | You can extend existing actions list, by specifying `extraActions` on the form.
468 |
469 | Let's say we need to introduce `replaceClassNames` action, that
470 | would just specify `classNames` `col-md-4` for all fields except for `ignore`d one.
471 | We also want to trigger it only when `password` is `empty`.
472 |
473 | This is how we can do this:
474 |
475 | ```js
476 | import applyRules from 'react-jsonschema-form-conditionals';
477 | import Engine from 'json-rules-engine-simplified';
478 | import Form from "react-jsonschema-form";
479 |
480 | ...
481 |
482 | const rules = [
483 | {
484 | conditons: {
485 | password: "empty"
486 | },
487 | event: {
488 | type: "replaceClassNames",
489 | params: {
490 | classNames: "col-md-4",
491 | ignore: [ "password" ]
492 | }
493 | }
494 | }
495 | ];
496 |
497 |
498 | let extraActions = {
499 | replaceClassNames: function(params, schema, uiSchema, formData) {
500 | Object.keys(schema.properties).forEach((field) => {
501 | if (uiSchema[field] === undefined) {
502 | uiSchema[field] = {}
503 | }
504 | uiSchema[field].classNames = params.classNames;
505 | }
506 | }
507 | };
508 |
509 | let FormWithConditionals = applyRules(schema, uiSchema, rules, Engine, extraActions)(Form);
510 |
511 | ReactDOM.render(
512 | ,
513 | document.querySelector('#app')
514 | );
515 | ```
516 |
517 | Provided snippet does just that.
518 |
519 | ### Extension with calculated values
520 |
521 | In case you need to calculate value, based on other field values, you can also do that.
522 |
523 | Let's say we want to have schema with `a`, `b` and `sum` fields
524 |
525 | ```js
526 | import applyRules from 'react-jsonschema-form-conditionals';
527 | import Engine from 'json-rules-engine-simplified';
528 | import Form from "react-jsonschema-form";
529 |
530 | ...
531 |
532 | const rules = [
533 | {
534 | conditons: {
535 | a: { not: "empty" },
536 | b: { not: "empty" }
537 | },
538 | event: {
539 | type: "updateSum"
540 | }
541 | }
542 | ];
543 |
544 |
545 | let extraActions = {
546 | updateSum: function(params, schema, uiSchema, formData) {
547 | formData.sum = formData.a + formData.b;
548 | }
549 | };
550 |
551 | let FormWithConditionals = applyRules(schema, uiSchema, rules, Engine, extraActions)(Form);
552 |
553 | ReactDOM.render(
554 | ,
555 | document.querySelector('#app')
556 | );
557 | ```
558 |
559 | This is how you can do that.
560 |
561 | > WARNING!!! You need to be careful with a rules order, when using calculated values.
562 | > Put calculation rules at the top of your rules specification.
563 |
564 | For example, let's say you want to mark `sum` field, if you have sum `greater` than `10`. The rule would look something like this:
565 |
566 | ```json
567 | {
568 | "conditions": {
569 | "sum": { "greater" : 10 }
570 | },
571 | "event": {
572 | "type": "appendClass",
573 | "classNames": "has-success"
574 | }
575 | }
576 | ```
577 |
578 | But it will work only if you put it after `updateSum` rule, like this
579 | ```json
580 | [
581 | {
582 | "conditons": {
583 | "a": { "not": "empty" },
584 | "b": { "not": "empty" }
585 | },
586 | "event": {
587 | "type": "updateSum"
588 | }
589 | },
590 | {
591 | "conditions": {
592 | "sum": { "greater" : 10 }
593 | },
594 | "event": {
595 | "type": "appendClass",
596 | "classNames": "has-success"
597 | }
598 | }
599 | ];
600 | ```
601 |
602 | Otherwise it will work with **old `sum` values** and therefor show incorrect value.
603 |
604 | ### Rules order
605 |
606 | Originally actions performed in sequence defined in the array. If you have interdependent rules, that you need to run in order
607 | you can specify `order` on a rule, so that it would be executed first. Rules are executed based on order from lowest to highest with
608 | rules without order executed last.
609 |
610 | For example to make updateSum work regardless the order rules were added, you can do following:
611 | ```json
612 | [
613 | {
614 | "conditions": {
615 | "sum": { "greater" : 10 }
616 | },
617 | "order": 1,
618 | "event": {
619 | "type": "appendClass",
620 | "classNames": "has-success"
621 | }
622 | },
623 | {
624 | "conditons": {
625 | "a": { "not": "empty" },
626 | "b": { "not": "empty" }
627 | },
628 | "order": 0,
629 | "event": {
630 | "type": "updateSum"
631 | }
632 | }
633 | ]
634 | ```
635 | Here although `updateSum` comes after `appendClass`, it will be executed first, since it has a lower order.
636 |
637 | ## Action validation mechanism
638 |
639 | All default actions are validated by default, checking that field exists in the schema, to save you some headaches.
640 | There are 2 levels of validation
641 |
642 | - `propTypes` validation, using FB `prop-types` package
643 | - explicit validation
644 |
645 | You can define those validations in your actions as well, to improve actions usability.
646 |
647 | All validation is disabled in production.
648 |
649 | ### Prop types action validation
650 |
651 | This is reuse of familiar `prop-types` validation used with React components, and it's used in the same way:
652 |
653 | In case of `require` it can look like this:
654 | ```js
655 | require.propTypes = {
656 | field: PropTypes.oneOfType([
657 | PropTypes.string,
658 | PropTypes.arrayOf(PropTypes.string),
659 | ]).isRequired,
660 | };
661 | ```
662 |
663 | The rest is magic.
664 |
665 | WARNING, the default behavior of `prop-types` is to send errors to console,
666 | which you need to have running in order to see them.
667 |
668 | For our `replaceClassNames` action, it can look like this:
669 |
670 | ```js
671 | replaceClassNames.propTypes = {
672 | classNames: PropTypes.string.isRequired,
673 | ignore: PropTypes.arrayOf(PropTypes.string)
674 | };
675 | ```
676 |
677 | ## Explicit validation
678 |
679 | In order to provide more granular validation, you can specify validate function on
680 | your action, that will receive `params`, `schema` and `uiSchema` so you could provide appropriate validation.
681 |
682 | For example, validation for `require` can be done like this:
683 |
684 | ```js
685 | require.validate = function({ field }, schema, uiSchema) {
686 | if (Array.isArray(field)) {
687 | field
688 | .filter(f => schema && schema.properties && schema.properties[f] === undefined)
689 | .forEach(f => console.error(`Field "${f}" is missing from schema on "require"`));
690 | } else if (
691 | schema &&
692 | schema.properties &&
693 | schema.properties[field] === undefined
694 | ) {
695 | console.error(`Field "${field}" is missing from schema on "require"`);
696 | }
697 | };
698 | ```
699 |
700 | Validation is not mandatory, and will be done only if field is provided.
701 |
702 | For our `replaceClassNames` action, it would look similar:
703 | ```js
704 | replaceClassNames.validate = function({ ignore }, schema, uiSchema) {
705 | if (Array.isArray(field)) {
706 | ignore
707 | .filter(f => schema && schema.properties && schema.properties[f] === undefined)
708 | .forEach(f => console.error(`Field "${f}" is missing from schema on "replaceClassNames"`));
709 | } else if (
710 | schema &&
711 | schema.properties &&
712 | schema.properties[ignore] === undefined
713 | ) {
714 | console.error(`Field "${ignore}" is missing from schema on "replaceClassNames"`);
715 | }
716 | };
717 | ```
718 |
719 | # Listening to configuration changes
720 |
721 | In order to listen for configuration changes you can specify `onSchemaConfChange`, which will be notified every time `schema` or `uiSchema` changes it's value.
722 |
723 | ```js
724 | let FormWithConditionals = applyRules(schema, uiSchema, rules, Engine, extraActions)(Form);
725 |
726 | ReactDOM.render(
727 | { console.log("configuration changed") }}/>,
728 | document.querySelector('#app')
729 | );
730 |
731 | ```
732 |
733 | ## Contribute
734 |
735 | - Issue Tracker: github.com/RxNT/react-jsonschema-form-conditionals/issues
736 | - Source Code: github.com/RxNT/react-jsonschema-form-conditionals
737 |
738 | ## Support
739 |
740 | If you are having issues, please let us know.
741 |
742 | ## License
743 |
744 | The project is licensed under the Apache-2.0 license.
745 |
746 |
747 | ## Migration
748 |
749 | ### Migration to 0.4.x
750 |
751 | The only significant change is signature of `applyRules` call. In 0.4.0 `schema`, `uiSchema`, `rules`, `Engine` and `extraActions` all consider to be constant that is why, they moved to `applyRules` call.
752 | This helps improve performance on large schemas.
753 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-jsonschema-form-conditionals",
3 | "description": "Extension of react-jsonschema-form with conditional field support",
4 | "private": false,
5 | "author": "mavarazy@gmail.com",
6 | "version": "0.3.17",
7 | "scripts": {
8 | "build:lib": "rimraf lib && cross-env NODE_ENV=production babel -d lib/ src/",
9 | "build:dist": "rimraf dist && cross-env NODE_ENV=production webpack --config webpack.config.dist.js --optimize-minimize",
10 | "build:playground": "rimraf build && cross-env NODE_ENV=production webpack --config webpack.config.prod.js --optimize-minimize && cp playground/index.prod.html build/index.html",
11 | "cs-check": "prettier -l $npm_package_prettierOptions '{playground,src,test}/**/*.js'",
12 | "cs-format": "prettier $npm_package_prettierOptions '{playground,src,test}/**/*.js' --write",
13 | "dist": "npm run build:lib && npm run build:dist",
14 | "lint": "eslint --fix src test playground",
15 | "precommit": "lint-staged",
16 | "prepush": "npm test",
17 | "publish-to-gh-pages": "npm run build:playground && gh-pages --dist build/",
18 | "publish-to-npm": "npm run dist && npm publish && npm version patch",
19 | "start": "webpack-dev-server",
20 | "tdd": "jest --watchAll",
21 | "test": "jest"
22 | },
23 | "jest": {
24 | "verbose": true,
25 | "collectCoverage": true,
26 | "collectCoverageFrom": [
27 | "src/**/*.{js,jsx}"
28 | ]
29 | },
30 | "prettierOptions": "--jsx-bracket-same-line --trailing-comma es5 --semi",
31 | "lint-staged": {
32 | "{playground,src,test}/**/*.js": [
33 | "npm run lint",
34 | "npm run cs-format",
35 | "git add"
36 | ]
37 | },
38 | "main": "lib/index.js",
39 | "files": [
40 | "dist",
41 | "lib"
42 | ],
43 | "engineStrict": false,
44 | "engines": {
45 | "node": ">=8"
46 | },
47 | "peerDependencies": {
48 | "prop-types": "^15.5.10",
49 | "react": "^16.0.0",
50 | "react-jsonschema-form": "^1.0.0"
51 | },
52 | "dependencies": {
53 | "deepcopy": "^0.6.3",
54 | "selectn": "^1.1.2"
55 | },
56 | "devDependencies": {
57 | "atob": "^2.0.3",
58 | "babel-cli": "^6.0.0",
59 | "babel-core": "^6.0.0",
60 | "babel-eslint": "^8.0.1",
61 | "babel-jest": "^21.0.2",
62 | "babel-loader": "^7.1.2",
63 | "babel-plugin-transform-class-properties": "^6.24.1",
64 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
65 | "babel-polyfill": "^6.26.0",
66 | "babel-preset-env": "^1.6.0",
67 | "babel-preset-react": "^6.24.1",
68 | "babel-preset-stage-0": "^6.24.1",
69 | "babel-register": "^6.26.0",
70 | "coveralls": "^3.0.0",
71 | "cross-env": "^5.0.5",
72 | "css-loader": "^0.28.7",
73 | "enzyme": "^3.1.0",
74 | "enzyme-adapter-react-16": "^1.0.3",
75 | "eslint": "^4.6.1",
76 | "eslint-plugin-jest": "^21.0.2",
77 | "eslint-plugin-react": "^7.3.0",
78 | "eslint-plugin-standard": "^3.0.1",
79 | "exit-hook": "^1.1.1",
80 | "express": "^4.15.4",
81 | "extract-text-webpack-plugin": "^3.0.0",
82 | "gh-pages": "^1.0.0",
83 | "has-flag": "^2.0.0",
84 | "html": "1.0.0",
85 | "husky": "^0.14.3",
86 | "jest": "^21.0.2",
87 | "jest-cli": "^21.1.0",
88 | "jsdom": "^11.2.0",
89 | "json-rules-engine": "^2.0.2",
90 | "json-rules-engine-simplified": "^0.1.11",
91 | "lint-staged": "^4.1.3",
92 | "prettier": "^1.6.1",
93 | "react": "^16.0.0",
94 | "react-dom": "^16.0.0",
95 | "react-jsonschema-form": "^1.0.0",
96 | "react-test-renderer": "^16.0.0",
97 | "react-transform-catch-errors": "^1.0.2",
98 | "react-transform-hmr": "^1.0.4",
99 | "regenerator-runtime": "^0.11.0",
100 | "rimraf": "^2.6.1",
101 | "sinon": "^4.0.2",
102 | "style-loader": "^0.19.0",
103 | "webpack": "^3.5.6",
104 | "webpack-dev-server": "^2.7.1",
105 | "webpack-hot-middleware": "^2.19.1"
106 | },
107 | "directories": {
108 | "test": "test"
109 | },
110 | "repository": {
111 | "type": "git",
112 | "url": "git+https://github.com/RxNT/react-jsonschema-form-conditionals.git"
113 | },
114 | "keywords": [
115 | "react",
116 | "form",
117 | "json-schema",
118 | "conditional",
119 | "predicate"
120 | ],
121 | "license": "Apache-2.0",
122 | "homepage": "https://github.com/RxNT/react-jsonschema-form-conditionals#readme"
123 | }
124 |
--------------------------------------------------------------------------------
/playground/app/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Form from "react-jsonschema-form";
3 | import applyRules from "../../src/applyRules";
4 | import conf from "./conf";
5 |
6 | let { schema, uiSchema, rules, rulesEngine, extraActions, formData } = conf;
7 |
8 | let FormToDisplay = applyRules(
9 | schema,
10 | uiSchema,
11 | rules,
12 | rulesEngine,
13 | extraActions
14 | )(Form);
15 |
16 | export default function() {
17 | return ;
18 | }
19 |
--------------------------------------------------------------------------------
/playground/app/conf/embedded.js:
--------------------------------------------------------------------------------
1 | import Engine from "json-rules-engine-simplified";
2 |
3 | const conf = {
4 | schema: {
5 | type: "object",
6 | properties: {
7 | general: {
8 | type: "object",
9 | properties: {
10 | firstName: {
11 | type: "string",
12 | title: "First Name",
13 | },
14 | lastName: {
15 | type: "string",
16 | title: "Last Name",
17 | },
18 | },
19 | },
20 | },
21 | },
22 | uiSchema: {
23 | general: {
24 | "ui:order": ["firstName", "lastName"],
25 | },
26 | },
27 | formData: {
28 | firstName: "adming",
29 | heightMeasure: "cms",
30 | weight: 117,
31 | weightMeasure: "Kgs",
32 | },
33 | rules: [
34 | {
35 | conditions: {
36 | "general.firstName": { is: "admin" },
37 | },
38 | event: {
39 | type: "remove",
40 | params: {
41 | field: ["general.firstName"],
42 | },
43 | },
44 | },
45 | ],
46 | rulesEngine: Engine,
47 | };
48 |
49 | export default conf;
50 |
--------------------------------------------------------------------------------
/playground/app/conf/index.js:
--------------------------------------------------------------------------------
1 | import Engine from "json-rules-engine-simplified";
2 |
3 | const conf = {
4 | schema: {
5 | type: "object",
6 | properties: {
7 | height: {
8 | type: "integer",
9 | title: "Height",
10 | },
11 | heightMeasure: {
12 | type: "string",
13 | title: "Height Measure",
14 | enum: ["In", "ft", "cms"],
15 | },
16 | weight: {
17 | type: "number",
18 | title: "Weight",
19 | },
20 | weightMeasure: {
21 | type: "string",
22 | title: "Weight Measure",
23 | enum: ["Lbs", "Kgs"],
24 | },
25 | bmi: {
26 | type: "number",
27 | title: "BMI",
28 | },
29 | oxygen: {
30 | type: "number",
31 | title: "Oxygen",
32 | },
33 | },
34 | },
35 | uiSchema: {
36 | height: {
37 | classNames: "col-md-9",
38 | "ui:autofocus": true,
39 | },
40 | heightMeasure: {
41 | classNames: "col-md-3",
42 | },
43 | weight: {
44 | classNames: "col-md-9",
45 | },
46 | weightMeasure: {
47 | classNames: "col-md-3",
48 | },
49 | bmi: {
50 | classNames: "col-md-9",
51 | "ui:disabled": true,
52 | },
53 | oxygen: {
54 | classNames: "col-md-9",
55 | },
56 | },
57 | rules: [
58 | {
59 | conditions: {
60 | height: { greater: 0 },
61 | },
62 | event: {
63 | type: "remove",
64 | params: { field: ["bmi", "heightMeasure"] },
65 | },
66 | },
67 | {
68 | conditions: {
69 | height: { greater: 0 },
70 | heightMeasure: { not: "empty" },
71 | weight: { greater: 0 },
72 | weightMeasure: { not: "empty" },
73 | },
74 | event: {
75 | type: "calculateBMI",
76 | params: { field: "bmi" },
77 | },
78 | },
79 | {
80 | conditions: {
81 | bmi: { greater: 25 },
82 | },
83 | event: {
84 | type: "uiAppend",
85 | params: {
86 | bmi: {
87 | classNames: "has-error",
88 | "ui:disabled": false,
89 | },
90 | },
91 | },
92 | },
93 | {
94 | conditions: {
95 | bmi: {
96 | greater: 18.5,
97 | lessEq: 25,
98 | },
99 | },
100 | event: {
101 | type: "uiAppend",
102 | params: {
103 | bmi: {
104 | classNames: "has-success",
105 | },
106 | },
107 | },
108 | },
109 | {
110 | conditions: {
111 | bmi: {
112 | lessEq: 18.5,
113 | },
114 | },
115 | event: {
116 | type: "uiAppend",
117 | params: {
118 | bmi: {
119 | classNames: "has-warning",
120 | },
121 | },
122 | },
123 | },
124 | {
125 | conditions: { bmi: { lessEq: 15 } },
126 | event: {
127 | type: "uiOverride",
128 | params: { bmi: { "ui:help": "Very severely underweight" } },
129 | },
130 | },
131 | {
132 | conditions: { bmi: { greater: 15, lessEq: 16 } },
133 | event: {
134 | type: "uiOverride",
135 | params: { bmi: { "ui:help": "Severely underweight" } },
136 | },
137 | },
138 | {
139 | conditions: { bmi: { greater: 16, lessEq: 18.5 } },
140 | event: {
141 | type: "uiOverride",
142 | params: { bmi: { "ui:help": "Underweight" } },
143 | },
144 | },
145 | {
146 | conditions: { bmi: { greater: 18.5, lessEq: 25 } },
147 | event: {
148 | type: "uiOverride",
149 | params: { bmi: { "ui:help": "Normal" } },
150 | },
151 | },
152 | {
153 | conditions: { bmi: { greater: 25, lessEq: 30 } },
154 | event: {
155 | type: "uiOverride",
156 | params: { bmi: { "ui:help": "Overweight" } },
157 | },
158 | },
159 | {
160 | conditions: { bmi: { greater: 30, lessEq: 35 } },
161 | event: {
162 | type: "uiOverride",
163 | params: { bmi: { "ui:help": "Obese Class I (Moderately obese)" } },
164 | },
165 | },
166 | {
167 | conditions: { bmi: { greater: 35, lessEq: 40 } },
168 | event: {
169 | type: "uiOverride",
170 | params: { bmi: { "ui:help": "Obese Class II (Severely obese)" } },
171 | },
172 | },
173 | {
174 | conditions: { bmi: { greater: 40 } },
175 | event: {
176 | type: "uiOverride",
177 | params: { bmi: { "ui:help": "Obese Class III (Very severely obese)" } },
178 | },
179 | },
180 | ],
181 | extraActions: {
182 | calculateBMI: function({ field }, schema, uiSchema, formData) {
183 | let weightKilo = formData.weight;
184 | switch (formData.weightMeasure) {
185 | case "Lbs":
186 | weightKilo = formData.weight * 0.453592;
187 | break;
188 | }
189 | let heightMeters = formData.height / 100;
190 | switch (formData.heightMeasure) {
191 | case "In":
192 | heightMeters = formData.height * 0.0254;
193 | break;
194 | case "ft":
195 | heightMeters = formData.height * 0.3048;
196 | break;
197 | }
198 | if (!uiSchema[field]) {
199 | uiSchema[field] = {};
200 | }
201 | let bmi = (weightKilo / (heightMeters * heightMeters)).toFixed(2);
202 | formData[field] = bmi;
203 | },
204 | },
205 | rulesEngine: Engine,
206 | };
207 |
208 | export default conf;
209 |
--------------------------------------------------------------------------------
/playground/app/conf/issue.59.js:
--------------------------------------------------------------------------------
1 | import Engine from "json-rules-engine-simplified";
2 |
3 | const conf = {
4 | schema: {
5 | type: "object",
6 | properties: {
7 | hasBenefitsReference: {
8 | title: "Do you have a Benefits Reference Number?",
9 | type: "boolean",
10 | },
11 | benefitsReference: {
12 | title: "Benefits Reference Number",
13 | type: "string",
14 | },
15 | hasBD2Reference: {
16 | title: "Do you have a BD2 Number?",
17 | type: "boolean",
18 | },
19 | BD2Reference: {
20 | title: "BD2 Number",
21 | type: "string",
22 | },
23 | },
24 | },
25 | uiSchema: {},
26 | formData: {},
27 | rules: [
28 | {
29 | conditions: {
30 | hasBenefitsReference: { is: true },
31 | },
32 | event: [
33 | {
34 | type: "require",
35 | params: {
36 | field: ["hasBD2Reference", "BD2Reference"],
37 | },
38 | },
39 | ],
40 | },
41 | ],
42 | rulesEngine: Engine,
43 | };
44 |
45 | export default conf;
46 |
--------------------------------------------------------------------------------
/playground/app/conf/simpleSum.js:
--------------------------------------------------------------------------------
1 | import Engine from "json-rules-engine-simplified";
2 |
3 | const SCHEMA = {
4 | type: "object",
5 | properties: {
6 | a: { type: "number" },
7 | b: { type: "number" },
8 | sum: { type: "number" },
9 | },
10 | };
11 |
12 | const RULES = [
13 | {
14 | conditions: {
15 | a: { greater: 0 },
16 | },
17 | event: {
18 | type: "sum",
19 | },
20 | },
21 | ];
22 |
23 | const EXTRA_ACTIONS = {
24 | sum: (params, schema, uiSchema, formData) => {
25 | formData.sum = formData.a + formData.b;
26 | },
27 | };
28 |
29 | export default {
30 | schema: SCHEMA,
31 | uiSchema: {},
32 | rules: RULES,
33 | rulesEngine: Engine,
34 | formData: { a: 1, b: 2 },
35 | extraActions: EXTRA_ACTIONS,
36 | };
37 |
--------------------------------------------------------------------------------
/playground/app/index.js:
--------------------------------------------------------------------------------
1 | import ReactDOM from "react-dom";
2 | import React from "react";
3 | import App from "./App";
4 |
5 | ReactDOM.render(, document.getElementById("app"));
6 |
--------------------------------------------------------------------------------
/playground/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Forms with predicates
6 |
7 |
8 |
10 |
11 |
12 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/playground/index.prod.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | react-jsonschema-form-conditionals playground
9 |
10 |
11 |
13 |
14 |
15 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/actions/index.js:
--------------------------------------------------------------------------------
1 | import remove from "./remove";
2 | import require from "./require";
3 | import uiAppend from "./uiAppend";
4 | import uiReplace from "./uiReplace";
5 | import uiOverride from "./uiOverride";
6 |
7 | export const DEFAULT_ACTIONS = {
8 | remove,
9 | require,
10 | uiAppend,
11 | uiReplace,
12 | uiOverride,
13 | };
14 |
15 | export default function execute(
16 | { type, params },
17 | schema,
18 | uiSchema,
19 | formData,
20 | extraActions = {}
21 | ) {
22 | let action = extraActions[type] ? extraActions[type] : DEFAULT_ACTIONS[type];
23 | action(params, schema, uiSchema, formData);
24 | }
25 |
--------------------------------------------------------------------------------
/src/actions/remove.js:
--------------------------------------------------------------------------------
1 | import { toArray, findRelSchemaAndField, findRelUiSchema } from "../utils";
2 | import { validateFields } from "./validateAction";
3 | import PropTypes from "prop-types";
4 |
5 | function doRemove({ field, schema }, uiSchema) {
6 | let requiredIndex = schema.required ? schema.required.indexOf(field) : -1;
7 | if (requiredIndex !== -1) {
8 | schema.required.splice(requiredIndex, 1);
9 | }
10 | delete schema.properties[field];
11 | delete uiSchema[field];
12 | let fieldIndex = (uiSchema["ui:order"] ? uiSchema["ui:order"] : []).indexOf(
13 | field
14 | );
15 | if (fieldIndex !== -1) {
16 | uiSchema["ui:order"].splice(fieldIndex, 1);
17 | }
18 | }
19 |
20 | /**
21 | * Remove specified field both from uiSchema and schema
22 | *
23 | * @param field
24 | * @param schema
25 | * @param uiSchema
26 | * @returns {{schema: *, uiSchema: *}}
27 | */
28 | export default function remove({ field }, schema, uiSchema) {
29 | let fieldArr = toArray(field);
30 | fieldArr.forEach(field =>
31 | doRemove(
32 | findRelSchemaAndField(field, schema),
33 | findRelUiSchema(field, uiSchema)
34 | )
35 | );
36 | }
37 |
38 | remove.propTypes = {
39 | field: PropTypes.oneOfType([
40 | PropTypes.string,
41 | PropTypes.arrayOf(PropTypes.string),
42 | ]).isRequired,
43 | };
44 |
45 | remove.validate = validateFields("remove", function({ field }) {
46 | return field;
47 | });
48 |
--------------------------------------------------------------------------------
/src/actions/require.js:
--------------------------------------------------------------------------------
1 | import { toArray, findRelSchemaAndField } from "../utils";
2 | import { validateFields } from "./validateAction";
3 | import PropTypes from "prop-types";
4 |
5 | function doRequire({ field, schema }) {
6 | if (!schema.required) {
7 | schema.required = [];
8 | }
9 |
10 | if (schema.required.indexOf(field) === -1) {
11 | schema.required.push(field);
12 | }
13 | }
14 |
15 | /**
16 | * Makes provided field required
17 | *
18 | * @param params
19 | * @param schema
20 | * @param uiSchema
21 | * @returns {{schema: *, uiSchema: *}}
22 | */
23 | export default function require({ field }, schema) {
24 | let fieldArr = toArray(field);
25 | toArray(fieldArr).forEach(field =>
26 | doRequire(findRelSchemaAndField(field, schema))
27 | );
28 | }
29 |
30 | require.propTypes = {
31 | field: PropTypes.oneOfType([
32 | PropTypes.string,
33 | PropTypes.arrayOf(PropTypes.string),
34 | ]).isRequired,
35 | };
36 |
37 | require.validate = validateFields("require", function({ field }) {
38 | return field;
39 | });
40 |
--------------------------------------------------------------------------------
/src/actions/uiAppend.js:
--------------------------------------------------------------------------------
1 | import { toArray } from "../utils";
2 | import { validateFields } from "./validateAction";
3 | import PropTypes from "prop-types";
4 |
5 | /**
6 | * Append original field in uiSchema with external configuration
7 | *
8 | * @param field
9 | * @param schema
10 | * @param uiSchema
11 | * @param conf
12 | * @returns {{schema: *, uiSchema: *}}
13 | */
14 | function doAppend(uiSchema, params) {
15 | Object.keys(params).forEach(field => {
16 | let appendVal = params[field];
17 | let fieldUiSchema = uiSchema[field];
18 | if (!fieldUiSchema) {
19 | uiSchema[field] = appendVal;
20 | } else if (Array.isArray(fieldUiSchema)) {
21 | toArray(appendVal)
22 | .filter(v => !fieldUiSchema.includes(v))
23 | .forEach(v => fieldUiSchema.push(v));
24 | } else if (typeof appendVal === "object" && !Array.isArray(appendVal)) {
25 | doAppend(fieldUiSchema, appendVal);
26 | } else if (typeof fieldUiSchema === "string") {
27 | if (!fieldUiSchema.includes(appendVal)) {
28 | uiSchema[field] = fieldUiSchema + " " + appendVal;
29 | }
30 | } else {
31 | uiSchema[field] = appendVal;
32 | }
33 | });
34 | }
35 |
36 | export default function uiAppend(params, schema, uiSchema) {
37 | doAppend(uiSchema, params);
38 | }
39 |
40 | uiAppend.propTypes = PropTypes.object.isRequired;
41 | uiAppend.validate = validateFields("uiAppend", function(params) {
42 | return Object.keys(params);
43 | });
44 |
--------------------------------------------------------------------------------
/src/actions/uiOverride.js:
--------------------------------------------------------------------------------
1 | import { validateFields } from "./validateAction";
2 | import PropTypes from "prop-types";
3 |
4 | /**
5 | * Override original field in uiSchema with defined configuration
6 | *
7 | * @param field
8 | * @param schema
9 | * @param uiSchema
10 | * @param conf
11 | * @returns {{schema: *, uiSchema: *}}
12 | */
13 | function doOverride(uiSchema, params) {
14 | Object.keys(params).forEach(field => {
15 | let appendVal = params[field];
16 | let fieldUiSchema = uiSchema[field];
17 | if (!fieldUiSchema) {
18 | uiSchema[field] = appendVal;
19 | } else if (typeof appendVal === "object" && !Array.isArray(appendVal)) {
20 | doOverride(fieldUiSchema, appendVal);
21 | } else {
22 | uiSchema[field] = appendVal;
23 | }
24 | });
25 | }
26 |
27 | export default function uiOverride(params, schema, uiSchema) {
28 | doOverride(uiSchema, params);
29 | }
30 |
31 | uiOverride.propTypes = PropTypes.object.isRequired;
32 | uiOverride.validate = validateFields("uiOverride", function(params) {
33 | return Object.keys(params);
34 | });
35 |
--------------------------------------------------------------------------------
/src/actions/uiReplace.js:
--------------------------------------------------------------------------------
1 | import { validateFields } from "./validateAction";
2 | import PropTypes from "prop-types";
3 |
4 | /**
5 | * Replace original field in uiSchema with defined configuration
6 | *
7 | * @param field
8 | * @param schema
9 | * @param uiSchema
10 | * @param conf
11 | * @returns {{schema: *, uiSchema: *}}
12 | */
13 | export default function uiReplace(params, schema, uiSchema) {
14 | Object.keys(params).forEach(f => {
15 | uiSchema[f] = params[f];
16 | });
17 | }
18 |
19 | uiReplace.propTypes = PropTypes.object.isRequired;
20 | uiReplace.validate = validateFields("uiReplace", function(params) {
21 | return Object.keys(params);
22 | });
23 |
--------------------------------------------------------------------------------
/src/actions/validateAction.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import { toArray, toError } from "../utils";
3 | import { extractRefSchema } from "json-rules-engine-simplified/lib/utils";
4 |
5 | const hasField = (field, schema) => {
6 | let separator = field.indexOf(".");
7 | if (separator === -1) {
8 | return schema.properties[field] !== undefined;
9 | } else {
10 | let parentField = field.substr(0, separator);
11 | let refSchema = extractRefSchema(parentField, schema);
12 | return refSchema ? hasField(field.substr(separator + 1), refSchema) : false;
13 | }
14 | };
15 |
16 | export const validateFields = (action, fetchFields) => {
17 | if (!fetchFields) {
18 | toError("validateFields requires fetchFields function");
19 | return;
20 | }
21 | return (params, schema) => {
22 | let relFields = toArray(fetchFields(params));
23 | relFields
24 | .filter(field => !hasField(field, schema))
25 | .forEach(field =>
26 | toError(`Field "${field}" is missing from schema on "${action}"`)
27 | );
28 | };
29 | };
30 |
31 | export default function(action, params, schema, uiSchema) {
32 | if (action.propTypes !== undefined && action.propTypes !== null) {
33 | PropTypes.checkPropTypes(action.propTypes, params, "prop", action);
34 | }
35 |
36 | if (action.validate && typeof action.validate === "function") {
37 | action.validate(params, schema, uiSchema);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/applyRules.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 | import { isDevelopment, toError } from "./utils";
4 | import rulesRunner from "./rulesRunner";
5 |
6 | import { DEFAULT_ACTIONS } from "./actions";
7 | import validateAction from "./actions/validateAction";
8 |
9 | export default function applyRules(
10 | schema,
11 | uiSchema,
12 | rules,
13 | Engine,
14 | extraActions = {}
15 | ) {
16 | if (isDevelopment()) {
17 | const propTypes = {
18 | Engine: PropTypes.func.isRequired,
19 | rules: PropTypes.arrayOf(
20 | PropTypes.shape({
21 | conditions: PropTypes.object.isRequired,
22 | order: PropTypes.number,
23 | event: PropTypes.oneOfType([
24 | PropTypes.shape({
25 | type: PropTypes.string.isRequired
26 | }),
27 | PropTypes.arrayOf(
28 | PropTypes.shape({
29 | type: PropTypes.string.isRequired
30 | })
31 | )
32 | ])
33 | })
34 | ).isRequired,
35 | extraActions: PropTypes.object
36 | };
37 |
38 | PropTypes.checkPropTypes(
39 | propTypes,
40 | { rules, Engine, extraActions },
41 | "props",
42 | "react-jsonschema-form-manager"
43 | );
44 |
45 | rules
46 | .reduce((agg, { event }) => agg.concat(event), [])
47 | .forEach(({ type, params }) => {
48 | // Find associated action
49 | let action = extraActions[type]
50 | ? extraActions[type]
51 | : DEFAULT_ACTIONS[type];
52 | if (action === undefined) {
53 | toError(`Rule contains invalid action "${type}"`);
54 | return;
55 | }
56 |
57 | validateAction(action, params, schema, uiSchema);
58 | });
59 | }
60 |
61 | const runRules = rulesRunner(schema, uiSchema, rules, Engine, extraActions);
62 |
63 | return FormComponent => {
64 | class FormWithConditionals extends Component {
65 | constructor(props) {
66 | super(props);
67 |
68 | this.handleChange = this.handleChange.bind(this);
69 | this.updateConf = this.updateConf.bind(this);
70 | let { formData = {} } = this.props;
71 |
72 | this.state = { schema, uiSchema, formData };
73 | this.updateConf(formData);
74 | }
75 |
76 | componentWillReceiveProps(nextProps) {
77 | this.updateConf(nextProps.formData);
78 | }
79 |
80 | updateConf(formData) {
81 | return runRules(formData).then(conf => {
82 | const newConf = JSON.stringify(conf);
83 | const oldConf = JSON.stringify(this.state);
84 |
85 | if (newConf !== oldConf) {
86 | this.setState(conf);
87 | }
88 |
89 | return conf;
90 | });
91 | }
92 |
93 | handleChange(change) {
94 | let { formData } = change;
95 | let updTask = this.updateConf(formData);
96 |
97 | let { onChange } = this.props;
98 | if (onChange) {
99 | updTask.then(conf => {
100 | let updChange = Object.assign({}, change, conf);
101 | onChange(updChange);
102 | });
103 | }
104 | }
105 |
106 | render() {
107 | // Assignment order is important
108 | let formConf = Object.assign({}, this.props, this.state, {
109 | onChange: this.handleChange
110 | });
111 |
112 | return ;
113 | }
114 | }
115 |
116 | return FormWithConditionals;
117 | };
118 | }
119 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import applyRules from "./applyRules";
2 | import { validateFields } from "./actions/validateAction";
3 | import { findRelSchemaAndField, findRelUiSchema } from "./utils";
4 |
5 | export { validateFields, findRelSchemaAndField, findRelUiSchema };
6 |
7 | export default applyRules;
8 |
--------------------------------------------------------------------------------
/src/rulesRunner.js:
--------------------------------------------------------------------------------
1 | import execute from "./actions";
2 | import deepcopy from "deepcopy";
3 | import { deepEquals } from "react-jsonschema-form/lib/utils";
4 |
5 | function doRunRules(engine, formData, schema, uiSchema, extraActions = {}) {
6 | let schemaCopy = deepcopy(schema);
7 | let uiSchemaCopy = deepcopy(uiSchema);
8 | let formDataCopy = deepcopy(formData);
9 |
10 | let res = engine.run(formData).then(events => {
11 | events.forEach(event =>
12 | execute(event, schemaCopy, uiSchemaCopy, formDataCopy, extraActions)
13 | );
14 | });
15 |
16 | return res.then(() => {
17 | return {
18 | schema: schemaCopy,
19 | uiSchema: uiSchemaCopy,
20 | formData: formDataCopy,
21 | };
22 | });
23 | }
24 |
25 | export function normRules(rules) {
26 | return rules.sort(function(a, b) {
27 | if (a.order === undefined) {
28 | return b.order === undefined ? 0 : 1;
29 | }
30 | return b.order === undefined ? -1 : a.order - b.order;
31 | });
32 | }
33 |
34 | export default function rulesRunner(
35 | schema,
36 | uiSchema,
37 | rules,
38 | engine,
39 | extraActions
40 | ) {
41 | engine = typeof engine === "function" ? new engine([], schema) : engine;
42 | normRules(rules).forEach(rule => engine.addRule(rule));
43 |
44 | return formData => {
45 | if (formData === undefined || formData === null) {
46 | return Promise.resolve({ schema, uiSchema, formData });
47 | }
48 |
49 | return doRunRules(
50 | engine,
51 | formData,
52 | schema,
53 | uiSchema,
54 | extraActions
55 | ).then(conf => {
56 | if (deepEquals(conf.formData, formData)) {
57 | return conf;
58 | } else {
59 | return doRunRules(
60 | engine,
61 | conf.formData,
62 | schema,
63 | uiSchema,
64 | extraActions
65 | );
66 | }
67 | });
68 | };
69 | }
70 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | import { extractRefSchema } from "json-rules-engine-simplified/lib/utils";
2 |
3 | export const isDevelopment = () => {
4 | return process.env.NODE_ENV !== "production";
5 | };
6 |
7 | export const toArray = field => {
8 | if (Array.isArray(field)) {
9 | return field;
10 | } else {
11 | return [field];
12 | }
13 | };
14 |
15 | export const toError = message => {
16 | if (isDevelopment()) {
17 | throw new ReferenceError(message);
18 | } else {
19 | console.error(message);
20 | }
21 | };
22 |
23 | /**
24 | * Find relevant schema for the field
25 | * @returns { field: "string", schema: "object" } relevant field and schema
26 | */
27 | export const findRelSchemaAndField = (field, schema) => {
28 | let separator = field.indexOf(".");
29 | if (separator === -1) {
30 | return { field, schema };
31 | }
32 |
33 | let parentField = field.substr(0, separator);
34 | let refSchema = extractRefSchema(parentField, schema);
35 | if (refSchema) {
36 | return findRelSchemaAndField(field.substr(separator + 1), refSchema);
37 | }
38 |
39 | toError(`Failed to retrieve ${refSchema} from schema`);
40 | return { field, schema };
41 | };
42 |
43 | export function findRelUiSchema(field, uiSchema) {
44 | let separator = field.indexOf(".");
45 | if (separator === -1) {
46 | return uiSchema;
47 | }
48 |
49 | let parentField = field.substr(0, separator);
50 | let refUiSchema = uiSchema[parentField];
51 | if (!refUiSchema) {
52 | return uiSchema;
53 | } else {
54 | return findRelUiSchema(field.substr(separator + 1), refUiSchema);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | },
5 | "globals": {
6 | "d": true
7 | },
8 | "rules": {
9 | "no-unused-vars": [
10 | 2,
11 | {
12 | "varsIgnorePattern": "^d$"
13 | }
14 | ]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/test/actions/remove.fromArray.test.js:
--------------------------------------------------------------------------------
1 | import remove from "../../src/actions/remove";
2 | import deepcopy from "deepcopy";
3 |
4 | test("remove from array", () => {
5 | let origSchema = {
6 | type: "object",
7 | properties: {
8 | arr: {
9 | type: "array",
10 | items: {
11 | type: "object",
12 | properties: {
13 | foo: { type: "string" },
14 | boo: { type: "string" },
15 | },
16 | },
17 | },
18 | },
19 | };
20 |
21 | let field = "arr.boo";
22 |
23 | let expSchema = {
24 | type: "object",
25 | properties: {
26 | arr: {
27 | type: "array",
28 | items: {
29 | type: "object",
30 | properties: {
31 | foo: { type: "string" },
32 | },
33 | },
34 | },
35 | },
36 | };
37 |
38 | let schema = deepcopy(origSchema);
39 | remove({ field }, schema, {});
40 | expect(origSchema).not.toEqual(schema);
41 | expect(schema).toEqual(expSchema);
42 | });
43 |
--------------------------------------------------------------------------------
/test/actions/remove.nested.test.js:
--------------------------------------------------------------------------------
1 | import remove from "../../src/actions/remove";
2 | import deepcopy from "deepcopy";
3 |
4 | let schema = {
5 | definitions: {
6 | address: {
7 | properties: {
8 | street: { type: "string" },
9 | zip: { type: "string" },
10 | },
11 | },
12 | },
13 | properties: {
14 | title: { type: "string" },
15 | firstName: { type: "string" },
16 | address: { $ref: "#/definitions/address" },
17 | profile: {
18 | type: "object",
19 | properties: {
20 | age: { type: "number" },
21 | height: { type: "number" },
22 | },
23 | },
24 | },
25 | };
26 |
27 | test("remove from ref fields", () => {
28 | let resSchema = deepcopy(schema);
29 | remove({ field: ["address.zip"] }, resSchema, {});
30 |
31 | let newDefinition = {
32 | address: {
33 | properties: {
34 | street: { type: "string" },
35 | },
36 | },
37 | };
38 |
39 | let expSchema = Object.assign({}, schema, { definitions: newDefinition });
40 | expect(resSchema).toEqual(expSchema);
41 | });
42 |
43 | test("remove from Object fields", () => {
44 | let resSchema = deepcopy(schema);
45 | remove({ field: ["profile.age"] }, resSchema, {});
46 |
47 | let properties = Object.assign({}, schema.properties, {
48 | profile: {
49 | type: "object",
50 | properties: {
51 | height: { type: "number" },
52 | },
53 | },
54 | });
55 |
56 | let expSchema = Object.assign({}, schema, { properties });
57 | expect(resSchema).toEqual(expSchema);
58 | });
59 |
--------------------------------------------------------------------------------
/test/actions/remove.test.js:
--------------------------------------------------------------------------------
1 | import deepcopy from "deepcopy";
2 | import remove from "../../src/actions/remove";
3 | import validateAction from "../../src/actions/validateAction";
4 | import { testInProd } from "../utils";
5 |
6 | let origSchema = {
7 | properties: {
8 | title: { type: "string" },
9 | firstName: { type: "string" },
10 | },
11 | };
12 | let origUiSchema = {
13 | title: {},
14 | firstName: {},
15 | };
16 |
17 | test("removes field", () => {
18 | let schema = deepcopy(origSchema);
19 | let uiSchema = deepcopy(origUiSchema);
20 | remove({ field: ["title"] }, schema, uiSchema);
21 |
22 | let schemaWithoutTitle = {
23 | properties: {
24 | firstName: { type: "string" },
25 | },
26 | };
27 | expect(schema).toEqual(schemaWithoutTitle);
28 |
29 | let uiSchemaWithoutTitle = {
30 | firstName: {},
31 | };
32 | expect(uiSchema).toEqual(uiSchemaWithoutTitle);
33 | });
34 |
35 | test("ignores invalid field", () => {
36 | let schema = deepcopy(origSchema);
37 | let uiSchema = deepcopy(origUiSchema);
38 | remove({ field: "lastName" }, schema, uiSchema);
39 | expect(schema).toEqual(origSchema);
40 | expect(uiSchema).toEqual(origUiSchema);
41 | });
42 |
43 | test("remove required", () => {
44 | let origSchema = {
45 | required: ["title"],
46 | properties: {
47 | title: { type: "string" },
48 | firstName: { type: "string" },
49 | },
50 | };
51 |
52 | let schema = deepcopy(origSchema);
53 | let uiSchema = deepcopy(origUiSchema);
54 |
55 | remove({ field: "title" }, schema, uiSchema);
56 |
57 | let schemaWithoutTitle = {
58 | required: [],
59 | properties: {
60 | firstName: { type: "string" },
61 | },
62 | };
63 | expect(schema).toEqual(schemaWithoutTitle);
64 |
65 | let uiSchemaWithoutTitle = {
66 | firstName: {},
67 | };
68 | expect(uiSchema).toEqual(uiSchemaWithoutTitle);
69 | });
70 |
71 | test("remove validates fields", () => {
72 | expect(() =>
73 | validateAction(remove, { field: ["totle"] }, origSchema, origUiSchema)
74 | ).toThrow();
75 | expect(
76 | testInProd(() =>
77 | validateAction(remove, { field: ["totle"] }, origSchema, origUiSchema)
78 | )
79 | ).toBeUndefined();
80 | expect(
81 | validateAction(remove, { field: ["title"] }, origSchema, origUiSchema)
82 | ).toBeUndefined();
83 | });
84 |
--------------------------------------------------------------------------------
/test/actions/require.nested.test.js:
--------------------------------------------------------------------------------
1 | import require from "../../src/actions/require";
2 | import deepcopy from "deepcopy";
3 |
4 | let schema = {
5 | definitions: {
6 | address: {
7 | type: "object",
8 | properties: {
9 | street: { type: "string" },
10 | zip: { type: "string" },
11 | },
12 | },
13 | },
14 | properties: {
15 | title: { type: "string" },
16 | firstName: { type: "string" },
17 | address: { $ref: "#/definitions/address" },
18 | profile: {
19 | type: "object",
20 | properties: {
21 | age: { type: "number" },
22 | height: { type: "number" },
23 | },
24 | },
25 | },
26 | };
27 |
28 | test("require from ref fields", () => {
29 | let resSchema = deepcopy(schema);
30 | require({ field: "address.zip" }, resSchema);
31 |
32 | let newDefinition = {
33 | address: {
34 | type: "object",
35 | required: ["zip"],
36 | properties: {
37 | street: { type: "string" },
38 | zip: { type: "string" },
39 | },
40 | },
41 | };
42 |
43 | let expSchema = Object.assign({}, schema, { definitions: newDefinition });
44 | expect(resSchema).toEqual(expSchema);
45 | });
46 |
47 | test("require from Object fields", () => {
48 | let resSchema = deepcopy(schema);
49 | require({ field: "profile.age" }, resSchema);
50 |
51 | let properties = Object.assign({}, schema.properties, {
52 | profile: {
53 | type: "object",
54 | required: ["age"],
55 | properties: {
56 | age: { type: "number" },
57 | height: { type: "number" },
58 | },
59 | },
60 | });
61 |
62 | let expSchema = Object.assign({}, schema, { properties });
63 | expect(resSchema).toEqual(expSchema);
64 | });
65 |
--------------------------------------------------------------------------------
/test/actions/require.test.js:
--------------------------------------------------------------------------------
1 | import deepcopy from "deepcopy";
2 | import require from "../../src/actions/require";
3 | import validateAction from "../../src/actions/validateAction";
4 | import { testInProd } from "../utils";
5 |
6 | let origUiSchema = {
7 | title: {},
8 | firstName: {},
9 | };
10 |
11 | let origSchema = {
12 | required: ["title"],
13 | properties: {
14 | title: { type: "string" },
15 | firstName: { type: "string" },
16 | },
17 | };
18 |
19 | test("add required section", () => {
20 | let schema = deepcopy(origSchema);
21 | let uiSchema = deepcopy(origUiSchema);
22 |
23 | require({ field: "firstName" }, schema);
24 |
25 | let schemaWithTitleReq = {
26 | required: ["title", "firstName"],
27 | properties: {
28 | title: { type: "string" },
29 | firstName: { type: "string" },
30 | },
31 | };
32 | expect(schema).toEqual(schemaWithTitleReq);
33 | expect(uiSchema).toEqual(origUiSchema);
34 | });
35 |
36 | test("ignores already required field", () => {
37 | let schema = deepcopy(origSchema);
38 | let uiSchema = deepcopy(origUiSchema);
39 |
40 | require({ field: ["title"] }, schema);
41 |
42 | expect(schema).toEqual(origSchema);
43 | expect(uiSchema).toEqual(origUiSchema);
44 | });
45 |
46 | test("require validates fields", () => {
47 | expect(() =>
48 | validateAction(require, { field: ["totle"] }, origSchema, origUiSchema)
49 | ).toThrow();
50 | expect(
51 | testInProd(() =>
52 | validateAction(require, { field: ["totle"] }, origSchema, origUiSchema)
53 | )
54 | ).toBeUndefined();
55 | expect(
56 | validateAction(require, { field: ["title"] }, origSchema, origUiSchema)
57 | ).toBeUndefined();
58 | });
59 |
--------------------------------------------------------------------------------
/test/actions/uiAppend.test.js:
--------------------------------------------------------------------------------
1 | import deepcopy from "deepcopy";
2 | import uiAppend from "../../src/actions/uiAppend";
3 |
4 | let origUiSchema = {
5 | "ui:order": ["firstName"],
6 | lastName: {
7 | classNames: "col-md-1",
8 | },
9 | firstName: {
10 | "ui:disabled": false,
11 | num: 23,
12 | },
13 | };
14 |
15 | let origSchema = {
16 | properties: {
17 | lastName: { type: "string" },
18 | firstName: { type: "string" },
19 | },
20 | };
21 |
22 | let params = {
23 | "ui:order": ["lastName"],
24 | lastName: {
25 | classNames: "has-error",
26 | },
27 | firstName: {
28 | classNames: "col-md-6",
29 | "ui:disabled": true,
30 | num: 22,
31 | },
32 | };
33 |
34 | test("default values", () => {
35 | let schema = {};
36 | let uiSchema = {};
37 | uiAppend(params, schema, uiSchema);
38 | expect(schema).toEqual({});
39 | expect(uiSchema).toEqual(params);
40 | });
41 |
42 | test("double append", () => {
43 | let schema = {};
44 | let uiSchema = {};
45 | uiAppend(params, schema, uiSchema);
46 | uiAppend(params, schema, uiSchema);
47 | uiAppend(params, schema, uiSchema);
48 | expect(schema).toEqual({});
49 | expect(uiSchema).toEqual(params);
50 | });
51 |
52 | test("append required section", () => {
53 | let schema = deepcopy(origSchema);
54 | let uiSchema = deepcopy(origUiSchema);
55 | uiAppend(params, schema, uiSchema);
56 | expect(schema).toEqual(origSchema);
57 | let expectedUiSchema = {
58 | "ui:order": ["firstName", "lastName"],
59 | lastName: {
60 | classNames: "col-md-1 has-error",
61 | },
62 | firstName: {
63 | classNames: "col-md-6",
64 | "ui:disabled": true,
65 | num: 22,
66 | },
67 | };
68 | expect(uiSchema).toEqual(expectedUiSchema);
69 | });
70 |
--------------------------------------------------------------------------------
/test/actions/uiAppend/nested/rules.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": [
3 | {
4 | "conditions": {
5 | "vitals$bmi": {
6 | "greater": 24.99
7 | }
8 | },
9 | "event": {
10 | "type": "uiAppend",
11 | "params": {
12 | "vitals": {
13 | "bmi": {
14 | "classNames": "has-error vitals-danger"
15 | }
16 | }
17 | }
18 | }
19 | },
20 | {
21 | "conditions": {
22 | "vitals$bmi": {
23 | "greater": 18.59,
24 | "lessEq": 24.99
25 | }
26 | },
27 | "event": {
28 | "type": "uiAppend",
29 | "params": {
30 | "vitals": {
31 | "bmi": {
32 | "classNames": "has-success vitals-success"
33 | }
34 | }
35 | }
36 | }
37 | },
38 | {
39 | "conditions": {
40 | "vitals$bmi": {
41 | "lessEq": 18.59
42 | }
43 | },
44 | "event": {
45 | "type": "uiAppend",
46 | "params": {
47 | "vitals": {
48 | "bmi": {
49 | "classNames": "has-warning vitals-warning"
50 | }
51 | }
52 | }
53 | }
54 | },
55 | {
56 | "conditions": {
57 | "vitals$oximetry": {
58 | "greater": 101
59 | }
60 | },
61 | "event": {
62 | "type": "uiAppend",
63 | "params": {
64 | "vitals": {
65 | "oximetry": {
66 | "classNames": "has-error vitals-danger"
67 | }
68 | }
69 | }
70 | }
71 | },
72 | {
73 | "conditions": {
74 | "vitals$oximetry": {
75 | "greater": 85,
76 | "lessEq": 101
77 | }
78 | },
79 | "event": {
80 | "type": "uiAppend",
81 | "params": {
82 | "vitals": {
83 | "oximetry": {
84 | "classNames": "has-success vitals-success"
85 | }
86 | }
87 | }
88 | }
89 | },
90 | {
91 | "conditions": {
92 | "vitals$oximetry": {
93 | "lessEq": 85
94 | }
95 | },
96 | "event": {
97 | "type": "uiAppend",
98 | "params": {
99 | "vitals": {
100 | "oximetry": {
101 | "classNames": "has-warning vitals-warning"
102 | }
103 | }
104 | }
105 | }
106 | },
107 | {
108 | "conditions": {
109 | "vitals$fiO2": {
110 | "greater": 45
111 | }
112 | },
113 | "event": {
114 | "type": "uiAppend",
115 | "params": {
116 | "vitals": {
117 | "fiO2": {
118 | "classNames": "has-error vitals-danger"
119 | }
120 | }
121 | }
122 | }
123 | },
124 | {
125 | "conditions": {
126 | "vitals$fiO2": {
127 | "greater": 0,
128 | "lessEq": 45
129 | }
130 | },
131 | "event": {
132 | "type": "uiAppend",
133 | "params": {
134 | "vitals": {
135 | "fiO2": {
136 | "classNames": "has-success vitals-success"
137 | }
138 | }
139 | }
140 | }
141 | },
142 | {
143 | "conditions": {
144 | "vitals$fiO2": {
145 | "lessEq": 0
146 | }
147 | },
148 | "event": {
149 | "type": "uiAppend",
150 | "params": {
151 | "vitals": {
152 | "fiO2": {
153 | "classNames": "has-warning vitals-warning"
154 | }
155 | }
156 | }
157 | }
158 | },
159 | {
160 | "conditions": {
161 | "vitals$flow": {
162 | "greater": 6
163 | }
164 | },
165 | "event": {
166 | "type": "uiAppend",
167 | "params": {
168 | "vitals": {
169 | "flow": {
170 | "classNames": "has-error vitals-danger"
171 | }
172 | }
173 | }
174 | }
175 | },
176 | {
177 | "conditions": {
178 | "vitals$flow": {
179 | "greater": 4,
180 | "lessEq": 6
181 | }
182 | },
183 | "event": {
184 | "type": "uiAppend",
185 | "params": {
186 | "vitals": {
187 | "flow": {
188 | "classNames": "has-success vitals-success"
189 | }
190 | }
191 | }
192 | }
193 | },
194 | {
195 | "conditions": {
196 | "vitals$flow": {
197 | "lessEq": 4
198 | }
199 | },
200 | "event": {
201 | "type": "uiAppend",
202 | "params": {
203 | "vitals": {
204 | "flow": {
205 | "classNames": "has-warning vitals-warning"
206 | }
207 | }
208 | }
209 | }
210 | },
211 | {
212 | "conditions": {
213 | "vitals$heartRate": {
214 | "greater": 100
215 | }
216 | },
217 | "event": {
218 | "type": "uiAppend",
219 | "params": {
220 | "vitals": {
221 | "heartRate": {
222 | "classNames": "has-error vitals-danger"
223 | }
224 | }
225 | }
226 | }
227 | },
228 | {
229 | "conditions": {
230 | "vitals$heartRate": {
231 | "greater": 60,
232 | "lessEq": 100
233 | }
234 | },
235 | "event": {
236 | "type": "uiAppend",
237 | "params": {
238 | "vitals": {
239 | "heartRate": {
240 | "classNames": "has-success vitals-success"
241 | }
242 | }
243 | }
244 | }
245 | },
246 | {
247 | "conditions": {
248 | "vitals$heartRate": {
249 | "lessEq": 60
250 | }
251 | },
252 | "event": {
253 | "type": "uiAppend",
254 | "params": {
255 | "vitals": {
256 | "heartRate": {
257 | "classNames": "has-warning vitals-warning"
258 | }
259 | }
260 | }
261 | }
262 | },
263 | {
264 | "conditions": {
265 | "vitals$glucose": {
266 | "greater": 500
267 | }
268 | },
269 | "event": {
270 | "type": "uiAppend",
271 | "params": {
272 | "vitals": {
273 | "glucose": {
274 | "classNames": "has-error vitals-danger"
275 | }
276 | }
277 | }
278 | }
279 | },
280 | {
281 | "conditions": {
282 | "vitals$glucose": {
283 | "greater": 45,
284 | "lessEq": 500
285 | }
286 | },
287 | "event": {
288 | "type": "uiAppend",
289 | "params": {
290 | "vitals": {
291 | "glucose": {
292 | "classNames": "has-success vitals-success"
293 | }
294 | }
295 | }
296 | }
297 | },
298 | {
299 | "conditions": {
300 | "vitals$glucose": {
301 | "lessEq": 45
302 | }
303 | },
304 | "event": {
305 | "type": "uiAppend",
306 | "params": {
307 | "vitals": {
308 | "glucose": {
309 | "classNames": "has-warning vitals-warning"
310 | }
311 | }
312 | }
313 | }
314 | },
315 | {
316 | "conditions": {
317 | "vitals$respirationRate$value": {
318 | "greater": 18
319 | }
320 | },
321 | "event": {
322 | "type": "uiAppend",
323 | "params": {
324 | "vitals": {
325 | "respirationRate": {
326 | "value": {
327 | "classNames": "has-error vitals-danger"
328 | }
329 | }
330 | }
331 | }
332 | }
333 | },
334 | {
335 | "conditions": {
336 | "vitals$respirationRate$value": {
337 | "greater": 12,
338 | "lessEq": 18
339 | }
340 | },
341 | "event": {
342 | "type": "uiAppend",
343 | "params": {
344 | "vitals": {
345 | "respirationRate": {
346 | "value": {
347 | "classNames": "has-success vitals-success"
348 | }
349 | }
350 | }
351 | }
352 | }
353 | },
354 | {
355 | "conditions": {
356 | "vitals$respirationRate$value": {
357 | "lessEq": 12
358 | }
359 | },
360 | "event": {
361 | "type": "uiAppend",
362 | "params": {
363 | "vitals": {
364 | "respirationRate": {
365 | "value": {
366 | "classNames": "has-warning vitals-warning"
367 | }
368 | }
369 | }
370 | }
371 | }
372 | },
373 | {
374 | "conditions": {
375 | "vitals$temperature$value": {
376 | "greater": 99.1
377 | },
378 | "vitals$temperature$measure": {
379 | "is": "F"
380 | }
381 | },
382 | "event": {
383 | "type": "uiAppend",
384 | "params": {
385 | "vitals": {
386 | "temperature": {
387 | "value": {
388 | "classNames": "has-error vitals-danger"
389 | }
390 | }
391 | }
392 | }
393 | }
394 | },
395 | {
396 | "conditions": {
397 | "vitals$temperature$value": {
398 | "greater": 97.8,
399 | "lessEq": 99.1
400 | },
401 | "vitals$temperature$measure": {
402 | "is": "F"
403 | }
404 | },
405 | "event": {
406 | "type": "uiAppend",
407 | "params": {
408 | "vitals": {
409 | "temperature": {
410 | "value": {
411 | "classNames": "has-success vitals-success"
412 | }
413 | }
414 | }
415 | }
416 | }
417 | },
418 | {
419 | "conditions": {
420 | "vitals$temperature$value": {
421 | "lessEq": 97.8
422 | },
423 | "vitals$temperature$measure": {
424 | "is": "F"
425 | }
426 | },
427 | "event": {
428 | "type": "uiAppend",
429 | "params": {
430 | "vitals": {
431 | "temperature": {
432 | "value": {
433 | "classNames": "has-warning vitals-warning"
434 | }
435 | }
436 | }
437 | }
438 | }
439 | },
440 | {
441 | "conditions": {
442 | "vitals$temperature$value": {
443 | "greater": 37.27777777777778
444 | },
445 | "vitals$temperature$measure": {
446 | "is": "C"
447 | }
448 | },
449 | "event": {
450 | "type": "uiAppend",
451 | "params": {
452 | "vitals": {
453 | "temperature": {
454 | "value": {
455 | "classNames": "has-error vitals-danger"
456 | }
457 | }
458 | }
459 | }
460 | }
461 | },
462 | {
463 | "conditions": {
464 | "vitals$temperature$value": {
465 | "greater": 36.55555555555556,
466 | "lessEq": 37.27777777777778
467 | },
468 | "vitals$temperature$measure": {
469 | "is": "C"
470 | }
471 | },
472 | "event": {
473 | "type": "uiAppend",
474 | "params": {
475 | "vitals": {
476 | "temperature": {
477 | "value": {
478 | "classNames": "has-success vitals-success"
479 | }
480 | }
481 | }
482 | }
483 | }
484 | },
485 | {
486 | "conditions": {
487 | "vitals$temperature$value": {
488 | "lessEq": 36.55555555555556
489 | },
490 | "vitals$temperature$measure": {
491 | "is": "C"
492 | }
493 | },
494 | "event": {
495 | "type": "uiAppend",
496 | "params": {
497 | "vitals": {
498 | "temperature": {
499 | "value": {
500 | "classNames": "has-warning vitals-warning"
501 | }
502 | }
503 | }
504 | }
505 | }
506 | },
507 | {
508 | "conditions": {
509 | "vitals$headCircum$value": {
510 | "greater": 55
511 | },
512 | "vitals$headCircum$measure": {
513 | "is": "In"
514 | }
515 | },
516 | "event": {
517 | "type": "uiAppend",
518 | "params": {
519 | "vitals": {
520 | "headCircum": {
521 | "value": {
522 | "classNames": "has-error vitals-danger"
523 | }
524 | }
525 | }
526 | }
527 | }
528 | },
529 | {
530 | "conditions": {
531 | "vitals$headCircum$value": {
532 | "greater": 25,
533 | "lessEq": 55
534 | },
535 | "vitals$headCircum$measure": {
536 | "is": "In"
537 | }
538 | },
539 | "event": {
540 | "type": "uiAppend",
541 | "params": {
542 | "vitals": {
543 | "headCircum": {
544 | "value": {
545 | "classNames": "has-success vitals-success"
546 | }
547 | }
548 | }
549 | }
550 | }
551 | },
552 | {
553 | "conditions": {
554 | "vitals$headCircum$value": {
555 | "lessEq": 25
556 | },
557 | "vitals$headCircum$measure": {
558 | "is": "In"
559 | }
560 | },
561 | "event": {
562 | "type": "uiAppend",
563 | "params": {
564 | "vitals": {
565 | "headCircum": {
566 | "value": {
567 | "classNames": "has-warning vitals-warning"
568 | }
569 | }
570 | }
571 | }
572 | }
573 | },
574 | {
575 | "conditions": {
576 | "vitals$headCircum$value": {
577 | "greater": 139.7
578 | },
579 | "vitals$headCircum$measure": {
580 | "is": "cms"
581 | }
582 | },
583 | "event": {
584 | "type": "uiAppend",
585 | "params": {
586 | "vitals": {
587 | "headCircum": {
588 | "value": {
589 | "classNames": "has-error vitals-danger"
590 | }
591 | }
592 | }
593 | }
594 | }
595 | },
596 | {
597 | "conditions": {
598 | "vitals$headCircum$value": {
599 | "greater": 63.5,
600 | "lessEq": 139.7
601 | },
602 | "vitals$headCircum$measure": {
603 | "is": "cms"
604 | }
605 | },
606 | "event": {
607 | "type": "uiAppend",
608 | "params": {
609 | "vitals": {
610 | "headCircum": {
611 | "value": {
612 | "classNames": "has-success vitals-success"
613 | }
614 | }
615 | }
616 | }
617 | }
618 | },
619 | {
620 | "conditions": {
621 | "vitals$headCircum$value": {
622 | "lessEq": 63.5
623 | },
624 | "vitals$headCircum$measure": {
625 | "is": "cms"
626 | }
627 | },
628 | "event": {
629 | "type": "uiAppend",
630 | "params": {
631 | "vitals": {
632 | "headCircum": {
633 | "value": {
634 | "classNames": "has-warning vitals-warning"
635 | }
636 | }
637 | }
638 | }
639 | }
640 | },
641 | {
642 | "conditions": {
643 | "vitals$bloodPressure$standing$systolic": {
644 | "greater": 139
645 | }
646 | },
647 | "event": {
648 | "type": "uiAppend",
649 | "params": {
650 | "vitals": {
651 | "bloodPressure": {
652 | "standing": {
653 | "systolic": {
654 | "classNames": "has-error vitals-danger"
655 | }
656 | }
657 | }
658 | }
659 | }
660 | }
661 | },
662 | {
663 | "conditions": {
664 | "vitals$bloodPressure$standing$systolic": {
665 | "greater": 120,
666 | "lessEq": 139
667 | }
668 | },
669 | "event": {
670 | "type": "uiAppend",
671 | "params": {
672 | "vitals": {
673 | "bloodPressure": {
674 | "standing": {
675 | "systolic": {
676 | "classNames": "has-success vitals-success"
677 | }
678 | }
679 | }
680 | }
681 | }
682 | }
683 | },
684 | {
685 | "conditions": {
686 | "vitals$bloodPressure$standing$systolic": {
687 | "lessEq": 120
688 | }
689 | },
690 | "event": {
691 | "type": "uiAppend",
692 | "params": {
693 | "vitals": {
694 | "bloodPressure": {
695 | "standing": {
696 | "systolic": {
697 | "classNames": "has-warning vitals-warning"
698 | }
699 | }
700 | }
701 | }
702 | }
703 | }
704 | },
705 | {
706 | "conditions": {
707 | "vitals$bloodPressure$standing$dystolic": {
708 | "greater": 89
709 | }
710 | },
711 | "event": {
712 | "type": "uiAppend",
713 | "params": {
714 | "vitals": {
715 | "bloodPressure": {
716 | "standing": {
717 | "dystolic": {
718 | "classNames": "has-error vitals-danger"
719 | }
720 | }
721 | }
722 | }
723 | }
724 | }
725 | },
726 | {
727 | "conditions": {
728 | "vitals$bloodPressure$standing$dystolic": {
729 | "greater": 80,
730 | "lessEq": 89
731 | }
732 | },
733 | "event": {
734 | "type": "uiAppend",
735 | "params": {
736 | "vitals": {
737 | "bloodPressure": {
738 | "standing": {
739 | "dystolic": {
740 | "classNames": "has-success vitals-success"
741 | }
742 | }
743 | }
744 | }
745 | }
746 | }
747 | },
748 | {
749 | "conditions": {
750 | "vitals$bloodPressure$standing$dystolic": {
751 | "lessEq": 80
752 | }
753 | },
754 | "event": {
755 | "type": "uiAppend",
756 | "params": {
757 | "vitals": {
758 | "bloodPressure": {
759 | "standing": {
760 | "dystolic": {
761 | "classNames": "has-warning vitals-warning"
762 | }
763 | }
764 | }
765 | }
766 | }
767 | }
768 | },
769 | {
770 | "conditions": {
771 | "vitals$bloodPressure$standing$pulse": {
772 | "greater": 100
773 | }
774 | },
775 | "event": {
776 | "type": "uiAppend",
777 | "params": {
778 | "vitals": {
779 | "bloodPressure": {
780 | "standing": {
781 | "pulse": {
782 | "classNames": "has-error vitals-danger"
783 | }
784 | }
785 | }
786 | }
787 | }
788 | }
789 | },
790 | {
791 | "conditions": {
792 | "vitals$bloodPressure$standing$pulse": {
793 | "greater": 60,
794 | "lessEq": 100
795 | }
796 | },
797 | "event": {
798 | "type": "uiAppend",
799 | "params": {
800 | "vitals": {
801 | "bloodPressure": {
802 | "standing": {
803 | "pulse": {
804 | "classNames": "has-success vitals-success"
805 | }
806 | }
807 | }
808 | }
809 | }
810 | }
811 | },
812 | {
813 | "conditions": {
814 | "vitals$bloodPressure$standing$pulse": {
815 | "lessEq": 60
816 | }
817 | },
818 | "event": {
819 | "type": "uiAppend",
820 | "params": {
821 | "vitals": {
822 | "bloodPressure": {
823 | "standing": {
824 | "pulse": {
825 | "classNames": "has-warning vitals-warning"
826 | }
827 | }
828 | }
829 | }
830 | }
831 | }
832 | },
833 | {
834 | "conditions": {
835 | "vitals$bloodPressure$sitting$systolic": {
836 | "greater": 139
837 | }
838 | },
839 | "event": {
840 | "type": "uiAppend",
841 | "params": {
842 | "vitals": {
843 | "bloodPressure": {
844 | "sitting": {
845 | "systolic": {
846 | "classNames": "has-error vitals-danger"
847 | }
848 | }
849 | }
850 | }
851 | }
852 | }
853 | },
854 | {
855 | "conditions": {
856 | "vitals$bloodPressure$sitting$systolic": {
857 | "greater": 120,
858 | "lessEq": 139
859 | }
860 | },
861 | "event": {
862 | "type": "uiAppend",
863 | "params": {
864 | "vitals": {
865 | "bloodPressure": {
866 | "sitting": {
867 | "systolic": {
868 | "classNames": "has-success vitals-success"
869 | }
870 | }
871 | }
872 | }
873 | }
874 | }
875 | },
876 | {
877 | "conditions": {
878 | "vitals$bloodPressure$sitting$systolic": {
879 | "lessEq": 120
880 | }
881 | },
882 | "event": {
883 | "type": "uiAppend",
884 | "params": {
885 | "vitals": {
886 | "bloodPressure": {
887 | "sitting": {
888 | "systolic": {
889 | "classNames": "has-warning vitals-warning"
890 | }
891 | }
892 | }
893 | }
894 | }
895 | }
896 | },
897 | {
898 | "conditions": {
899 | "vitals$bloodPressure$sitting$dystolic": {
900 | "greater": 89
901 | }
902 | },
903 | "event": {
904 | "type": "uiAppend",
905 | "params": {
906 | "vitals": {
907 | "bloodPressure": {
908 | "sitting": {
909 | "dystolic": {
910 | "classNames": "has-error vitals-danger"
911 | }
912 | }
913 | }
914 | }
915 | }
916 | }
917 | },
918 | {
919 | "conditions": {
920 | "vitals$bloodPressure$sitting$dystolic": {
921 | "greater": 80,
922 | "lessEq": 89
923 | }
924 | },
925 | "event": {
926 | "type": "uiAppend",
927 | "params": {
928 | "vitals": {
929 | "bloodPressure": {
930 | "sitting": {
931 | "dystolic": {
932 | "classNames": "has-success vitals-success"
933 | }
934 | }
935 | }
936 | }
937 | }
938 | }
939 | },
940 | {
941 | "conditions": {
942 | "vitals$bloodPressure$sitting$dystolic": {
943 | "lessEq": 80
944 | }
945 | },
946 | "event": {
947 | "type": "uiAppend",
948 | "params": {
949 | "vitals": {
950 | "bloodPressure": {
951 | "sitting": {
952 | "dystolic": {
953 | "classNames": "has-warning vitals-warning"
954 | }
955 | }
956 | }
957 | }
958 | }
959 | }
960 | },
961 | {
962 | "conditions": {
963 | "vitals$bloodPressure$sitting$pulse": {
964 | "greater": 100
965 | }
966 | },
967 | "event": {
968 | "type": "uiAppend",
969 | "params": {
970 | "vitals": {
971 | "bloodPressure": {
972 | "sitting": {
973 | "pulse": {
974 | "classNames": "has-error vitals-danger"
975 | }
976 | }
977 | }
978 | }
979 | }
980 | }
981 | },
982 | {
983 | "conditions": {
984 | "vitals$bloodPressure$sitting$pulse": {
985 | "greater": 60,
986 | "lessEq": 100
987 | }
988 | },
989 | "event": {
990 | "type": "uiAppend",
991 | "params": {
992 | "vitals": {
993 | "bloodPressure": {
994 | "sitting": {
995 | "pulse": {
996 | "classNames": "has-success vitals-success"
997 | }
998 | }
999 | }
1000 | }
1001 | }
1002 | }
1003 | },
1004 | {
1005 | "conditions": {
1006 | "vitals$bloodPressure$sitting$pulse": {
1007 | "lessEq": 60
1008 | }
1009 | },
1010 | "event": {
1011 | "type": "uiAppend",
1012 | "params": {
1013 | "vitals": {
1014 | "bloodPressure": {
1015 | "sitting": {
1016 | "pulse": {
1017 | "classNames": "has-warning vitals-warning"
1018 | }
1019 | }
1020 | }
1021 | }
1022 | }
1023 | }
1024 | },
1025 | {
1026 | "conditions": {
1027 | "vitals$bloodPressure$supine$systolic": {
1028 | "greater": 139
1029 | }
1030 | },
1031 | "event": {
1032 | "type": "uiAppend",
1033 | "params": {
1034 | "vitals": {
1035 | "bloodPressure": {
1036 | "supine": {
1037 | "systolic": {
1038 | "classNames": "has-error vitals-danger"
1039 | }
1040 | }
1041 | }
1042 | }
1043 | }
1044 | }
1045 | },
1046 | {
1047 | "conditions": {
1048 | "vitals$bloodPressure$supine$systolic": {
1049 | "greater": 120,
1050 | "lessEq": 139
1051 | }
1052 | },
1053 | "event": {
1054 | "type": "uiAppend",
1055 | "params": {
1056 | "vitals": {
1057 | "bloodPressure": {
1058 | "supine": {
1059 | "systolic": {
1060 | "classNames": "has-success vitals-success"
1061 | }
1062 | }
1063 | }
1064 | }
1065 | }
1066 | }
1067 | },
1068 | {
1069 | "conditions": {
1070 | "vitals$bloodPressure$supine$systolic": {
1071 | "lessEq": 120
1072 | }
1073 | },
1074 | "event": {
1075 | "type": "uiAppend",
1076 | "params": {
1077 | "vitals": {
1078 | "bloodPressure": {
1079 | "supine": {
1080 | "systolic": {
1081 | "classNames": "has-warning vitals-warning"
1082 | }
1083 | }
1084 | }
1085 | }
1086 | }
1087 | }
1088 | },
1089 | {
1090 | "conditions": {
1091 | "vitals$bloodPressure$supine$dystolic": {
1092 | "greater": 89
1093 | }
1094 | },
1095 | "event": {
1096 | "type": "uiAppend",
1097 | "params": {
1098 | "vitals": {
1099 | "bloodPressure": {
1100 | "supine": {
1101 | "dystolic": {
1102 | "classNames": "has-error vitals-danger"
1103 | }
1104 | }
1105 | }
1106 | }
1107 | }
1108 | }
1109 | },
1110 | {
1111 | "conditions": {
1112 | "vitals$bloodPressure$supine$dystolic": {
1113 | "greater": 80,
1114 | "lessEq": 89
1115 | }
1116 | },
1117 | "event": {
1118 | "type": "uiAppend",
1119 | "params": {
1120 | "vitals": {
1121 | "bloodPressure": {
1122 | "supine": {
1123 | "dystolic": {
1124 | "classNames": "has-success vitals-success"
1125 | }
1126 | }
1127 | }
1128 | }
1129 | }
1130 | }
1131 | },
1132 | {
1133 | "conditions": {
1134 | "vitals$bloodPressure$supine$dystolic": {
1135 | "lessEq": 80
1136 | }
1137 | },
1138 | "event": {
1139 | "type": "uiAppend",
1140 | "params": {
1141 | "vitals": {
1142 | "bloodPressure": {
1143 | "supine": {
1144 | "dystolic": {
1145 | "classNames": "has-warning vitals-warning"
1146 | }
1147 | }
1148 | }
1149 | }
1150 | }
1151 | }
1152 | },
1153 | {
1154 | "conditions": {
1155 | "vitals$bloodPressure$supine$pulse": {
1156 | "greater": 100
1157 | }
1158 | },
1159 | "event": {
1160 | "type": "uiAppend",
1161 | "params": {
1162 | "vitals": {
1163 | "bloodPressure": {
1164 | "supine": {
1165 | "pulse": {
1166 | "classNames": "has-error vitals-danger"
1167 | }
1168 | }
1169 | }
1170 | }
1171 | }
1172 | }
1173 | },
1174 | {
1175 | "conditions": {
1176 | "vitals$bloodPressure$supine$pulse": {
1177 | "greater": 60,
1178 | "lessEq": 100
1179 | }
1180 | },
1181 | "event": {
1182 | "type": "uiAppend",
1183 | "params": {
1184 | "vitals": {
1185 | "bloodPressure": {
1186 | "supine": {
1187 | "pulse": {
1188 | "classNames": "has-success vitals-success"
1189 | }
1190 | }
1191 | }
1192 | }
1193 | }
1194 | }
1195 | },
1196 | {
1197 | "conditions": {
1198 | "vitals$bloodPressure$supine$pulse": {
1199 | "lessEq": 60
1200 | }
1201 | },
1202 | "event": {
1203 | "type": "uiAppend",
1204 | "params": {
1205 | "vitals": {
1206 | "bloodPressure": {
1207 | "supine": {
1208 | "pulse": {
1209 | "classNames": "has-warning vitals-warning"
1210 | }
1211 | }
1212 | }
1213 | }
1214 | }
1215 | }
1216 | },
1217 | {
1218 | "conditions": {
1219 | "vitals$weight$value": "exists",
1220 | "vitals$weight$measure": "exists",
1221 | "vitals$height$value": "exists",
1222 | "vitals$height$measure": "exists"
1223 | },
1224 | "event": {
1225 | "type": "calculateBMI"
1226 | }
1227 | }
1228 | ]
1229 | }
--------------------------------------------------------------------------------
/test/actions/uiAppend/nested/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "schema": {
3 | "required": [],
4 | "type": "object",
5 | "properties": {
6 | "vitals": {
7 | "required": [],
8 | "type": "object",
9 | "properties": {
10 | "weight": {
11 | "title": "Weight",
12 | "type": "object",
13 | "properties": {
14 | "value": {
15 | "title": "Weight",
16 | "type": "number"
17 | },
18 | "measure": {
19 | "type": "string",
20 | "default": "Lbs",
21 | "enum": [
22 | "Lbs",
23 | "Kgs"
24 | ]
25 | }
26 | }
27 | },
28 | "height": {
29 | "title": "Height",
30 | "type": "object",
31 | "properties": {
32 | "value": {
33 | "title": "Height",
34 | "type": "number"
35 | },
36 | "measure": {
37 | "type": "string",
38 | "default": "In",
39 | "enum": [
40 | "In",
41 | "ft",
42 | "cms"
43 | ]
44 | }
45 | }
46 | },
47 | "bmi": {
48 | "type": "number",
49 | "title": "BMI"
50 | },
51 | "oximetry": {
52 | "type": "number",
53 | "title": "Oxygen"
54 | },
55 | "fiO2": {
56 | "type": "number",
57 | "title": "FiO2"
58 | },
59 | "flow": {
60 | "type": "number",
61 | "title": "Flow"
62 | },
63 | "temperature": {
64 | "title": "Temperature",
65 | "type": "object",
66 | "properties": {
67 | "value": {
68 | "title": "Temperature",
69 | "type": "number"
70 | },
71 | "measure": {
72 | "type": "string",
73 | "default": "F",
74 | "enum": [
75 | "F",
76 | "C"
77 | ]
78 | }
79 | }
80 | },
81 | "headCircum": {
82 | "title": "Head Circum",
83 | "type": "object",
84 | "properties": {
85 | "value": {
86 | "title": "Head Circum",
87 | "type": "number"
88 | },
89 | "measure": {
90 | "type": "string",
91 | "default": "In",
92 | "enum": [
93 | "In",
94 | "cms"
95 | ]
96 | }
97 | }
98 | },
99 | "heartRate": {
100 | "type": "number",
101 | "title": "Heart Rate"
102 | },
103 | "glucose": {
104 | "type": "number",
105 | "title": "Glucose"
106 | },
107 | "respirationRate": {
108 | "title": "Respiration Rate",
109 | "type": "object",
110 | "properties": {
111 | "value": {
112 | "title": "Respiration Rate",
113 | "type": "number"
114 | },
115 | "measure": {
116 | "type": "string",
117 | "default": "Normal",
118 | "enum": [
119 | "Normal",
120 | "Shallow"
121 | ]
122 | }
123 | }
124 | },
125 | "bloodPressure": {
126 | "required": [],
127 | "type": "object",
128 | "properties": {
129 | "standing": {
130 | "required": [],
131 | "type": "object",
132 | "properties": {
133 | "systolic": {
134 | "type": "number",
135 | "title": "Systolic"
136 | },
137 | "dystolic": {
138 | "type": "number",
139 | "title": "Dystolic"
140 | },
141 | "location": {
142 | "type": "string",
143 | "title": "Location",
144 | "enum": [
145 | "Right Upper Arm",
146 | "Right Thigh",
147 | "Right Hand",
148 | "Right Arm",
149 | "Left Upper Arm",
150 | "Left Thigh",
151 | "Left Hand",
152 | "Left Arm",
153 | "Other"
154 | ]
155 | },
156 | "pulse": {
157 | "type": "number",
158 | "title": "Pulse"
159 | },
160 | "pulseRhythm": {
161 | "type": "string",
162 | "title": "Pulse Rhythm",
163 | "enum": [
164 | "Regular",
165 | "Irregular"
166 | ]
167 | }
168 | }
169 | },
170 | "sitting": {
171 | "required": [],
172 | "type": "object",
173 | "properties": {
174 | "systolic": {
175 | "type": "number",
176 | "title": "Systolic"
177 | },
178 | "dystolic": {
179 | "type": "number",
180 | "title": "Dystolic"
181 | },
182 | "location": {
183 | "type": "string",
184 | "title": "Location",
185 | "enum": [
186 | "Right Upper Arm",
187 | "Right Thigh",
188 | "Right Hand",
189 | "Right Arm",
190 | "Left Upper Arm",
191 | "Left Thigh",
192 | "Left Hand",
193 | "Left Arm",
194 | "Other"
195 | ]
196 | },
197 | "pulse": {
198 | "type": "number",
199 | "title": "Pulse"
200 | },
201 | "pulseRhythm": {
202 | "type": "string",
203 | "title": "Pulse Rhythm",
204 | "enum": [
205 | "Regular",
206 | "Irregular"
207 | ]
208 | }
209 | }
210 | },
211 | "supine": {
212 | "required": [],
213 | "type": "object",
214 | "properties": {
215 | "systolic": {
216 | "type": "number",
217 | "title": "Systolic"
218 | },
219 | "dystolic": {
220 | "type": "number",
221 | "title": "Dystolic"
222 | },
223 | "location": {
224 | "type": "string",
225 | "title": "Location",
226 | "enum": [
227 | "Right Upper Arm",
228 | "Right Thigh",
229 | "Right Hand",
230 | "Right Arm",
231 | "Left Upper Arm",
232 | "Left Thigh",
233 | "Left Hand",
234 | "Left Arm",
235 | "Other"
236 | ]
237 | },
238 | "pulse": {
239 | "type": "number",
240 | "title": "Pulse"
241 | },
242 | "pulseRhythm": {
243 | "type": "string",
244 | "title": "Pulse Rhythm",
245 | "enum": [
246 | "Regular",
247 | "Irregular"
248 | ]
249 | }
250 | }
251 | }
252 | }
253 | }
254 | }
255 | }
256 | }
257 | }
258 | }
--------------------------------------------------------------------------------
/test/actions/uiAppend/nested/uiSchema.json:
--------------------------------------------------------------------------------
1 | {
2 | "uiSchema": {
3 | "vitals": {
4 | "classNames": "col-md-12",
5 | "ui:order": [
6 | "weight",
7 | "height",
8 | "bmi",
9 | "oximetry",
10 | "fiO2",
11 | "flow",
12 | "temperature",
13 | "headCircum",
14 | "heartRate",
15 | "glucose",
16 | "respirationRate",
17 | "bloodPressure"
18 | ],
19 | "weight": {
20 | "classNames": "col-md-4",
21 | "value": {
22 | "classNames": "custom-field-label col-md-8"
23 | },
24 | "measure": {
25 | "classNames": "custom-field-label col-md-4",
26 | "ui:options": {
27 | "label": false
28 | }
29 | }
30 | },
31 | "height": {
32 | "classNames": "col-md-4",
33 | "value": {
34 | "classNames": "custom-field-label col-md-8"
35 | },
36 | "measure": {
37 | "classNames": "custom-field-label col-md-4",
38 | "ui:options": {
39 | "label": false
40 | }
41 | }
42 | },
43 | "bmi": {
44 | "classNames": "custom-field-label col-md-4"
45 | },
46 | "oximetry": {
47 | "classNames": "custom-field-label col-md-4"
48 | },
49 | "fiO2": {
50 | "classNames": "custom-field-label col-md-4"
51 | },
52 | "flow": {
53 | "classNames": "custom-field-label col-md-4"
54 | },
55 | "temperature": {
56 | "classNames": "col-md-4",
57 | "value": {
58 | "classNames": "custom-field-label col-md-8"
59 | },
60 | "measure": {
61 | "classNames": "custom-field-label col-md-4",
62 | "ui:options": {
63 | "label": false
64 | }
65 | }
66 | },
67 | "headCircum": {
68 | "classNames": "col-md-4",
69 | "value": {
70 | "classNames": "custom-field-label col-md-8"
71 | },
72 | "measure": {
73 | "classNames": "custom-field-label col-md-4",
74 | "ui:options": {
75 | "label": false
76 | }
77 | }
78 | },
79 | "heartRate": {
80 | "classNames": "custom-field-label col-md-4"
81 | },
82 | "glucose": {
83 | "classNames": "custom-field-label col-md-4"
84 | },
85 | "respirationRate": {
86 | "classNames": "col-md-4",
87 | "value": {
88 | "classNames": "custom-field-label col-md-8"
89 | },
90 | "measure": {
91 | "classNames": "custom-field-label col-md-4",
92 | "ui:options": {
93 | "label": false
94 | }
95 | }
96 | },
97 | "bloodPressure": {
98 | "classNames": "col-md-12",
99 | "standing": {
100 | "classNames": "col-md-12",
101 | "systolic": {
102 | "classNames": "custom-field-label col-md-3"
103 | },
104 | "dystolic": {
105 | "classNames": "custom-field-label col-md-3"
106 | },
107 | "location": {
108 | "classNames": "custom-field-label col-md-2"
109 | },
110 | "pulse": {
111 | "classNames": "custom-field-label col-md-3"
112 | },
113 | "pulseRhythm": {
114 | "classNames": "custom-field-label col-md-1"
115 | }
116 | },
117 | "sitting": {
118 | "classNames": "col-md-12",
119 | "systolic": {
120 | "classNames": "custom-field-label col-md-3"
121 | },
122 | "dystolic": {
123 | "classNames": "custom-field-label col-md-3"
124 | },
125 | "location": {
126 | "classNames": "custom-field-label col-md-2"
127 | },
128 | "pulse": {
129 | "classNames": "custom-field-label col-md-3"
130 | },
131 | "pulseRhythm": {
132 | "classNames": "custom-field-label col-md-1"
133 | }
134 | },
135 | "supine": {
136 | "classNames": "col-md-12",
137 | "systolic": {
138 | "classNames": "custom-field-label col-md-3"
139 | },
140 | "dystolic": {
141 | "classNames": "custom-field-label col-md-3"
142 | },
143 | "location": {
144 | "classNames": "custom-field-label col-md-2"
145 | },
146 | "pulse": {
147 | "classNames": "custom-field-label col-md-3"
148 | },
149 | "pulseRhythm": {
150 | "classNames": "custom-field-label col-md-1"
151 | }
152 | }
153 | }
154 | }
155 | }
156 | }
--------------------------------------------------------------------------------
/test/actions/uiAppend/uiAppend.nested.test.js:
--------------------------------------------------------------------------------
1 | import { schema } from "./nested/schema.json";
2 | import { uiSchema } from "./nested/uiSchema.json";
3 | import { rules } from "./nested/rules.json";
4 | import deepCopy from "deepcopy";
5 | import Engine from "json-rules-engine-simplified";
6 | import runRules from "../../../src/rulesRunner";
7 |
8 | test("updates uiSchema only of target field", () => {
9 | return runRules(schema, uiSchema, rules, Engine, {})({
10 | vitals: { bloodPressure: { sitting: { pulse: 100 } } },
11 | }).then(res => {
12 | let expectedUiSchema = deepCopy(uiSchema);
13 | expectedUiSchema.vitals.bloodPressure.sitting.pulse.classNames =
14 | "custom-field-label col-md-3 has-success vitals-success";
15 |
16 | expect(res.schema).toEqual(schema);
17 | expect(res.uiSchema).toEqual(expectedUiSchema);
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/test/actions/uiOverride.test.js:
--------------------------------------------------------------------------------
1 | import deepcopy from "deepcopy";
2 | import uiOverride from "../../src/actions/uiOverride";
3 |
4 | let origUiSchema = {
5 | "ui:order": ["bar", "foo"],
6 | lastName: {
7 | classNames: "col-md-1",
8 | },
9 | firstName: {
10 | "ui:disabled": false,
11 | num: 23,
12 | },
13 | };
14 |
15 | let origSchema = {
16 | properties: {
17 | lastName: { type: "string" },
18 | firstName: { type: "string" },
19 | },
20 | };
21 |
22 | let params = {
23 | "ui:order": ["lastName"],
24 | lastName: {
25 | classNames: "has-error",
26 | },
27 | firstName: {
28 | classNames: "col-md-6",
29 | "ui:disabled": true,
30 | num: 22,
31 | },
32 | };
33 |
34 | test("default values", () => {
35 | let schema = {};
36 | let uiSchema = {};
37 | uiOverride(params, schema, uiSchema);
38 | expect(schema).toEqual({});
39 | expect(uiSchema).toEqual(params);
40 | });
41 |
42 | test("override required section", () => {
43 | let schema = deepcopy(origSchema);
44 | let uiSchema = deepcopy(origUiSchema);
45 | uiOverride(params, schema, uiSchema);
46 | expect(schema).toEqual(origSchema);
47 | let expectedUiSchema = {
48 | "ui:order": ["lastName"],
49 | lastName: {
50 | classNames: "has-error",
51 | },
52 | firstName: {
53 | classNames: "col-md-6",
54 | "ui:disabled": true,
55 | num: 22,
56 | },
57 | };
58 | expect(uiSchema).toEqual(expectedUiSchema);
59 | });
60 |
--------------------------------------------------------------------------------
/test/actions/uiReplace.test.js:
--------------------------------------------------------------------------------
1 | import deepcopy from "deepcopy";
2 | import uiReplace from "../../src/actions/uiReplace";
3 |
4 | let origUiSchema = {
5 | lastName: {
6 | classNames: "col-md-1",
7 | },
8 | firstName: {
9 | arr: [1],
10 | "ui:disabled": false,
11 | num: 23,
12 | },
13 | nickName: {
14 | classNames: "col-md-12",
15 | },
16 | };
17 |
18 | let origSchema = {
19 | properties: {
20 | lastName: { type: "string" },
21 | firstName: { type: "string" },
22 | },
23 | };
24 |
25 | let params = {
26 | "ui:order": ["lastName"],
27 | lastName: {
28 | classNames: "has-error",
29 | },
30 | firstName: {
31 | classNames: "col-md-6",
32 | arr: [2],
33 | "ui:disabled": true,
34 | num: 22,
35 | },
36 | };
37 |
38 | test("default values", () => {
39 | let schema = {};
40 | let uiSchema = {};
41 | uiReplace(params, schema, uiSchema);
42 | expect(schema).toEqual({});
43 | expect(uiSchema).toEqual(params);
44 | });
45 |
46 | test("replace required section", () => {
47 | let schema = deepcopy(origSchema);
48 | let uiSchema = deepcopy(origUiSchema);
49 | uiReplace(params, schema, uiSchema);
50 | expect(schema).toEqual(origSchema);
51 | expect(uiSchema).toEqual(
52 | Object.assign(params, { nickName: origUiSchema.nickName })
53 | );
54 | });
55 |
--------------------------------------------------------------------------------
/test/actions/validateAction.nested.test.js:
--------------------------------------------------------------------------------
1 | import deepcopy from "deepcopy";
2 | import require from "../../src/actions/require";
3 | import validateAction from "../../src/actions/validateAction";
4 |
5 | let schema = {
6 | definitions: {
7 | address: {
8 | type: "object",
9 | properties: {
10 | street: { type: "string" },
11 | zip: { type: "string" },
12 | },
13 | },
14 | },
15 | properties: {
16 | title: { type: "string" },
17 | firstName: { type: "string" },
18 | address: { $ref: "#/definitions/address" },
19 | profile: {
20 | type: "object",
21 | properties: {
22 | age: { type: "number" },
23 | height: { type: "number" },
24 | },
25 | },
26 | email: {
27 | $ref: "https://example.com/schema/email.json",
28 | },
29 | },
30 | };
31 |
32 | test("validate nested field", () => {
33 | let resSchema = deepcopy(schema);
34 | require({ field: "address.zip" }, resSchema);
35 |
36 | expect(() =>
37 | validateAction(require, { field: "email.host" }, schema, {})
38 | ).toThrow();
39 | expect(() =>
40 | validateAction(require, { field: "profile.weight" }, schema, {})
41 | ).toThrow();
42 | expect(
43 | validateAction(require, { field: "profile.height" }, schema, {})
44 | ).toBeUndefined();
45 | });
46 |
--------------------------------------------------------------------------------
/test/actions/validateAction.test.js:
--------------------------------------------------------------------------------
1 | import uiOverride from "../../src/actions/uiOverride";
2 | import uiAppend from "../../src/actions/uiAppend";
3 | import { testInProd } from "../utils";
4 | import validateAction from "../../src/actions/validateAction";
5 | import uiReplace from "../../src/actions/uiReplace";
6 | import remove from "../../src/actions/remove";
7 | import require from "../../src/actions/require";
8 |
9 | let origUiSchema = {
10 | "ui:order": ["firstName"],
11 | lastName: {
12 | classNames: "col-md-1",
13 | },
14 | firstName: {
15 | "ui:disabled": false,
16 | num: 23,
17 | },
18 | };
19 |
20 | let origSchema = {
21 | properties: {
22 | lastName: { type: "string" },
23 | firstName: { type: "string" },
24 | },
25 | };
26 |
27 | function invalidParams(action, invalidParams) {
28 | return test(`${action.name} invalid params for detected`, () => {
29 | expect(() =>
30 | validateAction(action, invalidParams, origSchema, origUiSchema)
31 | ).toThrow();
32 | expect(
33 | testInProd(() =>
34 | validateAction(action, invalidParams, origSchema, origUiSchema)
35 | )
36 | ).toBeUndefined();
37 | });
38 | }
39 |
40 | function validParams(action, validParams) {
41 | return test(`${action.name} valid params for detected`, () => {
42 | expect(
43 | validateAction(action, validParams, origSchema, origUiSchema)
44 | ).toBeUndefined();
45 | expect(
46 | testInProd(() =>
47 | validateAction(action, validParams, origSchema, origUiSchema)
48 | )
49 | ).toBeUndefined();
50 | });
51 | }
52 |
53 | invalidParams(uiAppend, { firstname: { classNames: "col-md-12" } });
54 | validParams(uiAppend, { firstName: "col-md-12" });
55 |
56 | invalidParams(uiOverride, { firstname: { classNames: "col-md-12" } });
57 | validParams(uiOverride, { firstName: { classNames: "col-md-12" } });
58 |
59 | invalidParams(uiReplace, { firstname: { classNames: "col-md-12" } });
60 | validParams(uiReplace, { firstName: { classNames: "col-md-12" } });
61 |
62 | invalidParams(remove, { field: "firstname" });
63 | invalidParams(remove, { field: ["firstname"] });
64 | validParams(remove, { field: "firstName" });
65 | validParams(remove, { field: ["firstName"] });
66 |
67 | invalidParams(require, { field: "firstname" });
68 | invalidParams(require, { field: ["firstname"] });
69 | validParams(require, { field: "firstName" });
70 | validParams(require, { field: ["firstName"] });
71 |
--------------------------------------------------------------------------------
/test/actions/validateField.test.js:
--------------------------------------------------------------------------------
1 | import { validateFields } from "../../src/actions/validateAction";
2 | import { testInProd } from "./../utils";
3 |
4 | function checkValidParams(fetchFunction, params, schema, uiSchema) {
5 | let validator = validateFields("fakeAction", fetchFunction);
6 | return test("field valid params on schema", () => {
7 | expect(validator(params, schema, uiSchema)).toBeUndefined();
8 | });
9 | }
10 |
11 | function checkInValidParams(fetchFunction, params, schema, uiSchema) {
12 | let validator = validateFields("fakeAction", fetchFunction);
13 | return test("field inValid params on schema", () => {
14 | expect(() => validator(params, schema, uiSchema)).toThrow();
15 | expect(
16 | testInProd(() => validator(params, schema, uiSchema))
17 | ).toBeUndefined();
18 | });
19 | }
20 |
21 | let schema = {
22 | properties: {
23 | lastName: { type: "string" },
24 | firstName: { type: "string" },
25 | },
26 | };
27 |
28 | checkValidParams(() => ["lastName"], {}, schema);
29 | checkValidParams(() => "lastName", {}, schema);
30 | checkValidParams(() => "lastName", {}, schema);
31 | checkValidParams(() => ["lastName"], {}, schema);
32 |
33 | checkInValidParams(() => ["lastname"], {}, schema);
34 | checkInValidParams(() => "lastname", {}, schema);
35 | checkInValidParams(() => "lastname", {}, schema);
36 | checkInValidParams(() => ["lastname"], {}, schema);
37 |
38 | test("validate field construction", () => {
39 | expect(validateFields("fakeAction", () => [])).not.toBeUndefined();
40 | expect(validateFields("fakeAction", () => [])).not.toBeUndefined();
41 | expect(
42 | testInProd(() => validateFields("fakeAction", () => "a"))
43 | ).not.toBeUndefined();
44 | expect(() => validateFields("fakeAction")).toThrow();
45 | expect(() => validateFields("fakeAction", null)).toThrow();
46 | expect(
47 | testInProd(() => validateFields("fakeAction", undefined))
48 | ).toBeUndefined();
49 | expect(testInProd(() => validateFields("fakeAction"))).toBeUndefined();
50 | expect(testInProd(() => validateFields("fakeAction", null))).toBeUndefined();
51 | expect(
52 | testInProd(() => validateFields("fakeAction", undefined))
53 | ).toBeUndefined();
54 | });
55 |
56 | test("validate field checks for a function", () => {
57 | expect(validateFields("fakeAction", [])).not.toBeUndefined();
58 | expect(validateFields("fakeAction", () => [])).not.toBeUndefined();
59 | expect(
60 | testInProd(() => validateFields("fakeAction", () => "a"))
61 | ).not.toBeUndefined();
62 | expect(() => validateFields("fakeAction")).toThrow();
63 | expect(() => validateFields("fakeAction", null)).toThrow();
64 | expect(
65 | testInProd(() => validateFields("fakeAction", undefined))
66 | ).toBeUndefined();
67 | expect(testInProd(() => validateFields("fakeAction"))).toBeUndefined();
68 | expect(testInProd(() => validateFields("fakeAction", null))).toBeUndefined();
69 | expect(
70 | testInProd(() => validateFields("fakeAction", undefined))
71 | ).toBeUndefined();
72 | });
73 |
--------------------------------------------------------------------------------
/test/actions/validation.test.js:
--------------------------------------------------------------------------------
1 | import validateAction from "../../src/actions/validateAction";
2 | import execute from "../../src/actions";
3 |
4 | test("empty", () => {
5 | let emptyRules = {};
6 |
7 | expect(validateAction(emptyRules, {})).toBeUndefined();
8 | });
9 |
10 | test("rules with invalid actions", () => {
11 | let invalidRule = {
12 | conditions: {},
13 | event: {
14 | type: "swim",
15 | },
16 | };
17 |
18 | expect(() => execute(invalidRule.event)).toThrow();
19 | });
20 |
--------------------------------------------------------------------------------
/test/applyRules.render.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Form from "react-jsonschema-form";
3 | import Engine from "json-rules-engine-simplified";
4 | import applyRules from "../src";
5 | import sinon from "sinon";
6 | import Adapter from "enzyme-adapter-react-16";
7 | import { shallow, configure } from "enzyme";
8 |
9 | configure({ adapter: new Adapter() });
10 |
11 | const schema = {
12 | type: "object",
13 | title: "Encounter",
14 | required: [],
15 | properties: {
16 | firstName: { type: "string" },
17 | lastName: { type: "string" },
18 | name: { type: "string" },
19 | },
20 | };
21 |
22 | const RULES = [
23 | {
24 | conditions: {
25 | firstName: "empty",
26 | },
27 | event: {
28 | type: "remove",
29 | params: {
30 | field: ["lastName", "name"],
31 | },
32 | },
33 | },
34 | ];
35 |
36 | test("NO re render on same data", () => {
37 | let ResForm = applyRules(schema, {}, RULES, Engine)(Form);
38 | const renderSpy = sinon.spy(ResForm.prototype, "render");
39 | const wrapper = shallow();
40 |
41 | expect(renderSpy.calledOnce).toEqual(true);
42 |
43 | wrapper.setProps({ formData: { firstName: "A" } });
44 | expect(renderSpy.calledOnce).toEqual(true);
45 | });
46 |
47 | test("Re render on formData change", () => {
48 | let ResForm = applyRules(schema, {}, RULES, Engine)(Form);
49 | const renderSpy = sinon.spy(ResForm.prototype, "render");
50 | const wrapper = shallow();
51 |
52 | return new Promise(resolve => setTimeout(resolve, 500))
53 | .then(() => {
54 | wrapper.setProps({ formData: { firstName: "An" } });
55 | return new Promise(resolve => setTimeout(resolve, 500));
56 | })
57 | .then(() => {
58 | expect(renderSpy.calledTwice).toEqual(true);
59 | });
60 | });
--------------------------------------------------------------------------------
/test/applyRules.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Form from "react-jsonschema-form";
3 | import Engine from "json-rules-engine-simplified";
4 | import applyRules from "../src";
5 | import sinon from "sinon";
6 | import Adapter from "enzyme-adapter-react-16";
7 | import { mount, configure } from "enzyme";
8 |
9 | configure({ adapter: new Adapter() });
10 |
11 | const schema = {
12 | type: "object",
13 | properties: {
14 | firstName: { type: "string" },
15 | lastName: { type: "string" },
16 | name: { type: "string" },
17 | },
18 | };
19 |
20 | const RULES = [
21 | {
22 | conditions: {
23 | firstName: "empty",
24 | },
25 | event: {
26 | type: "remove",
27 | params: {
28 | field: ["lastName", "name"],
29 | },
30 | },
31 | },
32 | ];
33 |
34 | test("Re render on rule change", () => {
35 | let ResForm = applyRules(schema, {}, RULES, Engine)(Form);
36 |
37 | const renderSpy = sinon.spy(ResForm.prototype, "render");
38 | const shouldComponentUpdateSpy = sinon.spy(
39 | ResForm.prototype,
40 | "shouldComponentUpdate"
41 | );
42 | const handleChangeSpy = sinon.spy(ResForm.prototype, "handleChange");
43 | const updateConfSpy = sinon.spy(ResForm.prototype, "updateConf");
44 | const setStateSpy = sinon.spy(ResForm.prototype, "setState");
45 |
46 | const wrapper = mount();
47 |
48 | expect(renderSpy.calledOnce).toEqual(true);
49 | expect(updateConfSpy.calledOnce).toEqual(true);
50 |
51 | wrapper
52 | .find("#root_firstName")
53 | .find("input")
54 | .simulate("change", { target: { value: "" } });
55 | expect(renderSpy.calledOnce).toEqual(true);
56 |
57 | return new Promise(resolve => setTimeout(resolve, 1000)).then(() => {
58 | expect(handleChangeSpy.calledOnce).toEqual(true);
59 | expect(setStateSpy.calledOnce).toEqual(true);
60 | expect(shouldComponentUpdateSpy.calledOnce).toEqual(true);
61 |
62 | expect(updateConfSpy.calledTwice).toEqual(true);
63 | expect(renderSpy.calledTwice).toEqual(true);
64 | });
65 | });
66 |
67 | test("onChange called with corrected schema", () => {
68 | let ResForm = applyRules(schema, {}, RULES, Engine)(Form);
69 | const changed = sinon.spy(() => {});
70 | const wrapper = mount(
71 |
72 | );
73 |
74 | wrapper
75 | .find("#root_firstName")
76 | .find("input")
77 | .simulate("change", { target: { value: "" } });
78 |
79 | return new Promise(resolve => setTimeout(resolve, 500)).then(() => {
80 | const expSchema = {
81 | type: "object",
82 | properties: {
83 | firstName: { type: "string" },
84 | },
85 | };
86 |
87 | expect(changed.calledOnce).toEqual(true);
88 | expect(changed.getCall(0).args[0].schema).toEqual(expSchema);
89 | });
90 | });
91 |
--------------------------------------------------------------------------------
/test/applyRules.validation.test.js:
--------------------------------------------------------------------------------
1 | import applyRules from "../src";
2 | import Engine from "json-rules-engine-simplified";
3 | import sinon from "sinon";
4 | import { testInProd } from "./utils";
5 |
6 | let SCHEMA = {
7 | properties: {
8 | firstName: { type: "string" },
9 | lastName: { type: "string" },
10 | name: { type: "string" },
11 | },
12 | };
13 |
14 | const formWithRules = rules => {
15 | try {
16 | applyRules(SCHEMA, {}, rules, Engine);
17 | } catch (error) {
18 | console.log(error);
19 | }
20 | };
21 |
22 | test("validation on creation", () => {
23 | expect(() => applyRules(SCHEMA, {}, [{}], Engine)).toThrow();
24 | expect(() =>
25 | applyRules(SCHEMA, {}, [{ conditions: "some" }], Engine)
26 | ).toThrow();
27 | });
28 |
29 | test("validation with PropTypes", () => {
30 | let consoleErrorSpy = sinon.spy(console, "error");
31 | // order is a string
32 | formWithRules([
33 | {
34 | conditions: { lastName: "empty" },
35 | order: "1",
36 | event: { type: "remove", params: { field: "name" } },
37 | },
38 | ]);
39 | // type is a number
40 | expect(consoleErrorSpy.calledOnce).toEqual(true);
41 | formWithRules([
42 | {
43 | conditions: { lastName: "empty" },
44 | order: 1,
45 | event: { type: 1, params: { field: "name" } },
46 | },
47 | ]);
48 | expect(consoleErrorSpy.calledTwice).toEqual(true);
49 | // Everything is fine, console log was not called
50 | formWithRules([
51 | {
52 | conditions: { lastName: "empty" },
53 | order: 1,
54 | event: { type: "remove", params: { field: "name" } },
55 | },
56 | ]);
57 | expect(consoleErrorSpy.calledTwice).toEqual(true);
58 | consoleErrorSpy.restore();
59 | });
60 |
61 | test("validation PropTypes ignored in prod", () => {
62 | let consoleSpy = sinon.spy(console, "error");
63 | testInProd(() =>
64 | formWithRules([
65 | { conditions: { lastName: "empty" }, order: "1", event: { type: "1" } },
66 | ])
67 | );
68 | expect(consoleSpy.calledOnce).toEqual(false);
69 | testInProd(() =>
70 | formWithRules([
71 | { conditions: { lastName: "empty" }, order: 1, event: { type: 1 } },
72 | ])
73 | );
74 | expect(consoleSpy.calledTwice).toEqual(false);
75 | testInProd(() =>
76 | formWithRules([
77 | { conditions: { lastName: "empty" }, order: 1, event: { type: "1" } },
78 | ])
79 | );
80 | expect(consoleSpy.calledTwice).toEqual(false);
81 | consoleSpy.restore();
82 | });
83 |
--------------------------------------------------------------------------------
/test/calculatedField.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Form from "react-jsonschema-form";
3 | import Engine from "json-rules-engine-simplified";
4 | import applyRules from "../src";
5 | import sinon from "sinon";
6 | import Adapter from "enzyme-adapter-react-16";
7 | import { mount, configure } from "enzyme";
8 |
9 | configure({ adapter: new Adapter() });
10 |
11 | const SCHEMA = {
12 | type: "object",
13 | properties: {
14 | a: { type: "number" },
15 | b: { type: "number" },
16 | sum: { type: "number" },
17 | },
18 | };
19 |
20 | const RULES = [
21 | {
22 | conditions: {
23 | a: { greater: 0 },
24 | },
25 | event: {
26 | type: "sum",
27 | },
28 | },
29 | ];
30 |
31 | const EXTRA_ACTIONS = {
32 | sum: (params, schema, uiSchema, formData) => {
33 | formData.sum = formData.a + formData.b;
34 | },
35 | };
36 |
37 | test("formData has calculated field specified", () => {
38 | let ResForm = applyRules(SCHEMA, {}, RULES, Engine, EXTRA_ACTIONS)(Form);
39 | const renderSpy = sinon.spy(ResForm.prototype, "render");
40 |
41 | mount();
42 | expect(renderSpy.calledOnce).toEqual(true);
43 |
44 | return new Promise(resolve => setTimeout(resolve, 1000)).then(() => {
45 | expect(renderSpy.calledTwice).toEqual(true);
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/test/issues/34.test.js:
--------------------------------------------------------------------------------
1 | import rulesRunner from "../../src/rulesRunner";
2 | import Engine from "json-rules-engine-simplified";
3 |
4 | function populateField(field, val, formData) {
5 | let separator = field.indexOf(".");
6 | if (separator === -1) {
7 | return (formData[field] = val);
8 | } else {
9 | let parentField = field.substr(0, separator);
10 | return populateField(
11 | field.substr(separator + 1),
12 | val,
13 | formData[parentField]
14 | );
15 | }
16 | }
17 |
18 | const extraActions = {
19 | populate: function({ options }, schema, uiSchema, formData) {
20 | Object.keys(options).forEach(field =>
21 | populateField(field, options[field], formData)
22 | );
23 | },
24 | };
25 |
26 | const origSchema = {
27 | type: "object",
28 | properties: {
29 | registration: {
30 | type: "object",
31 | properties: {
32 | firstName: { type: "string" },
33 | lastName: { type: "string" },
34 | gender: { type: "string" },
35 | email: { type: "string" },
36 | password: { type: "string" },
37 | confirmPassword: { type: "string" },
38 | },
39 | },
40 | address: {
41 | type: "object",
42 | properties: {
43 | zip: { type: "string" },
44 | },
45 | },
46 | },
47 | };
48 |
49 | let origUiSchema = {};
50 |
51 | const hideNonRelevant = {
52 | title: "Rule #2",
53 | description:
54 | "This hides Address, Email, Gender and the Password fields until First Name and Last Name have a value",
55 | conditions: {
56 | and: [
57 | {
58 | or: [
59 | {
60 | "registration.firstName": "empty",
61 | },
62 | {
63 | "registration.lastName": "empty",
64 | },
65 | ],
66 | },
67 | ],
68 | },
69 | event: {
70 | type: "remove",
71 | params: {
72 | field: [
73 | "address",
74 | "registration.gender",
75 | "registration.email",
76 | "registration.password",
77 | "registration.confirmPassword",
78 | ],
79 | },
80 | },
81 | };
82 |
83 | const fillDefaults = {
84 | title: "Rule #3",
85 | description: "prefills firstName",
86 | conditions: {
87 | and: [
88 | {
89 | "registration.firstName": {
90 | equal: "Barry",
91 | },
92 | },
93 | ],
94 | },
95 | event: {
96 | params: {
97 | field: ["registration.lastName"],
98 | options: {
99 | "registration.lastName": "White",
100 | "registration.gender": "Male",
101 | },
102 | },
103 | type: "populate",
104 | },
105 | };
106 |
107 | test("Direct rule", () => {
108 | let rules = [hideNonRelevant, fillDefaults];
109 | let runRules = rulesRunner(
110 | origSchema,
111 | origUiSchema,
112 | rules,
113 | Engine,
114 | extraActions
115 | );
116 |
117 | return runRules({
118 | registration: { firstName: "Barry" },
119 | }).then(({ schema, uiSchema, formData }) => {
120 | let expectedSchema = {
121 | firstName: { type: "string" },
122 | lastName: { type: "string" },
123 | };
124 | expect(schema.properties.registration).not.toEqual(expectedSchema);
125 | expect(uiSchema).toEqual({});
126 | expect(formData).toEqual({
127 | registration: { firstName: "Barry", lastName: "White", gender: "Male" },
128 | });
129 | });
130 | });
131 |
132 | test("Opposite rule", () => {
133 | let rules = [fillDefaults, hideNonRelevant];
134 | let runRules = rulesRunner(
135 | origSchema,
136 | origUiSchema,
137 | rules,
138 | Engine,
139 | extraActions
140 | );
141 | return runRules({
142 | registration: { firstName: "Barry" },
143 | }).then(({ schema, uiSchema, formData }) => {
144 | expect(schema.properties.registration).toEqual(
145 | origSchema.properties.registration
146 | );
147 | expect(uiSchema).toEqual({});
148 | expect(formData).toEqual({
149 | registration: { firstName: "Barry", lastName: "White", gender: "Male" },
150 | });
151 | });
152 | });
153 |
154 | test("Opposite rule with order", () => {
155 | let rules = [
156 | Object.assign({}, hideNonRelevant, { order: 1 }),
157 | Object.assign({}, fillDefaults, { order: 0 }),
158 | ];
159 | let runRules = rulesRunner(
160 | origSchema,
161 | origUiSchema,
162 | rules,
163 | Engine,
164 | extraActions
165 | );
166 | return runRules({
167 | registration: { firstName: "Barry" },
168 | }).then(({ schema, uiSchema, formData }) => {
169 | expect(schema.properties.registration).toEqual(
170 | origSchema.properties.registration
171 | );
172 | expect(uiSchema).toEqual({});
173 | expect(formData).toEqual({
174 | registration: { firstName: "Barry", lastName: "White", gender: "Male" },
175 | });
176 | });
177 | });
178 |
--------------------------------------------------------------------------------
/test/issues/35.test.js:
--------------------------------------------------------------------------------
1 | import remove from "../../src/actions/remove";
2 | import deepcopy from "deepcopy";
3 | import { findRelSchemaAndField } from "../../src/utils";
4 |
5 | let origSchemaWithRef = {
6 | definitions: {
7 | address: {
8 | properties: {
9 | street: { type: "string" },
10 | zip: { type: "string" },
11 | },
12 | },
13 | },
14 | properties: {
15 | title: { type: "string" },
16 | firstName: { type: "string" },
17 | address: { $ref: "#/definitions/address" },
18 | },
19 | };
20 |
21 | let originUiSchemaWithRef = {
22 | title: {},
23 | firstName: {},
24 | address: {},
25 | };
26 |
27 | test("remove field which contains ref", () => {
28 | let schemaWithRef = deepcopy(origSchemaWithRef);
29 | let uiSchemaWithRef = deepcopy(originUiSchemaWithRef);
30 | remove({ field: ["address"] }, schemaWithRef, uiSchemaWithRef);
31 |
32 | let schemaWithoutAddress = {
33 | definitions: {
34 | address: {
35 | properties: {
36 | street: { type: "string" },
37 | zip: { type: "string" },
38 | },
39 | },
40 | },
41 | properties: {
42 | title: { type: "string" },
43 | firstName: { type: "string" },
44 | },
45 | };
46 |
47 | expect(findRelSchemaAndField("address", origSchemaWithRef)).toEqual({
48 | field: "address",
49 | schema: origSchemaWithRef,
50 | });
51 |
52 | expect(schemaWithRef).toEqual(schemaWithoutAddress);
53 |
54 | let uiSchemaWithoutAddress = {
55 | title: {},
56 | firstName: {},
57 | };
58 | expect(uiSchemaWithRef).toEqual(uiSchemaWithoutAddress);
59 | });
60 |
--------------------------------------------------------------------------------
/test/issues/38.test.js:
--------------------------------------------------------------------------------
1 | import rulesRunner from "../../src/rulesRunner";
2 | import Engine from "json-rules-engine-simplified";
3 |
4 | let schema = {
5 | properties: {
6 | firstName: { type: "string" },
7 | lastName: { type: "string" },
8 | email: { type: "string" },
9 | },
10 | };
11 |
12 | let rules = [
13 | {
14 | conditions: {
15 | firstName: "empty",
16 | },
17 | event: [
18 | {
19 | type: "remove",
20 | params: { field: "email" },
21 | },
22 | {
23 | type: "uiAppend",
24 | params: { lastName: { classNames: "danger" } },
25 | },
26 | ],
27 | },
28 | ];
29 |
30 | test("array works", () => {
31 | let expSchema = {
32 | properties: {
33 | firstName: { type: "string" },
34 | lastName: { type: "string" },
35 | },
36 | };
37 | let expUiSchema = {
38 | lastName: {
39 | classNames: "danger",
40 | },
41 | };
42 |
43 | let runRules = rulesRunner(schema, {}, rules, Engine);
44 | return runRules({}).then(({ schema, uiSchema }) => {
45 | expect(schema).toEqual(expSchema);
46 | expect(uiSchema).toEqual(expUiSchema);
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/test/issues/43.test.js:
--------------------------------------------------------------------------------
1 | import remove from "../../src/actions/remove";
2 |
3 | const schema = {
4 | type: "object",
5 | properties: {
6 | firstName: {
7 | type: "string",
8 | title: "First Name",
9 | },
10 | lastName: {
11 | type: "string",
12 | title: "Last Name",
13 | },
14 | },
15 | };
16 | const uiSchema = {
17 | "ui:order": ["firstName", "lastName"],
18 | };
19 |
20 | test("remove updates uiOrder ", () => {
21 | remove({ field: ["firstName"] }, schema, uiSchema);
22 |
23 | expect(uiSchema["ui:order"]).toEqual(["lastName"]);
24 | });
25 |
--------------------------------------------------------------------------------
/test/issues/44.test.js:
--------------------------------------------------------------------------------
1 | import { deepEquals } from "react-jsonschema-form/lib/utils";
2 |
3 | test("Deep equal on large filed", () => {
4 | let a = {
5 | name: "some",
6 | func: () => true,
7 | };
8 |
9 | let b = {
10 | name: "some",
11 | func: () => true,
12 | };
13 |
14 | expect(deepEquals(a, b)).toBeTruthy();
15 | });
16 |
--------------------------------------------------------------------------------
/test/issues/46.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Form from "react-jsonschema-form";
3 | import Engine from "json-rules-engine-simplified";
4 | import applyRules from "../../src";
5 | import sinon from "sinon";
6 | import Adapter from "enzyme-adapter-react-16";
7 | import { mount, configure } from "enzyme";
8 |
9 | configure({ adapter: new Adapter() });
10 |
11 | let schema = {
12 | type: "object",
13 | properties: {
14 | firstName: { type: "string" },
15 | lastName: { type: "string" },
16 | },
17 | };
18 |
19 | let uiSchema = {
20 | firstName: {},
21 | lastName: {},
22 | };
23 |
24 | test("no exception on formData undefined", () => {
25 | let ResForm = applyRules(
26 | schema,
27 | uiSchema,
28 | [
29 | {
30 | conditions: { firstName: { is: "An" } },
31 | event: { type: "remove", params: { field: "lastName" } },
32 | },
33 | ],
34 | Engine
35 | )(Form);
36 |
37 | const updateConfSpy = sinon.spy(ResForm.prototype, "updateConf");
38 |
39 | mount();
40 | expect(updateConfSpy.calledOnce).toEqual(true);
41 | expect(updateConfSpy.threw()).toEqual(false);
42 | });
43 |
--------------------------------------------------------------------------------
/test/issues/47.test.js:
--------------------------------------------------------------------------------
1 | import Form from "react-jsonschema-form";
2 | import Engine from "json-rules-engine-simplified";
3 | import applyRules from "../../src";
4 |
5 | let schema = {
6 | type: "object",
7 | properties: {
8 | firstName: { type: "string" },
9 | lastName: { type: "string" },
10 | },
11 | };
12 |
13 | let uiSchema = {
14 | firstName: {},
15 | lastName: {},
16 | };
17 |
18 | let invalidNickName = [
19 | {
20 | conditions: { firstName: { is: "An" } },
21 | event: { type: "remove", params: { field: "nickName" } },
22 | },
23 | ];
24 |
25 | test("validation happens on initial render", () => {
26 | expect(() =>
27 | applyRules(schema, uiSchema, invalidNickName, Engine)(Form)
28 | ).toThrow();
29 | });
30 |
31 | let invalidAction = [
32 | {
33 | conditions: { firstName: { is: "An" } },
34 | event: { type: "jump" },
35 | },
36 | ];
37 |
38 | test("validation triggered on invalid action", () => {
39 | expect(() =>
40 | applyRules(schema, uiSchema, invalidAction, Engine)(Form)
41 | ).toThrow();
42 | });
43 |
--------------------------------------------------------------------------------
/test/issues/53.test.js:
--------------------------------------------------------------------------------
1 | import Engine from "json-rules-engine-simplified";
2 |
3 | import rulesRunner from "../../src/rulesRunner";
4 |
5 | const rules = [
6 | {
7 | event: { type: "remove", params: { field: "two" } },
8 | conditions: { not: { one: { equal: true } } },
9 | },
10 | ];
11 |
12 | const schema = {
13 | type: "object",
14 | required: ["one", "two", "three", "four"],
15 | properties: {
16 | one: { type: "boolean", title: "1. Should we show (2)?" },
17 | two: { type: "string", title: "2. Only shown if (1) is true" },
18 | three: { type: "string", title: "3. Always shown and required" },
19 | four: { type: "string", title: "4. Always shown and required" },
20 | },
21 | };
22 |
23 | const runRules = rulesRunner(schema, {}, rules, Engine);
24 |
25 | test("remove only single field", () => {
26 | return runRules({ one: false }).then(({ schema }) => {
27 | expect(schema.required).toEqual(["one", "three", "four"]);
28 | });
29 | });
30 |
31 | test("keeps original required", () => {
32 | return runRules({ one: true }).then(({ schema }) => {
33 | expect(schema.required).toEqual(["one", "two", "three", "four"]);
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/test/issues/59.test.js:
--------------------------------------------------------------------------------
1 | import Engine from "json-rules-engine-simplified";
2 | import applyRules from "../../src/applyRules";
3 |
4 | const rules = [
5 | {
6 | conditions: {
7 | hasBenefitsReference: { is: true },
8 | },
9 | event: [
10 | {
11 | type: "require",
12 | params: {
13 | field: ["hasBD2Reference", "BD2Reference"],
14 | },
15 | },
16 | ],
17 | },
18 | ];
19 |
20 | const schema = {
21 | type: "object",
22 | properties: {
23 | hasBenefitsReference: {
24 | title: "Do you have a Benefits Reference Number?",
25 | type: "boolean",
26 | },
27 | benefitsReference: {
28 | title: "Benefits Reference Number",
29 | type: "string",
30 | },
31 | hasBD2Reference: {
32 | title: "Do you have a BD2 Number?",
33 | type: "boolean",
34 | },
35 | BD2Reference: {
36 | title: "BD2 Number",
37 | type: "string",
38 | },
39 | },
40 | };
41 |
42 | test("Processes events with 2 events", () => {
43 | expect(applyRules(schema, {}, rules, Engine)).not.toBeUndefined();
44 | });
45 |
--------------------------------------------------------------------------------
/test/issues/61.test.js:
--------------------------------------------------------------------------------
1 | import rulesRunner from "../../src/rulesRunner";
2 | import Engine from "json-rules-engine-simplified";
3 |
4 | test("rulesRunner with own Engine instantiation", () => {
5 | let rules = [
6 | {
7 | conditions: { name: { not: "empty" } },
8 | event: "foo",
9 | },
10 | ];
11 |
12 | rulesRunner(
13 | // schema
14 | { properties: { name: { type: "string" } } },
15 | // ui
16 | {},
17 | // rules
18 | rules,
19 | new Engine(rules)
20 | )({}).then(() => {});
21 | });
22 |
--------------------------------------------------------------------------------
/test/rulesRuneer.test.js:
--------------------------------------------------------------------------------
1 | import rulesRuner from "../src/rulesRunner";
2 | import Engine from "json-rules-engine-simplified";
3 |
4 | let SCHEMA = {
5 | properties: {
6 | firstName: { type: "string" },
7 | lastName: { type: "string" },
8 | name: { type: "string" },
9 | },
10 | };
11 |
12 | test("executes single action", () => {
13 | let rules = [
14 | {
15 | conditions: { lastName: "empty" },
16 | event: {
17 | type: "remove",
18 | params: { field: "firstName" },
19 | },
20 | },
21 | {
22 | conditions: { lastName: "empty" },
23 | event: {
24 | type: "require",
25 | params: { field: "name" },
26 | },
27 | },
28 | ];
29 |
30 | let runRules = rulesRuner(SCHEMA, {}, rules, Engine);
31 |
32 | return runRules({}).then(({ schema }) => {
33 | let expectedSchema = {
34 | required: ["name"],
35 | properties: {
36 | lastName: schema.properties.lastName,
37 | name: schema.properties.name,
38 | },
39 | };
40 | expect(schema).toEqual(expectedSchema);
41 | });
42 | });
43 |
44 | test("executes multiple actions", () => {
45 | let rules = [
46 | {
47 | conditions: { lastName: "empty" },
48 | event: {
49 | type: "remove",
50 | params: { field: "firstName" },
51 | },
52 | },
53 | {
54 | conditions: { lastName: "empty" },
55 | event: {
56 | type: "require",
57 | params: { field: ["name"] },
58 | },
59 | },
60 | {
61 | conditions: { lastName: "empty" },
62 | event: {
63 | type: "uiReplace",
64 | params: { name: { classNames: "col-md-5" } },
65 | },
66 | },
67 | ];
68 |
69 | let runRules = rulesRuner(SCHEMA, {}, rules, Engine);
70 |
71 | return runRules({}).then(({ schema, uiSchema }) => {
72 | let expectedSchema = {
73 | required: ["name"],
74 | properties: {
75 | lastName: schema.properties.lastName,
76 | name: schema.properties.name,
77 | },
78 | };
79 | expect(schema).toEqual(expectedSchema);
80 | expect(uiSchema).toEqual({ name: { classNames: "col-md-5" } });
81 | });
82 | });
83 |
84 | test("ignored if no formData defined", () => {
85 | let rules = [
86 | {
87 | conditions: { lastName: "empty" },
88 | event: {
89 | type: "remove",
90 | params: { field: "firstName" },
91 | },
92 | },
93 | {
94 | conditions: { lastName: "empty" },
95 | event: {
96 | type: "require",
97 | params: { field: ["name"] },
98 | },
99 | },
100 | {
101 | conditions: { lastName: "empty" },
102 | event: {
103 | type: "uiReplace",
104 | params: { name: { classNames: "col-md-5" } },
105 | },
106 | },
107 | ];
108 |
109 | let runRules = rulesRuner(SCHEMA, {}, rules, Engine);
110 |
111 | return Promise.all([
112 | runRules(undefined),
113 | runRules(null),
114 | ]).then(([withUndef, withNull]) => {
115 | expect(withNull).toEqual({ schema: SCHEMA, uiSchema: {}, formData: null });
116 | expect(withUndef).toEqual({
117 | schema: SCHEMA,
118 | uiSchema: {},
119 | formData: undefined,
120 | });
121 | });
122 | });
123 |
--------------------------------------------------------------------------------
/test/runRules.normRules.test.js:
--------------------------------------------------------------------------------
1 | import { normRules } from "../src/rulesRunner";
2 |
3 | test("order from low to high ", () => {
4 | let unOrderedRules = [
5 | {
6 | conditions: "A",
7 | order: 1,
8 | },
9 | {
10 | conditions: "B",
11 | order: 0,
12 | },
13 | {
14 | conditions: "C",
15 | order: 2,
16 | },
17 | ];
18 |
19 | let orderedRules = [
20 | {
21 | conditions: "B",
22 | order: 0,
23 | },
24 | {
25 | conditions: "A",
26 | order: 1,
27 | },
28 | {
29 | conditions: "C",
30 | order: 2,
31 | },
32 | ];
33 |
34 | expect(normRules(unOrderedRules)).toEqual(orderedRules);
35 | });
36 |
37 | test("put without order as last ", () => {
38 | let unOrderedRules = [
39 | {
40 | conditions: "A",
41 | },
42 | {
43 | conditions: "B",
44 | order: 0,
45 | },
46 | {
47 | conditions: "D",
48 | },
49 | {
50 | conditions: "C",
51 | order: 2,
52 | },
53 | {
54 | conditions: "E",
55 | },
56 | {
57 | conditions: "F",
58 | order: 3,
59 | },
60 | ];
61 |
62 | let orderedRules = [
63 | {
64 | conditions: "B",
65 | order: 0,
66 | },
67 | {
68 | conditions: "C",
69 | order: 2,
70 | },
71 | {
72 | conditions: "F",
73 | order: 3,
74 | },
75 | {
76 | conditions: "A",
77 | },
78 | {
79 | conditions: "D",
80 | },
81 | {
82 | conditions: "E",
83 | },
84 | ];
85 |
86 | expect(normRules(unOrderedRules)).toEqual(orderedRules);
87 | });
88 |
89 | test("keep without order as last ", () => {
90 | let unOrderedRules = [
91 | {
92 | conditions: "A",
93 | },
94 | {
95 | conditions: "B",
96 | },
97 | {
98 | conditions: "C",
99 | order: 2,
100 | },
101 | ];
102 |
103 | let orderedRules = [
104 | {
105 | conditions: "C",
106 | order: 2,
107 | },
108 | {
109 | conditions: "A",
110 | },
111 | {
112 | conditions: "B",
113 | },
114 | ];
115 |
116 | expect(normRules(unOrderedRules)).toEqual(orderedRules);
117 | });
118 |
--------------------------------------------------------------------------------
/test/utils.findRelUiSchema.test.js:
--------------------------------------------------------------------------------
1 | import { findRelUiSchema } from "../src/utils";
2 |
3 | let uiSchema = {
4 | lastName: {
5 | classNames: "col-md-12",
6 | },
7 | age: {
8 | classNames: "col-md-3",
9 | },
10 | someAddress: {
11 | street: {
12 | className: "col-md-6",
13 | },
14 | zip: {
15 | className: "col-md-6",
16 | },
17 | },
18 | houses: {
19 | street: {
20 | className: "col-md-9",
21 | },
22 | zip: {
23 | className: "col-md-3",
24 | },
25 | },
26 | };
27 |
28 | test("extract relevant uiSchema for general field", () => {
29 | expect(findRelUiSchema("lastName", uiSchema)).toEqual(uiSchema);
30 | expect(findRelUiSchema("age", uiSchema)).toEqual(uiSchema);
31 | });
32 |
33 | test("extract relevant uiSchema for embedded field", () => {
34 | expect(findRelUiSchema("someAddress.street", uiSchema)).toEqual(
35 | uiSchema.someAddress
36 | );
37 | expect(findRelUiSchema("someAddress.zip", uiSchema)).toEqual(
38 | uiSchema.someAddress
39 | );
40 | });
41 |
42 | test("extract relevant uiSchema for embedded array field", () => {
43 | expect(findRelUiSchema("houses.street", uiSchema)).toEqual(uiSchema.houses);
44 | expect(findRelUiSchema("houses.zip", uiSchema)).toEqual(uiSchema.houses);
45 | });
46 |
--------------------------------------------------------------------------------
/test/utils.js:
--------------------------------------------------------------------------------
1 | export function testInProd(f) {
2 | process.env.NODE_ENV = "production";
3 | let res = f();
4 | process.env.NODE_ENV = "test";
5 | return res;
6 | }
7 |
--------------------------------------------------------------------------------
/test/utils.test.js:
--------------------------------------------------------------------------------
1 | import { findRelSchemaAndField, isDevelopment, toError } from "../src/utils";
2 | import { testInProd } from "./utils";
3 | import selectn from "selectn";
4 |
5 | let addressSchema = {
6 | properties: {
7 | street: { type: "string" },
8 | zip: { type: "string" },
9 | },
10 | };
11 |
12 | let schema = {
13 | definitions: {
14 | address: addressSchema,
15 | },
16 | properties: {
17 | lastName: { type: "string" },
18 | age: { type: "number" },
19 | someAddress: {
20 | $ref: "#/definitions/address",
21 | },
22 | houses: {
23 | type: "array",
24 | items: {
25 | $ref: "#/definitions/address",
26 | },
27 | },
28 | email: {
29 | $ref: "https://example.com/email.json",
30 | },
31 | },
32 | };
33 |
34 | test("isProduction", () => {
35 | expect(isDevelopment()).toBeTruthy();
36 | expect(testInProd(() => isDevelopment())).toBeFalsy();
37 | });
38 |
39 | test("error throws exception", () => {
40 | expect(() => toError("Yes")).toThrow();
41 | expect(testInProd(() => toError("Yes"))).toBeUndefined();
42 | });
43 |
44 | test("find rel schema with plain schema", () => {
45 | expect(findRelSchemaAndField("lastName", schema)).toEqual({
46 | field: "lastName",
47 | schema,
48 | });
49 | expect(findRelSchemaAndField("age", schema)).toEqual({
50 | field: "age",
51 | schema,
52 | });
53 | });
54 |
55 | test("find rel schema with ref object schema", () => {
56 | expect(findRelSchemaAndField("someAddress", schema)).toEqual({
57 | field: "someAddress",
58 | schema,
59 | });
60 | expect(findRelSchemaAndField("someAddress.street", schema)).toEqual({
61 | field: "street",
62 | schema: addressSchema,
63 | });
64 | });
65 |
66 | test("find rel schema with ref array object schema", () => {
67 | let { definitions: { address } } = schema;
68 | expect(findRelSchemaAndField("houses", schema)).toEqual({
69 | field: "houses",
70 | schema,
71 | });
72 | expect(findRelSchemaAndField("houses.street", schema)).toEqual({
73 | field: "street",
74 | schema: address,
75 | });
76 | });
77 |
78 | test("fail to find rel schema", () => {
79 | expect(() => findRelSchemaAndField("email.host", schema)).toThrow();
80 | expect(
81 | testInProd(() => findRelSchemaAndField("email.host", schema))
82 | ).toEqual({ field: "email.host", schema });
83 | });
84 |
85 | test("fail to find rel schema field", () => {
86 | expect(() => findRelSchemaAndField("email.protocol", schema)).toThrow();
87 | expect(
88 | testInProd(() => findRelSchemaAndField("email.protocol", schema))
89 | ).toEqual({ field: "email.protocol", schema });
90 | });
91 |
92 | test("invalid field", () => {
93 | expect(() => findRelSchemaAndField("lastName.protocol", schema)).toThrow();
94 | expect(
95 | testInProd(() => findRelSchemaAndField("lastName.protocol", schema))
96 | ).toEqual({ field: "lastName.protocol", schema });
97 | });
98 |
99 | test("selectn", () => {
100 | expect(selectn("firstName", { firstName: {} })).toEqual({});
101 | });
102 |
--------------------------------------------------------------------------------
/webpack.config.dist.js:
--------------------------------------------------------------------------------
1 | var webpack = require("webpack");
2 | var path = require("path");
3 |
4 | module.exports = {
5 | cache: true,
6 | context: __dirname + "/src",
7 | entry: {
8 | main: "./index.js"
9 | },
10 | output: {
11 | path: path.resolve(__dirname, 'dist'),
12 | publicPath: "/dist/",
13 | filename: "[name].js",
14 | library: "JSONSchemaForm",
15 | libraryTarget: "umd"
16 | },
17 | plugins: [
18 | new webpack.DefinePlugin({
19 | "process.env": {
20 | NODE_ENV: JSON.stringify("production")
21 | }
22 | })
23 | ],
24 | devtool: "source-map",
25 | externals: {
26 | react: {
27 | root: "React",
28 | commonjs: "react",
29 | commonjs2: "react",
30 | amd: "react"
31 | }
32 | },
33 | module: {
34 | loaders: [
35 | {
36 | test: /\.js$/,
37 | loaders: ["babel-loader"],
38 | }
39 | ]
40 | }
41 | };
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require("path");
2 | var webpack = require("webpack");
3 |
4 | module.exports = {
5 | devtool: "source-maps",
6 | entry: [
7 | "./playground/app",
8 | ],
9 | output: {
10 | path: path.join(__dirname, "build"),
11 | filename: "bundle.js",
12 | publicPath: "/static/"
13 | },
14 | plugins: [
15 | new webpack.HotModuleReplacementPlugin()
16 | ],
17 | module: {
18 | rules: [
19 | {
20 | test: /\.jsx?$/,
21 | include: [
22 | path.resolve(__dirname, "src"),
23 | path.resolve(__dirname, "playground")
24 | ],
25 | exclude: [
26 | path.resolve(__dirname, "node_modules"),
27 | ],
28 | use: {
29 | loader: 'babel-loader',
30 | }
31 | },
32 | {
33 | test: /\.css$/,
34 | include: [
35 | path.join(__dirname, "css"),
36 | path.join(__dirname, "playground"),
37 | path.join(__dirname, "node_modules"),
38 | ],
39 | use: [
40 | { loader: "style-loader" },
41 | { loader: "css-loader" }
42 | ]
43 | }
44 | ]
45 | },
46 | devServer: {
47 | contentBase: path.join(__dirname, "playground"),
48 | historyApiFallback: true,
49 | hot: true,
50 | lazy: false,
51 | noInfo: false,
52 | overlay: {
53 | warnings: true,
54 | errors: true
55 | }
56 | },
57 | };
58 |
--------------------------------------------------------------------------------
/webpack.config.prod.js:
--------------------------------------------------------------------------------
1 | var path = require("path");
2 | var webpack = require("webpack");
3 | var ExtractTextPlugin = require("extract-text-webpack-plugin");
4 |
5 | module.exports = {
6 | entry: path.join(__dirname, "playground/app"),
7 | output: {
8 | path: path.join(__dirname, "build"),
9 | filename: "bundle.js",
10 | publicPath: "/static/"
11 | },
12 | plugins: [
13 | new webpack.DefinePlugin({
14 | "process.env": {
15 | NODE_ENV: JSON.stringify("production")
16 | }
17 | })
18 | ],
19 | resolve: {
20 | modules: [
21 | "node_modules",
22 | path.resolve(__dirname, "app")
23 | ],
24 | extensions: [".js", ".jsx", ".css"],
25 | },
26 | module: {
27 | loaders: [
28 | {
29 | test: /\.jsx?$/,
30 | loader: "babel-loader",
31 | include: [
32 | path.join(__dirname, "src"),
33 | path.join(__dirname, "playground"),
34 | path.join(__dirname, "node_modules", "codemirror", "mode", "javascript"),
35 | ],
36 | },
37 | {
38 | test: /\.css$/,
39 | loader: ExtractTextPlugin.extract("css-loader"),
40 | include: [
41 | path.join(__dirname, "playground"),
42 | path.join(__dirname, "node_modules"),
43 | ],
44 | }
45 | ]
46 | }
47 | };
48 |
--------------------------------------------------------------------------------