├── .circleci
└── config.yml
├── .eslintrc.json
├── .gitignore
├── .npmignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── babel.config.js
├── demo
├── field-properties.js
├── index.html
└── index.js
├── package.json
├── scripts
├── clean-build.js
├── copy-css.js
└── generate-packages.js
├── setupTests.js
├── src
├── builder-store
│ ├── builder-reducer.js
│ ├── builder-store.js
│ └── index.js
├── component-picker
│ ├── component-picker.js
│ └── index.js
├── components-context
│ ├── components-context.js
│ └── index.js
├── constants
│ ├── constants.js
│ └── index.js
├── drop-target
│ ├── drop-target.js
│ └── index.js
├── field
│ ├── field.js
│ └── index.js
├── form-builder-layout
│ ├── form-builder-layout.js
│ └── index.js
├── form-builder
│ ├── form-builder.js
│ └── index.js
├── helpers
│ ├── create-export-schema.js
│ ├── create-initial-data.js
│ └── index.js
├── index.js
├── layout-context
│ ├── index.js
│ └── layout-context.js
├── mui-builder-mappers
│ ├── builder-mapper.js
│ ├── builder-template.js
│ ├── field-properties.js
│ ├── index.js
│ ├── picker-mapper.js
│ └── properties-mapper.js
├── pf4-builder-mappers
│ ├── builder-mapper.js
│ ├── builder-template.js
│ ├── field-properties.js
│ ├── index.js
│ ├── pf4-mapper-style.css
│ ├── picker-mapper.js
│ └── properties-mapper.js
├── picker-field
│ ├── index.js
│ └── picker-field.js
├── properties-editor
│ ├── convert-initial-value.js
│ ├── index.js
│ ├── initial-value-checker.js
│ ├── memoized-property.js
│ ├── memozied-validator.js
│ ├── properties-editor.js
│ └── validator-property.js
├── tests
│ ├── .eslintrc.json
│ ├── __mocks__
│ │ └── builder-fields.js
│ ├── builder-store
│ │ ├── builder-reducer.test.js
│ │ └── builder-store.test.js
│ ├── helpers
│ │ ├── create-export-schema.test.js
│ │ └── create-initial-data.test.js
│ ├── picker-field.test.js
│ └── properties-editor
│ │ ├── initial-value-checker.test.js
│ │ ├── memoized-property.test.js
│ │ ├── memoized-validator.test.js
│ │ ├── properties-editor.test.js
│ │ └── validator-property.test.js
└── validators-properties
│ ├── index.js
│ └── validators-properties.js
├── webpack.config.js
└── yarn.lock
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 | orbs:
3 | codecov: codecov/codecov@1.0.4
4 | jobs:
5 | build:
6 | docker:
7 | - image: circleci/node:16
8 | steps:
9 | - checkout
10 | - run:
11 | name: update-npm
12 | command: 'sudo npm install -g npm@latest'
13 | - run:
14 | name: install-deps
15 | command: yarn
16 | - run:
17 | name: build-packages
18 | command: yarn build
19 | - run:
20 | name: test
21 | command: yarn test
22 | - codecov/upload:
23 | file: coverage/*.json
24 | token: 5ac8ab74-745a-40ca-ae7f-6044ee59c222
25 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@babel/eslint-parser",
3 | "env": {
4 | "browser": true,
5 | "es6": true
6 | },
7 | "extends": [
8 | "react-app",
9 | "eslint:recommended",
10 | "plugin:react/recommended",
11 | "plugin:prettier/recommended"
12 | ],
13 | "globals": {
14 | "Atomics": "readonly",
15 | "SharedArrayBuffer": "readonly"
16 | },
17 | "parserOptions": {
18 | "ecmaFeatures": {
19 | "jsx": true
20 | },
21 | "ecmaVersion": 2018,
22 | "sourceType": "module"
23 | },
24 | "plugins": [
25 | "react"
26 | ],
27 | "rules": {
28 | "react/jsx-filename-extension": "off",
29 | "react/destructuring-assignment": "off",
30 | "no-shadow": "off",
31 | "react/jsx-props-no-spreading": "off",
32 | "import/prefer-default-export": "off",
33 | "consistent-return": "off",
34 | "react/jsx-fragments": "off",
35 | "arrow-parens": "off",
36 | "no-confusing-arrow": "off",
37 | "no-unused-vars": ["error", { "vars": "all", "args": "after-used", "ignoreRestSiblings": true }],
38 | "react/display-name": "off",
39 | "react-hooks/exhaustive-deps": "off"
40 | }
41 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
63 |
64 | .size-snapshot.json
65 |
66 | dist
67 |
68 | /*
69 |
70 | !src
71 | !src/
72 | !demo/
73 | !demo
74 | !src/*
75 | !demo/*
76 | !config
77 | !.npmignore
78 | !.gitignore
79 | !babel.config.js
80 | !LICENSE
81 | !package.json
82 | !README.md
83 | !tsconfig.json
84 | !scripts
85 | !setupTests.js
86 | !webpack.config.js
87 | !yarn.lock
88 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | demo
2 | src
3 | babel.config.js
4 | .gitignore
5 | webpack.config.js
6 | .circleci
7 | scripts
8 | .eslintrc.json
9 | .prettierrc.json
10 | setupTests.js
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 150,
3 | "arrowParens": "always",
4 | "semi": true,
5 | "tabWidth": 2,
6 | "singleQuote": true,
7 | "jsxSingleQuote": false
8 | }
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Data driven forms
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://codecov.io/gh/data-driven-forms/form-builder)
2 | [](https://circleci.com/gh/data-driven-forms/form-builder/tree/master)
3 | [](https://badge.fury.io/js/%40data-driven-forms%2Fform-builder)
4 | [](https://twitter.com/intent/tweet?text=Check%20DataDrivenForms%20React%20library%21%20https%3A%2F%2Fdata-driven-forms.org%2F&hashtags=react,opensource,datadrivenforms)
5 | [](https://twitter.com/DataDrivenForms)
6 |
7 | [](https://data-driven-forms.org/)
8 |
9 | # FormBuilder
10 |
11 | **THIS PROJECT IS WORK IN PROGRESS. ALL THE API IS CONSIDERED UNSTABLE. EVERYTHING CAN BE CHANGED. NEW FEATURES/BUG FIXES CAN TAKE A LONG TIME TO IMPLEMENT.**
12 |
13 | This component allows to build [Data Driven Forms](https://github.com/data-driven-forms/react-forms) forms via DnD feature.
14 |
15 | **Table of contents**
16 |
17 | - [FormBuilder](#formbuilder)
18 | - [Installation](#installation)
19 | - [Development](#development)
20 | - [Requirements](#requirements)
21 | - [Install with yarn](#install-with-yarn)
22 | - [Start the playground](#start-the-playground)
23 | - [Lint](#lint)
24 | - [Tests](#tests)
25 | - [Build](#build)
26 | - [Usage](#usage)
27 | - [Props](#props)
28 | - [render or children](#render-or-children)
29 | - [componentMapper](#componentmapper)
30 | - [builderMapper](#buildermapper)
31 | - [FieldLayout](#fieldlayout)
32 | - [PropertiesEditor](#propertieseditor)
33 | - [FormContainer](#formcontainer)
34 | - [BUILDER_FIELD](#builder_field)
35 | - [BuilderColumn](#buildercolumn)
36 | - [PropertyGroup](#propertygroup)
37 | - [DragHandle](#draghandle)
38 | - [EmptyTarget](#emptytarget)
39 | - [componentProperties](#componentproperties)
40 | - [attributes](#attributes)
41 | - [propertyName](#propertyname)
42 | - [label](#label)
43 | - [component](#component)
44 | - [disableValidators](#disablevalidators)
45 | - [disableInitialValue](#disableinitialvalue)
46 | - [Example](#example)
47 | - [pickerMapper](#pickermapper)
48 | - [propertiesMapper](#propertiesmapper)
49 | - [mode](#mode)
50 | - [schema](#schema)
51 | - [schemaTemplate](#schematemplate)
52 | - [cloneWhileDragging](#clonewhiledragging)
53 | - [debug](#debug)
54 | - [openEditor](#openeditor)
55 | - [disabledAdd](#disabledadd)
56 | - [disableDrag](#disabledrag)
57 | - [Mappers](#mappers)
58 | - [Material UI](#material-ui)
59 | - [Version 5](#version-5)
60 | - [PatternFly 4](#patternfly-4)
61 | - [Example](#example-1)
62 |
63 | # Installation
64 |
65 | `npm install --save @data-driven-forms/form-builder`
66 |
67 | or
68 |
69 | `yarn add @data-driven-forms/form-builder`
70 |
71 | # Development
72 |
73 | ## Requirements
74 |
75 | - UNIX like system (Linux, MacOS)
76 | - Node 12+
77 |
78 | ## Install with yarn
79 |
80 | `yarn`
81 |
82 | ## Start the playground
83 |
84 | `yarn start`
85 |
86 | Running on `http://localhost:8080/`
87 |
88 | ## Lint
89 |
90 | `yarn lint`
91 |
92 | For automatic fix: `yarn lint --fix`
93 |
94 | ## Tests
95 |
96 | `yarn test`
97 |
98 | For watching changes: `yarn test --watchAll`
99 |
100 | ## Build
101 |
102 | `yarn build`
103 |
104 | To clean built files: `yarn clean-build`. This is also done automatically before `build`.
105 |
106 | # Usage
107 |
108 | Import form builder:
109 |
110 | ```jsx
111 | import FormBuilder from '@data-driven-forms/form-builder/form-builder';
112 | ```
113 |
114 | and render the component with following props:
115 |
116 | # Props
117 |
118 | ## render or children
119 |
120 | *({ ComponentPicker, PropertiesEditor, DropTarget, isValid, getSchema, children }) => React.Element*
121 |
122 | Use children or a render function to render the whole editor.
123 |
124 | ## componentMapper
125 |
126 | *object*
127 |
128 | Data Driven Forms component mapper. See [here](https://data-driven-forms.org/components/renderer#heading-formrenderer#requiredprops).
129 |
130 | ## builderMapper
131 |
132 | *object*
133 |
134 | A set of components that creates the form builder.
135 |
136 | ```jsx
137 | import { builderComponentTypes } from '../src/constants';
138 |
139 |
140 | const builderMapper = {
141 | FieldLayout,
142 | PropertiesEditor,
143 | FormContainer,
144 | [builderComponentTypes.BUILDER_FIELD]: builderField,
145 | BuilderColumn,
146 | PropertyGroup,
147 | DragHandle,
148 | EmptyTarget
149 | }
150 | ```
151 |
152 | 
153 |
154 | ### FieldLayout
155 |
156 | *({ children, disableDrag, dragging, selected }) => React.Element*
157 |
158 | A wrapper around a single field = BUILDER_FORM + DragHandle.
159 |
160 | ### PropertiesEditor
161 |
162 | *({*
163 | *propertiesChildren,*
164 | *validationChildren,*
165 | *addValidator*,
166 | *avaiableValidators*,
167 | *handleClose*,
168 | *handleDelete*,
169 | *hasPropertyError*
170 | *}) => React.Element*
171 |
172 | An editor.
173 |
174 | ### FormContainer
175 |
176 | *({ children, isDraggingOver }) => React.Element*
177 |
178 | A wrapper around the form in the form column.
179 |
180 | ### BUILDER_FIELD
181 |
182 | *({*
183 | *innerProps: { hideField, snapshot },*
184 | *Component*,
185 | *propertyName*,
186 | *fieldId*,
187 | *propertyValidation*,
188 | *hasPropertyError*,
189 | *...props*
190 | *}) => React.Element*
191 |
192 | A wrapper around the field.
193 |
194 | ### BuilderColumn
195 |
196 | *({ children, isDraggingOver }) => React.Element*
197 |
198 | A column.
199 |
200 | ### PropertyGroup
201 |
202 | *({ className, children, title, handleDelete }) => React.Element*
203 |
204 | A wrapper around a single property group in the properties editor > validation.
205 |
206 | ### DragHandle
207 |
208 | *({ dragHandleProps, hasPropertyError, disableDrag }) => React.Element*
209 |
210 | A drag handle. Is passed as a child to FieldLayout.
211 |
212 | ### EmptyTarget
213 |
214 | *() => React.Element*
215 |
216 | EmptyTarget is shown when there are no fields created.
217 |
218 | ## componentProperties
219 |
220 | *object*
221 |
222 | A mapper of editable properties for each component. A property is a object with following attributes:
223 |
224 | ### attributes
225 |
226 | Editable attributes of the component.
227 | #### propertyName
228 |
229 | *string*
230 |
231 | Corresponds to an attribute in the schema.
232 |
233 | #### label
234 |
235 | *string*
236 |
237 | A label shown for the property in the editor.
238 |
239 | #### component
240 |
241 | *string*
242 |
243 | A component corresponding to a key in properties mapper.
244 |
245 | ### disableValidators
246 |
247 | Disables validator selection in `PropertiesEditor`.
248 |
249 | Automatically disabled in fields that are not registered in the form state.
250 |
251 | ### disableInitialValue
252 |
253 | Disables initial value field in `PropertiesEditor`.
254 |
255 | Automatically disabled in fields that are not registered in the form state.
256 |
257 | ### Example
258 |
259 | ```jsx
260 | const LABEL = {
261 | propertyName: 'label',
262 | label: 'Label',
263 | component: 'input'
264 | }
265 |
266 | const componentProperties = {
267 | [componentTypes.TEXT_FIELD]: {
268 | attributes: [LABEL, IS_REQUIRED]
269 | },
270 | [componentTypes.PLAIN_TEXT]: {
271 | attributes: [LABEL],
272 | disableValidators: true,
273 | disableInitialValue: true
274 | }
275 | }
276 | ```
277 |
278 | ## pickerMapper
279 |
280 | *object*
281 |
282 | A mapper of components available in the editor.
283 |
284 | ```jsx
285 | const pickerMapper = {
286 | [componentTypes.TEXT_FIELD]: ({ component }) =>
{component}
287 | }
288 | ```
289 |
290 | ## propertiesMapper
291 |
292 | *object*
293 |
294 | A mapper of components available in component properties.
295 |
296 | ```jsx
297 | const propertiesMapper = {
298 | input: ({ label, value, fieldId, innerProps: { propertyValidation }, ...rest }) =>
299 | }
300 | ```
301 |
302 | ## mode
303 |
304 | *one of 'subset' | 'default'* optional
305 |
306 | If 'subset', options will be only editable to certain degree. See schemaTemplate.
307 |
308 | ## schema
309 |
310 | *object* optional
311 |
312 | A Data Driven Forms schema. See [here](https://data-driven-forms.org/components/renderer#heading-formrenderer#requiredprops).
313 |
314 | ## schemaTemplate
315 |
316 | *object* optional
317 |
318 | An original schema from which a subset is created. If not specified, editable boundaries will be created from the schema.
319 |
320 | ## cloneWhileDragging
321 |
322 | *boolean* optional
323 |
324 | Components from the components list are being cloned when dragged.
325 |
326 | ## debug
327 |
328 | *boolean* optional
329 |
330 | Turns on debug mode. Will show current field as a JSON object.
331 |
332 | ## openEditor
333 |
334 | *boolean* optional
335 |
336 | Opens the first field on mount.
337 |
338 | ## disabledAdd
339 |
340 | *boolean* optional
341 |
342 | Disables adding new fields.
343 |
344 | ## disableDrag
345 |
346 | *boolean* optional
347 |
348 | Disables dragging.
349 |
350 | # Mappers
351 |
352 | Form builder contains two mappers of components: **PatternFly 4** and **Material UI** versions.
353 |
354 | ## Material UI
355 |
356 | ```jsx
357 | import {
358 | pickerMapper,
359 | propertiesMapper,
360 | builderMapper,
361 | BuilderTemplate,
362 | fieldProperties
363 | } from '@data-driven-forms/form-builder/mui-builder-mappers';
364 | ```
365 |
366 | ### Version 5
367 |
368 | MUI-BUILDER is using MUI of version 5. To use version 4 (`@material-ui/core`), please use the builder of version `0.0.12-rc1`.
369 |
370 | ## PatternFly 4
371 |
372 | ```jsx
373 | import {
374 | pickerMapper,
375 | propertiesMapper,
376 | builderMapper,
377 | BuilderTemplate,
378 | fieldProperties
379 | } from '@data-driven-forms/form-builder/pf4-builder-mappers';
380 | ```
381 |
382 | ## Example
383 |
384 | ```jsx
385 | render={({ isValid, getSchema, ...props }) => (
386 |
387 |
388 |
389 | )}
390 | ```
391 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | require.extensions['.css'] = () => undefined;
2 | const path = require('path');
3 | const glob = require('glob');
4 |
5 | const mapper = {
6 | TextVariants: 'Text',
7 | ButtonVariant: 'Button',
8 | DropdownPosition: 'dropdownConstants',
9 | TextListVariants: 'TextList',
10 | TextListItemVariants: 'TextListItem',
11 | };
12 |
13 | const camelToSnake = (string) => {
14 | return string
15 | .replace(/[\w]([A-Z])/g, function (m) {
16 | return m[0] + '-' + m[1];
17 | })
18 | .toLowerCase();
19 | };
20 |
21 | module.exports = {
22 | presets: ['@babel/preset-env', '@babel/preset-react'],
23 | plugins: [
24 | '@babel/plugin-transform-runtime',
25 | '@babel/plugin-syntax-dynamic-import',
26 | '@babel/plugin-proposal-class-properties',
27 | [
28 | 'transform-imports',
29 | {
30 | '@data-driven-forms/react-form-renderer': {
31 | transform: (importName) => `@data-driven-forms/react-form-renderer/${camelToSnake(importName)}`,
32 | preventFullImport: true,
33 | },
34 | },
35 | '@data-driven-forms/react-form-renderer',
36 | ],
37 | ],
38 | env: {
39 | cjs: {
40 | presets: [['@babel/preset-env', { modules: 'commonjs' }]],
41 | plugins: [
42 | [
43 | 'transform-imports',
44 | {
45 | '@patternfly/react-core': {
46 | transform: (importName) => {
47 | let res;
48 | const files = glob.sync(
49 | path.resolve(__dirname, `./node_modules/@patternfly/react-core/dist/js/**/${mapper[importName] || importName}.js`)
50 | );
51 | if (files.length > 0) {
52 | res = files[0];
53 | } else {
54 | throw new Error(`File with importName ${importName} does not exist`);
55 | }
56 |
57 | res = res.replace(path.resolve(__dirname, './node_modules/'), '');
58 | res = res.replace(/^\//, '');
59 | return res;
60 | },
61 | preventFullImport: false,
62 | skipDefaultConversion: true,
63 | },
64 | },
65 | 'react-core-CJS',
66 | ],
67 | [
68 | 'transform-imports',
69 | {
70 | '@patternfly/react-icons': {
71 | transform: (importName) =>
72 | `@patternfly/react-icons/dist/js/icons/${importName
73 | .split(/(?=[A-Z])/)
74 | .join('-')
75 | .toLowerCase()}`,
76 | preventFullImport: true,
77 | },
78 | },
79 | 'react-icons-CJS',
80 | ],
81 | [
82 | 'transform-imports',
83 | {
84 | 'patternfly-react': {
85 | transform: (importName) => {
86 | let res;
87 | const files = glob.sync(path.resolve(__dirname, `./node_modules/patternfly-react/dist/js/**/${importName}.js`));
88 | if (files.length > 0) {
89 | res = files[0];
90 | } else {
91 | throw new Error(`File with importName ${importName} does not exist`);
92 | }
93 |
94 | res = res.replace(path.resolve(__dirname, './node_modules/'), '');
95 | res = res.replace(/^\//, '');
96 | return res;
97 | },
98 | preventFullImport: false,
99 | skipDefaultConversion: false,
100 | },
101 | },
102 | 'pf3-react-CJS',
103 | ],
104 | [
105 | 'transform-imports',
106 | {
107 | '@mui/material': {
108 | transform: (importName) => `@mui/material/node/${importName}`,
109 | preventFullImport: false,
110 | skipDefaultConversion: false,
111 | },
112 | },
113 | 'MUI-CJS',
114 | ],
115 | [
116 | 'transform-imports',
117 | {
118 | '@data-driven-forms/pf4-component-mapper': {
119 | transform: (importName) => `@data-driven-forms/pf4-component-mapper/${camelToSnake(importName)}`,
120 | preventFullImport: true,
121 | },
122 | },
123 | '@data-driven-forms/pf4-component-mapper-CJS',
124 | ],
125 | [
126 | 'transform-imports',
127 | {
128 | '@data-driven-forms/mui-component-mapper': {
129 | transform: (importName) => `@data-driven-forms/mui-component-mapper/${camelToSnake(importName)}`,
130 | preventFullImport: true,
131 | },
132 | },
133 | '@data-driven-forms/mui-component-mapper-CJS',
134 | ],
135 | ],
136 | },
137 | esm: {
138 | presets: [['@babel/preset-env', { modules: false }]],
139 | plugins: [
140 | [
141 | 'transform-imports',
142 | {
143 | '@patternfly/react-core': {
144 | transform: (importName) => {
145 | let res;
146 | const files = glob.sync(
147 | path.resolve(__dirname, `./node_modules/@patternfly/react-core/dist/esm/**/${mapper[importName] || importName}.js`)
148 | );
149 | if (files.length > 0) {
150 | res = files[0];
151 | } else {
152 | throw new Error(`File with importName ${importName} does not exist`);
153 | }
154 |
155 | res = res.replace(path.resolve(__dirname, './node_modules/'), '');
156 | res = res.replace(/^\//, '');
157 | return res;
158 | },
159 | preventFullImport: false,
160 | skipDefaultConversion: true,
161 | },
162 | },
163 | 'react-core-ESM',
164 | ],
165 |
166 | [
167 | 'transform-imports',
168 | {
169 | '@patternfly/react-icons': {
170 | transform: (importName) =>
171 | `@patternfly/react-icons/dist/esm/icons/${importName
172 | .split(/(?=[A-Z])/)
173 | .join('-')
174 | .toLowerCase()}`,
175 | preventFullImport: true,
176 | },
177 | },
178 | 'react-icons-ESM',
179 | ],
180 | [
181 | 'transform-imports',
182 | {
183 | 'patternfly-react': {
184 | transform: (importName) => {
185 | let res;
186 | const files = glob.sync(path.resolve(__dirname, `./node_modules/patternfly-react/dist/esm/**/${importName}.js`));
187 | if (files.length > 0) {
188 | res = files[0];
189 | } else {
190 | throw new Error(`File with importName ${importName} does not exist`);
191 | }
192 |
193 | res = res.replace(path.resolve(__dirname, './node_modules/'), '');
194 | res = res.replace(/^\//, '');
195 | return res;
196 | },
197 | preventFullImport: false,
198 | skipDefaultConversion: false,
199 | },
200 | },
201 | 'pf3-react-ESM',
202 | ],
203 | [
204 | 'transform-imports',
205 | {
206 | '@mui/material': {
207 | transform: (importName) => `@mui/material/${importName}`,
208 | preventFullImport: false,
209 | skipDefaultConversion: false,
210 | },
211 | },
212 | 'MUI-ESM',
213 | ],
214 | [
215 | 'transform-imports',
216 | {
217 | '@data-driven-forms/pf4-component-mapper': {
218 | transform: (importName) => `@data-driven-forms/pf4-component-mapper/${camelToSnake(importName)}`,
219 | preventFullImport: true,
220 | },
221 | },
222 | '@data-driven-forms/pf4-component-mapper-ESM',
223 | ],
224 | [
225 | 'transform-imports',
226 | {
227 | '@data-driven-forms/mui-component-mapper': {
228 | transform: (importName) => `@data-driven-forms/mui-component-mapper/${camelToSnake(importName)}`,
229 | preventFullImport: true,
230 | },
231 | },
232 | '@data-driven-forms/mui-component-mapper-ESM',
233 | ],
234 | ],
235 | },
236 | },
237 | };
238 |
--------------------------------------------------------------------------------
/demo/field-properties.js:
--------------------------------------------------------------------------------
1 | export const LABEL = {
2 | propertyName: 'label',
3 | label: 'Label',
4 | component: 'input',
5 | };
6 |
7 | export const HELPER_TEXT = {
8 | propertyName: 'helperText',
9 | label: 'Helper text',
10 | component: 'input',
11 | };
12 |
13 | export const PLACEHOLDER = {
14 | propertyName: 'placeholder',
15 | label: 'Placeholder',
16 | component: 'input',
17 | };
18 |
19 | export const INPUT_TYPE = {
20 | label: 'Input Type',
21 | propertyName: 'type',
22 | options: ['text', 'number', 'password'],
23 | component: 'select',
24 | };
25 |
26 | export const IS_DISABLED = {
27 | propertyName: 'isDisabled',
28 | label: 'Disabled',
29 | component: 'switch',
30 | };
31 |
32 | export const IS_READ_ONLY = {
33 | propertyName: 'isReadOnly',
34 | label: 'Read only',
35 | component: 'switch',
36 | };
37 |
38 | export const OPTIONS = {
39 | propertyName: 'options',
40 | label: 'Options',
41 | component: 'options',
42 | };
43 |
44 | export const IS_CLEARABLE = {
45 | propertyName: 'isClearable',
46 | label: 'Clearable',
47 | component: 'switch',
48 | };
49 |
50 | export const CLOSE_ON_DAY_SELECT = {
51 | propertyName: 'closeOnDaySelect',
52 | label: 'Close on day select',
53 | component: 'switch',
54 | };
55 |
56 | export const SHOW_TODAY_BUTTON = {
57 | propertyName: 'showTodayButton',
58 | label: 'Show today button',
59 | component: 'switch',
60 | };
61 |
62 | export const TODAY_BUTTON_LABEL = {
63 | propertyName: 'todayButtonLabel',
64 | label: 'Today button label',
65 | component: 'input',
66 | };
67 |
68 | export const MULTI_LINE_LABEL = {
69 | propertyName: 'label',
70 | label: 'Label',
71 | component: 'input',
72 | };
73 |
74 | export const TITLE = {
75 | propertyName: 'title',
76 | label: 'Title',
77 | component: 'textarea',
78 | };
79 |
80 | export const DESCRIPTION = {
81 | propertyName: 'description',
82 | label: 'Description',
83 | component: 'input',
84 | };
85 |
86 | export const HIDE_FIELD = {
87 | propertyName: 'hideField',
88 | label: 'Hidden',
89 | component: 'switch',
90 | };
91 |
92 | export const LEFT_TITLE = {
93 | propertyName: 'leftTitle',
94 | label: 'Left title',
95 | component: 'input',
96 | };
97 |
98 | export const RIGHT_TITLE = {
99 | propertyName: 'rightTitle',
100 | label: 'Right title',
101 | component: 'input',
102 | };
103 |
104 | export const MOVE_LEFT_TITLE = {
105 | propertyName: 'moveLeftTitle',
106 | label: 'Move left button title',
107 | component: 'input',
108 | };
109 |
110 | export const MOVE_RIGHT_TITLE = {
111 | propertyName: 'moveRightTitle',
112 | label: 'Move right button title',
113 | component: 'input',
114 | };
115 |
116 | export const MOVE_ALL_LEFT_TITLE = {
117 | propertyName: 'moveAllLeftTitle',
118 | label: 'Move all left button title',
119 | component: 'input',
120 | };
121 |
122 | export const MOVE_ALL_RIGHT_TITLE = {
123 | propertyName: 'moveAllRightTitle',
124 | label: 'Move alll right button title',
125 | component: 'input',
126 | };
127 |
128 | export const ALL_TO_LEFT = {
129 | propertyName: 'allToLeft',
130 | label: 'Allow to move all to left',
131 | component: 'switch',
132 | };
133 |
134 | export const ALL_TO_RIGHT = {
135 | propertyName: 'allToRight',
136 | label: 'Allow to move all to right',
137 | component: 'switch',
138 | };
139 |
140 | export const NO_VALUE_TITLE = {
141 | propertyName: 'noValueTitle',
142 | label: 'Placeholder for empty value',
143 | component: 'input',
144 | };
145 |
146 | export const NO_OPTIONS_TITLE = {
147 | propertyName: 'noOptionsTitle',
148 | label: 'Placeholder for empty options',
149 | component: 'input',
150 | };
151 |
152 | export const FILTER_OPTIONS_TITLE = {
153 | propertyName: 'filterOptionsTitle',
154 | label: 'Placeholder for options filter input',
155 | component: 'input',
156 | };
157 |
158 | export const FILTER_VALUE_TITLE = {
159 | propertyName: 'filterValueTitle',
160 | label: 'Placeholder for value filter input',
161 | component: 'input',
162 | };
163 |
164 | export const FILTER_VALUE_TEXT = {
165 | propertyName: 'filterValueText',
166 | label: 'Placeholder for value when there is no filtered value',
167 | component: 'input',
168 | };
169 |
170 | export const FILTER_OPTIONS_TEXT = {
171 | propertyName: 'filterOptionsText',
172 | label: 'Placeholder for options when there is no filtered option',
173 | component: 'input',
174 | };
175 |
176 | export const CHECKBOX_VARIANT = {
177 | propertyName: 'checkboxVariant',
178 | label: 'Checkbox variant',
179 | component: 'switch',
180 | };
181 |
182 | export const MIN = {
183 | propertyName: 'min',
184 | label: 'Minimum of range',
185 | component: 'input',
186 | };
187 |
188 | export const MAX = {
189 | propertyName: 'max',
190 | label: 'Maximum of range',
191 | component: 'input',
192 | };
193 |
194 | export const STEP = {
195 | propertyName: 'step',
196 | label: 'Step size',
197 | component: 'input',
198 | };
199 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Form builder demo
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/demo/index.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment, useState } from 'react';
2 | import ReactDom from 'react-dom';
3 | import { componentTypes, validatorTypes } from '@data-driven-forms/react-form-renderer';
4 | import { createTheme, ThemeProvider, StyledEngineProvider } from '@mui/material/styles';
5 | import CssBaseline from '@mui/material/CssBaseline';
6 | import FormBuilder from '../src/form-builder';
7 |
8 | import { componentMapper as pf4ComponentMapper } from '@data-driven-forms/pf4-component-mapper';
9 | import { componentMapper as muiComponentMapper } from '@data-driven-forms/mui-component-mapper';
10 |
11 | import {
12 | pickerMapper as muiPickerMapper,
13 | propertiesMapper as muiPropertiesMapper,
14 | builderMapper as muiPBuilderMapper,
15 | BuilderTemplate as muiBuilderTemplate,
16 | } from '../src/mui-builder-mappers';
17 | import {
18 | pickerMapper as pf4PickerMapper,
19 | propertiesMapper as pf4PropertiesMapper,
20 | builderMapper as pf4PBuilderMapper,
21 | BuilderTemplate as pf4BuilderTemplate,
22 | } from '../src/pf4-builder-mappers';
23 |
24 | import {
25 | LABEL,
26 | HELPER_TEXT,
27 | PLACEHOLDER,
28 | INPUT_TYPE,
29 | IS_DISABLED,
30 | IS_READ_ONLY,
31 | OPTIONS,
32 | TODAY_BUTTON_LABEL,
33 | IS_CLEARABLE,
34 | CLOSE_ON_DAY_SELECT,
35 | SHOW_TODAY_BUTTON,
36 | MULTI_LINE_LABEL,
37 | TITLE,
38 | DESCRIPTION,
39 | HIDE_FIELD,
40 | LEFT_TITLE,
41 | RIGHT_TITLE,
42 | MOVE_LEFT_TITLE,
43 | MOVE_RIGHT_TITLE,
44 | MOVE_ALL_LEFT_TITLE,
45 | MOVE_ALL_RIGHT_TITLE,
46 | ALL_TO_LEFT,
47 | ALL_TO_RIGHT,
48 | NO_VALUE_TITLE,
49 | NO_OPTIONS_TITLE,
50 | FILTER_OPTIONS_TITLE,
51 | FILTER_VALUE_TITLE,
52 | FILTER_VALUE_TEXT,
53 | FILTER_OPTIONS_TEXT,
54 | CHECKBOX_VARIANT,
55 | STEP,
56 | MIN,
57 | MAX,
58 | } from './field-properties';
59 |
60 | const componentProperties = {
61 | [componentTypes.TEXT_FIELD]: {
62 | attributes: [LABEL, HELPER_TEXT, PLACEHOLDER, INPUT_TYPE, IS_DISABLED, IS_READ_ONLY, HIDE_FIELD],
63 | },
64 | [componentTypes.CHECKBOX]: {
65 | attributes: [LABEL, IS_DISABLED, OPTIONS, HIDE_FIELD],
66 | },
67 | [componentTypes.SELECT]: {
68 | attributes: [LABEL, OPTIONS, IS_DISABLED, PLACEHOLDER, HELPER_TEXT, HIDE_FIELD],
69 | },
70 | [componentTypes.DATE_PICKER]: {
71 | attributes: [LABEL, TODAY_BUTTON_LABEL, IS_CLEARABLE, CLOSE_ON_DAY_SELECT, SHOW_TODAY_BUTTON, HIDE_FIELD],
72 | },
73 | [componentTypes.PLAIN_TEXT]: { attributes: [MULTI_LINE_LABEL] },
74 | [componentTypes.RADIO]: { attributes: [LABEL, IS_DISABLED, OPTIONS, HIDE_FIELD] },
75 | [componentTypes.SWITCH]: {
76 | attributes: [LABEL, IS_READ_ONLY, IS_DISABLED, HIDE_FIELD],
77 | },
78 | [componentTypes.TEXTAREA]: {
79 | attributes: [LABEL, HELPER_TEXT, IS_READ_ONLY, IS_DISABLED, HIDE_FIELD],
80 | },
81 | [componentTypes.SUB_FORM]: {
82 | isContainer: true,
83 | attributes: [TITLE, DESCRIPTION],
84 | },
85 | [componentTypes.DUAL_LIST_SELECT]: {
86 | attributes: [
87 | LABEL,
88 | HELPER_TEXT,
89 | DESCRIPTION,
90 | OPTIONS,
91 | HIDE_FIELD,
92 | LEFT_TITLE,
93 | RIGHT_TITLE,
94 | MOVE_LEFT_TITLE,
95 | MOVE_RIGHT_TITLE,
96 | MOVE_ALL_LEFT_TITLE,
97 | MOVE_ALL_RIGHT_TITLE,
98 | ALL_TO_LEFT,
99 | ALL_TO_RIGHT,
100 | NO_VALUE_TITLE,
101 | NO_OPTIONS_TITLE,
102 | FILTER_OPTIONS_TITLE,
103 | FILTER_VALUE_TITLE,
104 | FILTER_VALUE_TEXT,
105 | FILTER_OPTIONS_TEXT,
106 | CHECKBOX_VARIANT,
107 | ],
108 | },
109 | [componentTypes.SLIDER]: {
110 | attributes: [LABEL, HELPER_TEXT, DESCRIPTION, HIDE_FIELD, MIN, MAX, STEP],
111 | },
112 | };
113 |
114 | const schema = {
115 | fields: [
116 | {
117 | component: componentTypes.TEXT_FIELD,
118 | name: 'my-text-field',
119 | label: 'Something',
120 | initialValue: 'Foo',
121 | isRequired: true,
122 | hideField: true,
123 | validate: [
124 | {
125 | type: validatorTypes.REQUIRED,
126 | message: 'This field is required',
127 | },
128 | {
129 | type: validatorTypes.MIN_LENGTH,
130 | threshold: 7,
131 | },
132 | {
133 | type: validatorTypes.MAX_LENGTH,
134 | threshold: 10,
135 | },
136 | ],
137 | },
138 | {
139 | component: componentTypes.TEXT_FIELD,
140 | name: 'my-number-field',
141 | label: 'Number field',
142 | dataType: 'integer',
143 | initialValue: 5,
144 | validate: [
145 | {
146 | type: validatorTypes.MAX_NUMBER_VALUE,
147 | value: 33,
148 | },
149 | {
150 | type: validatorTypes.MIN_NUMBER_VALUE,
151 | value: 14,
152 | },
153 | ],
154 | },
155 | {
156 | component: componentTypes.TEXT_FIELD,
157 | name: 'pattern-field',
158 | label: 'Pattern field',
159 | validate: [
160 | {
161 | type: validatorTypes.PATTERN,
162 | pattern: /^Foo$/,
163 | },
164 | ],
165 | },
166 | {
167 | component: componentTypes.SELECT,
168 | label: 'Select',
169 | name: 'select',
170 | initialValue: '2',
171 | options: [
172 | {
173 | label: 'Option 1',
174 | value: '1',
175 | },
176 | {
177 | label: 'Option 2',
178 | value: '2',
179 | },
180 | ],
181 | },
182 | ],
183 | };
184 |
185 | const schemaTemplate = {
186 | fields: [
187 | {
188 | component: componentTypes.TEXT_FIELD,
189 | name: 'my-text-field',
190 | label: 'Something',
191 | isRequired: true,
192 | initialValue: 'Foo',
193 | validate: [
194 | {
195 | type: validatorTypes.REQUIRED,
196 | message: 'This field is required',
197 | },
198 | {
199 | type: validatorTypes.MIN_LENGTH,
200 | threshold: 5,
201 | },
202 | {
203 | type: validatorTypes.MAX_LENGTH,
204 | threshold: 10,
205 | },
206 | ],
207 | },
208 | {
209 | component: componentTypes.TEXT_FIELD,
210 | name: 'my-number-field',
211 | label: 'Number field',
212 | type: 'number',
213 | dataType: 'integer',
214 | initialValue: 5,
215 | validate: [
216 | {
217 | type: validatorTypes.MAX_NUMBER_VALUE,
218 | value: 50,
219 | },
220 | {
221 | type: validatorTypes.MIN_NUMBER_VALUE,
222 | value: 14,
223 | },
224 | ],
225 | },
226 | {
227 | component: componentTypes.TEXT_FIELD,
228 | name: 'pattern-field',
229 | label: 'Pattern field',
230 | validate: [
231 | {
232 | type: validatorTypes.PATTERN,
233 | pattern: /^Foo$/,
234 | },
235 | ],
236 | },
237 | {
238 | component: componentTypes.SELECT,
239 | label: 'Select',
240 | name: 'select',
241 | initialValue: '2',
242 | options: [
243 | {
244 | label: 'Option 1',
245 | value: '1',
246 | },
247 | {
248 | label: 'Option 2',
249 | value: '2',
250 | },
251 | {
252 | label: 'Option 3',
253 | value: '3',
254 | },
255 | ],
256 | },
257 | ],
258 | };
259 |
260 | const pf4State = {
261 | pickerMapper: pf4PickerMapper,
262 | propertiesMapper: pf4PropertiesMapper,
263 | builderMapper: pf4PBuilderMapper,
264 | BuilderTemplate: pf4BuilderTemplate,
265 | componentMapper: pf4ComponentMapper,
266 | };
267 |
268 | const muiState = {
269 | pickerMapper: muiPickerMapper,
270 | propertiesMapper: muiPropertiesMapper,
271 | builderMapper: muiPBuilderMapper,
272 | BuilderTemplate: muiBuilderTemplate,
273 | componentMapper: muiComponentMapper,
274 | };
275 |
276 | const Demo = () => {
277 | const [state, setState] = useState(pf4State);
278 |
279 | return (
280 |
281 |
282 |
283 |
284 |
285 |
286 | (
301 |
302 |
303 |
304 |
305 |
306 | )}
307 | />
308 |
309 |
310 |
311 | );
312 | };
313 |
314 | ReactDom.render(, document.getElementById('root'));
315 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@data-driven-forms/form-builder",
3 | "version": "0.0.12-rc1",
4 | "description": "Interactive drag and drop editor for creating data driven forms schema",
5 | "main": "index.js",
6 | "module": "esm/index.js",
7 | "engines": {
8 | "node": ">=12.0.0"
9 | },
10 | "scripts": {
11 | "test": "jest --coverage",
12 | "start": "webpack-dev-server --hot --open --mode development",
13 | "lint": "eslint ./src",
14 | "build": "yarn build:cjs && yarn build:esm && yarn build:packages && yarn build:css",
15 | "build:cjs": "BABEL_ENV=cjs babel src --out-dir ./ --ignore \"src/tests/*\"",
16 | "build:esm": "BABEL_ENV=esm babel src --out-dir ./esm --ignore \"src/tests/*\"",
17 | "build:packages": "node ./scripts/generate-packages.js",
18 | "build:css": "node ./scripts/copy-css.js",
19 | "clean-build": "node ./scripts/clean-build.js",
20 | "prebuild": "node ./scripts/clean-build.js"
21 | },
22 | "author": "Martin Marosi",
23 | "license": "MIT",
24 | "devDependencies": {
25 | "@babel/cli": "^7.16.0",
26 | "@babel/core": "^7.16.0",
27 | "@babel/eslint-parser": "^7.16.0",
28 | "@babel/plugin-proposal-class-properties": "^7.16.0",
29 | "@babel/plugin-transform-runtime": "^7.16.0",
30 | "@babel/preset-env": "^7.16.0",
31 | "@babel/preset-react": "^7.16.0",
32 | "@data-driven-forms/mui-component-mapper": "3.16.0-v5-beta-1",
33 | "@data-driven-forms/pf4-component-mapper": "^3.15.6",
34 | "@data-driven-forms/react-form-renderer": "^3.15.6",
35 | "@mui/material": "^5.1.0",
36 | "@mui/styles": "^5.1.0",
37 | "@mui/icons-material": "^5.1.0",
38 | "@patternfly/react-core": "^4.168.8",
39 | "@patternfly/react-icons": "^4.19.8",
40 | "@wojtekmaj/enzyme-adapter-react-17": "^0.6.5",
41 | "babel-jest": "^27.3.1",
42 | "babel-loader": "^8.2.3",
43 | "babel-plugin-transform-imports": "^2.0.0",
44 | "css-loader": "^6.5.0",
45 | "enzyme": "^3.11.0",
46 | "enzyme-to-json": "^3.6.2",
47 | "eslint": "^8.1.0",
48 | "eslint-config-prettier": "^8.3.0",
49 | "eslint-config-react-app": "^6.0.0",
50 | "eslint-plugin-flowtype": "^8.0.3",
51 | "eslint-plugin-import": "^2.25.2",
52 | "eslint-plugin-jsx-a11y": "^6.4.1",
53 | "eslint-plugin-prettier": "^4.0.0",
54 | "eslint-plugin-react": "^7.26.1",
55 | "eslint-plugin-react-hooks": "^4.2.0",
56 | "fs-extra": "10.0.0",
57 | "html-webpack-plugin": "^5.5.0",
58 | "identity-obj-proxy": "^3.0.0",
59 | "jest": "^27.3.1",
60 | "prettier": "2.4.1",
61 | "react": "^17.0.2",
62 | "react-dom": "^17.0.2",
63 | "redux-mock-store": "^1.5.4",
64 | "style-loader": "^3.3.1",
65 | "webpack": "^5.61.0",
66 | "webpack-cli": "^4.9.1",
67 | "webpack-dev-server": "^4.4.0"
68 | },
69 | "dependencies": {
70 | "clsx": "^1.1.1",
71 | "lodash": "^4.17.21",
72 | "prop-types": "^15.7.2",
73 | "react-beautiful-dnd": "^13.1.0",
74 | "react-redux": "^7.2.6",
75 | "redux": "^4.1.2"
76 | },
77 | "peerDependencies": {
78 | "@data-driven-forms/react-form-renderer": ">=3.4.1",
79 | "react": "^16.13.1 || ^17.0.2",
80 | "react-dom": "^16.13.1 || ^17.0.2"
81 | },
82 | "jest": {
83 | "testEnvironment": "jsdom",
84 | "snapshotSerializers": [
85 | "enzyme-to-json/serializer"
86 | ],
87 | "collectCoverage": true,
88 | "collectCoverageFrom": [
89 | "src/**/*.{js,jsx}"
90 | ],
91 | "roots": [
92 | "/src/"
93 | ],
94 | "setupFiles": [
95 | "/setupTests.js"
96 | ],
97 | "moduleNameMapper": {
98 | "\\.(css|scss)$": "identity-obj-proxy"
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/scripts/clean-build.js:
--------------------------------------------------------------------------------
1 | if (process.env.CI) {
2 | return;
3 | }
4 |
5 | const path = require('path');
6 | const fse = require('fs-extra');
7 | const glob = require('glob');
8 |
9 | const root = path.resolve(__dirname, '../');
10 |
11 | function cleanPackage() {
12 | const ignore = fse.readFileSync(path.resolve(root, '.gitignore'), 'utf8');
13 | const ignores = ignore
14 | .split('\n')
15 | .filter((line) => !(line.length === 0 || line[0] === '#'))
16 | .filter((item) => !item.includes('node_modules'));
17 | const positive = [];
18 | const negative = [];
19 | ignores.forEach((item) => {
20 | if (item[0] === '!') {
21 | negative.push(item.replace(/^!/, ''));
22 | } else {
23 | positive.push(item);
24 | }
25 | });
26 | const pattern = `{${positive.join(',')}}`;
27 | const files = glob
28 | .sync(path.resolve(root, `./${pattern}`))
29 | .filter((item) => !negative.find((n) => item.endsWith(n) || item.includes('node_modules')));
30 | files.forEach((file) => {
31 | fse.removeSync(file);
32 | });
33 | }
34 |
35 | function run() {
36 | try {
37 | cleanPackage();
38 | } catch (err) {
39 | console.error(err);
40 | process.exit(1);
41 | }
42 | }
43 |
44 | run();
45 |
--------------------------------------------------------------------------------
/scripts/copy-css.js:
--------------------------------------------------------------------------------
1 | const glob = require('glob');
2 | const path = require('path');
3 | const { copyFileSync } = require('fs');
4 |
5 | const packagePath = process.cwd();
6 | const src = path.resolve(packagePath, './src');
7 |
8 | function copyCss() {
9 | const directories = glob.sync(`${src}/*/`).filter((name) => !name.includes('/tests/'));
10 |
11 | directories.forEach((dir) => {
12 | const cssFiles = glob.sync(`${dir}/**/*.css`);
13 |
14 | cssFiles.forEach((file) => {
15 | const fileName = file.replace(/^.*src\//, '');
16 |
17 | copyFileSync(file, `./${fileName}`);
18 | copyFileSync(file, `./esm/${fileName}`);
19 | });
20 | });
21 | }
22 |
23 | function run() {
24 | try {
25 | copyCss();
26 | } catch (err) {
27 | console.error(err);
28 | process.exit(1);
29 | }
30 | }
31 |
32 | run();
33 |
--------------------------------------------------------------------------------
/scripts/generate-packages.js:
--------------------------------------------------------------------------------
1 | const glob = require('glob');
2 | const path = require('path');
3 | const fse = require('fs-extra');
4 |
5 | const packagePath = process.cwd();
6 | const src = path.resolve(packagePath, './src');
7 |
8 | async function generatePackages() {
9 | const directories = glob
10 | .sync(`${src}/*/`)
11 | .filter((name) => !name.includes('/tests/'))
12 | .map((path) => path.replace(/\/$/, '').split('/').pop());
13 | const cmds = directories.map((dir) => {
14 | const pckg = {
15 | main: 'index.js',
16 | module: `../esm/${dir}`,
17 | };
18 | return fse.writeJSON(path.resolve(packagePath, dir, 'package.json'), pckg);
19 | });
20 |
21 | return Promise.all(cmds);
22 | }
23 |
24 | async function run() {
25 | try {
26 | await generatePackages();
27 | } catch (err) {
28 | console.error(err);
29 | process.exit(1);
30 | }
31 | }
32 |
33 | run();
34 |
--------------------------------------------------------------------------------
/setupTests.js:
--------------------------------------------------------------------------------
1 | import { configure } from 'enzyme';
2 | import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
3 |
4 | configure({ adapter: new Adapter() });
5 |
--------------------------------------------------------------------------------
/src/builder-store/builder-reducer.js:
--------------------------------------------------------------------------------
1 | import { validatorTypes } from '@data-driven-forms/react-form-renderer';
2 | import propertiesValidation from '../properties-editor/initial-value-checker';
3 | import { FORM_LAYOUT } from '../helpers';
4 |
5 | const isInContainer = (index, containers) => containers.find((c) => index > c.boundaries[0] && index <= c.boundaries[1]);
6 |
7 | const mutateColumns = (result, state) => {
8 | const { destination, source, draggableId } = result;
9 | const { dropTargets, fields, containers } = state;
10 | if (!destination) {
11 | return {};
12 | }
13 |
14 | if (destination.droppableId === source.droppableId && destination.index === source.index) {
15 | return {};
16 | }
17 |
18 | const start = dropTargets[source.droppableId];
19 | const finish = dropTargets[destination.droppableId];
20 | const template = fields[draggableId];
21 |
22 | const isMovingInColumn = start === finish;
23 |
24 | if (isMovingInColumn) {
25 | const noContainerNesting = template.isContainer && isInContainer(destination.index, containers);
26 | if (noContainerNesting) {
27 | return;
28 | }
29 | if (template.isContainer) {
30 | const newFieldsIds = [...start.fieldsIds];
31 | newFieldsIds.splice(source.index, template.children.length + 2);
32 | newFieldsIds.splice(destination.index, 0, draggableId, ...template.children, `${draggableId}-end`);
33 | return {
34 | dropTargets: {
35 | ...dropTargets,
36 | [source.droppableId]: { ...start, fieldsIds: newFieldsIds },
37 | },
38 | };
39 | } else {
40 | const newFieldsIds = [...start.fieldsIds];
41 | const moveContainer = isInContainer(destination.index, containers);
42 | const newFields = { ...fields };
43 | /**
44 | * Move into from root
45 | * filed was not in container before
46 | */
47 | if (moveContainer && !fields[draggableId].container) {
48 | newFields[moveContainer.id].children = [...newFields[moveContainer.id].children, draggableId];
49 | newFields[draggableId].container = moveContainer.id;
50 | }
51 | /**
52 | * Move field outside of a container to root
53 | */
54 | if (fields[draggableId].container && !moveContainer) {
55 | newFields[fields[draggableId].container].children = newFields[fields[draggableId].container].children.filter(
56 | (child) => child !== draggableId
57 | );
58 | delete newFields[draggableId].container;
59 | }
60 | /**
61 | * Move field between containers
62 | */
63 | if (moveContainer && fields[draggableId].container && fields[draggableId].container !== moveContainer.id) {
64 | newFields[moveContainer.id].children = [...newFields[moveContainer.id].children, draggableId];
65 | newFields[fields[draggableId].container].children = newFields[fields[draggableId].container].children.filter(
66 | (child) => child !== draggableId
67 | );
68 | newFields[draggableId].container = moveContainer.id;
69 | }
70 | newFieldsIds.splice(source.index, 1);
71 | newFieldsIds.splice(destination.index, 0, draggableId);
72 | return {
73 | fields: newFields,
74 | dropTargets: {
75 | ...dropTargets,
76 | [source.droppableId]: { ...start, fieldsIds: newFieldsIds },
77 | },
78 | };
79 | }
80 | }
81 | /**
82 | * Copy to column
83 | */
84 |
85 | const newId = Date.now().toString();
86 | const finishFieldsIds = [...finish.fieldsIds];
87 | const container = isInContainer(destination.index, containers);
88 |
89 | const newFields = {
90 | ...fields,
91 | [newId]: {
92 | ...fields[draggableId],
93 | name: fields[draggableId].component,
94 | preview: false,
95 | id: newId,
96 | initialized: false,
97 | container: container && container.id,
98 | children: template.isContainer && [],
99 | },
100 | };
101 | let newContainers = [...containers];
102 | if (container) {
103 | newFields[container.id] = {
104 | ...newFields[container.id],
105 | children: [...newFields[container.id].children, newId],
106 | };
107 | newContainers = newContainers.map((c) => (c.id === container.id ? { ...c, boundaries: [c.boundaries[0], c.boundaries[1] + 1] } : c));
108 | }
109 | if (template.isContainer) {
110 | finishFieldsIds.splice(destination.index, 0, newId, `${newId}-end`);
111 | newFields[`${newId}-end`] = {
112 | component: 'container-end',
113 | id: `${newId}-end`,
114 | };
115 | newContainers.push({
116 | id: newId,
117 | boundaries: [destination.index, destination.index + 1],
118 | });
119 | } else {
120 | finishFieldsIds.splice(destination.index, 0, newId);
121 | }
122 | const newFinish = {
123 | ...finish,
124 | fieldsIds: finishFieldsIds,
125 | };
126 | return {
127 | dropTargets: { ...dropTargets, [newFinish.id]: newFinish },
128 | fields: newFields,
129 | selectedComponent: newId,
130 | containers: newContainers,
131 | };
132 | };
133 |
134 | const removeComponent = (componentId, state) => {
135 | const { fields } = state;
136 | const field = { ...fields[componentId] };
137 | let containers = [...state.containers];
138 | if (field.container) {
139 | /**
140 | * adjust container size if field was in container
141 | */
142 | containers = containers.map((c) => (c.id === field.container ? { ...c, boundaries: [c.boundaries[0], c.boundaries[1] - 1] } : c));
143 | }
144 | delete fields[componentId];
145 | delete fields[`${componentId}-end`];
146 | return {
147 | selectedComponent: undefined,
148 | dropTargets: {
149 | ...state.dropTargets,
150 | [FORM_LAYOUT]: {
151 | ...state.dropTargets[FORM_LAYOUT],
152 | fieldsIds: state.dropTargets[FORM_LAYOUT].fieldsIds.filter((id) => id !== componentId && id !== `${componentId}-end`),
153 | },
154 | },
155 | fields: { ...state.fields },
156 | containers,
157 | };
158 | };
159 |
160 | const setFieldproperty = (field, payload) => {
161 | const modifiedField = {
162 | ...field,
163 | initialized: true,
164 | [payload.propertyName]: payload.value,
165 | };
166 | return {
167 | ...modifiedField,
168 | propertyValidation: {
169 | ...modifiedField.propertyValidation,
170 | ...propertiesValidation(payload.propertyName)(modifiedField),
171 | },
172 | };
173 | };
174 |
175 | const dragStart = (field, state) => {
176 | if (field.draggableId.match(/^initial-/) || !state.fields[field.draggableId].isContainer) {
177 | return {};
178 | }
179 | return { draggingContainer: field.draggableId };
180 | };
181 |
182 | const changeValidator = (field, { index, action, fieldId, ...validator }) => {
183 | const result = { ...field };
184 | const validate = result.validate || [];
185 | if (validator.type === validatorTypes.REQUIRED) {
186 | result.isRequired = action !== 'remove';
187 | }
188 | if (action === 'remove') {
189 | result.validate = [...validate.slice(0, index), ...validate.slice(index + 1)];
190 | }
191 |
192 | if (action === 'add') {
193 | result.validate = [...validate, { ...validator }];
194 | }
195 |
196 | if (action === 'modify') {
197 | result.validate = validate.map((item, itemIndex) => (itemIndex === index ? { ...item, ...validator } : item));
198 | }
199 |
200 | return result;
201 | };
202 |
203 | export const SET_COLUMNS = 'setColumns';
204 | export const SET_SELECTED_COMPONENT = 'setSelectedComponent';
205 | export const REMOVE_COMPONENT = 'removeComponent';
206 | export const DRAG_START = 'dragStart';
207 | export const SET_FIELD_PROPERTY = 'setFieldProperty';
208 | export const SET_FIELD_VALIDATOR = 'setFieldValidator';
209 | export const INITIALIZE = 'initialize';
210 | export const UNINITIALIZE = 'uninitialize';
211 |
212 | const builderReducer = (state, action) => {
213 | switch (action.type) {
214 | case INITIALIZE:
215 | return { ...state, ...action.payload, initialized: true };
216 | case UNINITIALIZE:
217 | return { initialized: false };
218 | case SET_COLUMNS:
219 | return {
220 | ...state,
221 | ...mutateColumns(action.payload, state),
222 | draggingContainer: undefined,
223 | };
224 | case SET_SELECTED_COMPONENT:
225 | return { ...state, selectedComponent: action.payload };
226 | case REMOVE_COMPONENT:
227 | return { ...state, ...removeComponent(action.payload, state) };
228 | case DRAG_START:
229 | return { ...state, ...dragStart(action.payload, state) };
230 | case SET_FIELD_PROPERTY:
231 | return {
232 | ...state,
233 | fields: {
234 | ...state.fields,
235 | [action.payload.fieldId]: setFieldproperty(state.fields[action.payload.fieldId], action.payload),
236 | },
237 | };
238 | case SET_FIELD_VALIDATOR:
239 | return {
240 | ...state,
241 | fields: {
242 | ...state.fields,
243 | [action.payload.fieldId]: changeValidator(state.fields[action.payload.fieldId], action.payload),
244 | },
245 | };
246 | default:
247 | return state;
248 | }
249 | };
250 |
251 | export default builderReducer;
252 |
--------------------------------------------------------------------------------
/src/builder-store/builder-store.js:
--------------------------------------------------------------------------------
1 | import { createStore } from 'redux';
2 |
3 | import builderReducer from './builder-reducer';
4 |
5 | const builderStore = createStore(builderReducer, { initialized: false });
6 |
7 | export default builderStore;
8 |
--------------------------------------------------------------------------------
/src/builder-store/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './builder-store';
2 | export { default as builderReducer } from './builder-reducer';
3 | export * from './builder-store';
4 | export * from './builder-reducer';
5 |
--------------------------------------------------------------------------------
/src/component-picker/component-picker.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { Droppable } from 'react-beautiful-dnd';
3 | import ComponentsContext from '../components-context';
4 | import PickerField from '../picker-field';
5 | import { COMPONENTS_LIST } from '../helpers';
6 | import { useSelector } from 'react-redux';
7 | import { ComponentPickerContext } from '../layout-context';
8 |
9 | const ComponentPicker = () => {
10 | const {
11 | builderMapper: { BuilderColumn },
12 | } = useContext(ComponentsContext);
13 | const { fields, disableAdd } = useContext(ComponentPickerContext);
14 | const dropTargetId = useSelector(({ dropTargets }) => dropTargets[COMPONENTS_LIST].id);
15 |
16 | if (disableAdd) {
17 | return null;
18 | }
19 | return (
20 |
21 | {(provided, snapshot) => (
22 |
23 |
24 | {fields.map((field, index) => (
25 |
26 | ))}
27 | {provided.placeholder}
28 |
29 |
30 | )}
31 |
32 | );
33 | };
34 |
35 | export default ComponentPicker;
36 |
--------------------------------------------------------------------------------
/src/component-picker/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './component-picker';
2 | export * from './component-picker';
3 |
--------------------------------------------------------------------------------
/src/components-context/components-context.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | const ComponentsContext = createContext({});
4 |
5 | export default ComponentsContext;
6 |
--------------------------------------------------------------------------------
/src/components-context/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './components-context';
2 | export * from './components-context';
3 |
--------------------------------------------------------------------------------
/src/constants/constants.js:
--------------------------------------------------------------------------------
1 | export const builderComponentTypes = {
2 | BUILDER_FIELD: 'builder-field',
3 | PICKER_FIELD: 'picker-field',
4 | };
5 |
6 | export default { builderComponentTypes };
7 |
--------------------------------------------------------------------------------
/src/constants/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './constants';
2 | export * from './constants';
3 |
--------------------------------------------------------------------------------
/src/drop-target/drop-target.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { Droppable } from 'react-beautiful-dnd';
3 | import PropTypes from 'prop-types';
4 | import Field from '../field';
5 | import ComponentsContext from '../components-context';
6 | import { useSelector } from 'react-redux';
7 | import { FORM_LAYOUT } from '../helpers';
8 | import { DropTargetContext } from '../layout-context';
9 |
10 | const DropTarget = () => {
11 | const {
12 | builderMapper: { FormContainer, EmptyTarget },
13 | } = useContext(ComponentsContext);
14 | const { disableDrag } = useContext(DropTargetContext);
15 | const dropTargets = useSelector(({ dropTargets }) => dropTargets);
16 | const dropTargetId = useSelector(({ dropTargets }) => dropTargets[FORM_LAYOUT].id);
17 | const fields = dropTargets[FORM_LAYOUT].fieldsIds;
18 | return (
19 |
20 | {(provided, snapshot) => {
21 | return (
22 |
23 |
24 | {fields.length === 0 && }
25 | {fields.map((fieldId, index) => (
26 |
27 | ))}
28 | {provided.placeholder}
29 |
30 |
31 | );
32 | }}
33 |
34 | );
35 | };
36 |
37 | DropTarget.propTypes = {
38 | isDropDisabled: PropTypes.bool,
39 | shouldClone: PropTypes.bool,
40 | disableDrag: PropTypes.bool,
41 | disableDelete: PropTypes.bool,
42 | };
43 |
44 | export default DropTarget;
45 |
--------------------------------------------------------------------------------
/src/drop-target/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './drop-target';
2 | export * from './drop-target';
3 |
--------------------------------------------------------------------------------
/src/field/field.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, memo, Fragment } from 'react';
2 | import { Draggable } from 'react-beautiful-dnd';
3 | import PropTypes from 'prop-types';
4 | import ComponentsContext from '../components-context';
5 | import { useDispatch, useSelector } from 'react-redux';
6 | import isEqual from 'lodash/isEqual';
7 | import { builderComponentTypes } from '../constants';
8 |
9 | const Field = memo(({ fieldId, index, shouldClone, disableDrag, draggingContainer }) => {
10 | const {
11 | builderMapper: { FieldActions, FieldLayout, DragHandle, ...rest },
12 | componentMapper,
13 | } = useContext(ComponentsContext);
14 | const { clone, isContainer, validate, ...field } = useSelector(({ fields }) => fields[fieldId]);
15 | const selectedComponent = useSelector(({ selectedComponent }) => selectedComponent);
16 | const dispatch = useDispatch();
17 | const FieldComponent = rest[field.component] || rest[builderComponentTypes.BUILDER_FIELD];
18 |
19 | const hasPropertyError = field.propertyValidation && Object.entries(field.propertyValidation).find(([, value]) => value);
20 | if (field.component === 'container-end') {
21 | return (
22 |
23 | {(provided) => (
24 |
39 | )}
40 |
41 | );
42 | }
43 | const { hideField, initialized, preview, restricted, ...cleanField } = field;
44 | return (
45 |
46 | {(provided, snapshot) => {
47 | const innerProps = {
48 | snapshot: snapshot,
49 | hasPropertyError: !!hasPropertyError,
50 | hideField,
51 | initialized,
52 | preview,
53 | restricted,
54 | };
55 | return (
56 | dispatch({ type: 'setSelectedComponent', payload: field.id })}
71 | >
72 |
79 | {field.preview ? (
80 | {field.content}
81 | ) : (
82 |
83 |
84 | {!shouldClone && (
85 |
86 | )}
87 |
88 | )}
89 |
90 |
91 | );
92 | }}
93 |
94 | );
95 | });
96 |
97 | Field.propTypes = {
98 | index: PropTypes.number.isRequired,
99 | shouldClone: PropTypes.bool,
100 | disableDrag: PropTypes.bool,
101 | selectedComponent: PropTypes.string,
102 | draggingContainer: PropTypes.string,
103 | fieldId: PropTypes.string.isRequired,
104 | };
105 |
106 | const MemoizedField = (props) => {
107 | const { selectedComponent, draggingContainer } = useSelector(
108 | ({ selectedComponent, draggingContainer }) => ({
109 | selectedComponent,
110 | draggingContainer,
111 | }),
112 | isEqual
113 | );
114 | return ;
115 | };
116 |
117 | export default MemoizedField;
118 |
--------------------------------------------------------------------------------
/src/field/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './field';
2 | export * from './field';
3 |
--------------------------------------------------------------------------------
/src/form-builder-layout/form-builder-layout.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { DragDropContext } from 'react-beautiful-dnd';
3 | import PropTypes from 'prop-types';
4 | import throttle from 'lodash/throttle';
5 | import DropTarget from '../drop-target';
6 | import PropertiesEditor from '../properties-editor';
7 | import ComponentPicker from '../component-picker';
8 | import { INITIALIZE, UNINITIALIZE } from '../builder-store';
9 | import { validateOutput, COMPONENTS_LIST, FORM_LAYOUT } from '../helpers';
10 | import { useDispatch, useSelector, shallowEqual } from 'react-redux';
11 | import { DropTargetContext, ComponentPickerContext } from '../layout-context';
12 | import createSchema from '../helpers/create-export-schema';
13 |
14 | const throttleValidator = throttle(validateOutput, 250);
15 |
16 | const Layout = ({ getSchema, state, render, children }) => {
17 | const layoutProps = {
18 | getSchema,
19 | isValid: throttleValidator(state.fields),
20 | ComponentPicker,
21 | DropTarget,
22 | PropertiesEditor,
23 | };
24 | if (render) {
25 | return render(layoutProps);
26 | }
27 | if (children && children.length > 1) {
28 | throw new Error('Form builder requires only one child node. Please wrap your childnre in a Fragment');
29 | }
30 | if (children) {
31 | return children(layoutProps);
32 | }
33 |
34 | throw new Error('Form builder requires either render prop or children');
35 | };
36 |
37 | const FormBuilderLayout = ({ initialFields, disableDrag, mode, disableAdd, children, render }) => {
38 | const dispatch = useDispatch();
39 | const state = useSelector((state) => state, shallowEqual);
40 | useEffect(() => {
41 | dispatch({ type: INITIALIZE, payload: initialFields });
42 |
43 | return () => dispatch({ type: UNINITIALIZE });
44 | }, []);
45 | const getSchema = () => createSchema(state.dropTargets[FORM_LAYOUT].fieldsIds, state.fields);
46 |
47 | const onDragEnd = (result) => dispatch({ type: 'setColumns', payload: result });
48 | const onDragStart = (draggable) => dispatch({ type: 'dragStart', payload: draggable });
49 | const { dropTargets, fields } = state;
50 | if (!state.initialized) {
51 | return Loading
;
52 | }
53 |
54 | return (
55 |
56 |
61 | fields[taskId]),
64 | disableAdd,
65 | }}
66 | >
67 |
68 | {children}
69 |
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | FormBuilderLayout.propTypes = {
77 | initialFields: PropTypes.object,
78 | disableDrag: PropTypes.bool,
79 | mode: PropTypes.string.isRequired,
80 | children: PropTypes.node,
81 | render: PropTypes.func,
82 | disableAdd: PropTypes.bool,
83 | };
84 |
85 | export default FormBuilderLayout;
86 |
--------------------------------------------------------------------------------
/src/form-builder-layout/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './form-builder-layout';
2 | export * from './form-builder-layout';
3 |
--------------------------------------------------------------------------------
/src/form-builder/form-builder.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import FormBuilderLayout from '../form-builder-layout';
4 | import ComponentsContext from '../components-context';
5 | import helpers from '../helpers';
6 | import { Provider } from 'react-redux';
7 | import builderStore from '../builder-store';
8 | import { Form, RendererContext } from '@data-driven-forms/react-form-renderer';
9 |
10 | const ContainerEnd = ({ id }) => {id}
;
11 |
12 | ContainerEnd.propTypes = {
13 | id: PropTypes.string,
14 | };
15 |
16 | const FormBuilder = ({
17 | builderMapper,
18 | componentProperties,
19 | pickerMapper,
20 | propertiesMapper,
21 | cloneWhileDragging,
22 | schema,
23 | schemaTemplate,
24 | mode,
25 | debug,
26 | children,
27 | componentMapper,
28 | openEditor,
29 | ...props
30 | }) => {
31 | const initialFields = Object.keys(componentProperties).reduce(
32 | (acc, curr) => ({
33 | ...acc,
34 | [`initial-${curr}`]: {
35 | preview: true,
36 | id: `initial-${curr}`,
37 | component: curr,
38 | clone: cloneWhileDragging,
39 | isContainer: componentProperties[curr].isContainer,
40 | },
41 | }),
42 | {}
43 | );
44 | return (
45 |
56 |
73 |
74 | );
75 | };
76 |
77 | FormBuilder.propTypes = {
78 | mode: PropTypes.oneOf(['default', 'subset']),
79 | debug: PropTypes.bool,
80 | builderMapper: PropTypes.object.isRequired,
81 | componentProperties: PropTypes.shape({
82 | attributes: PropTypes.arrayOf(
83 | PropTypes.shape({
84 | component: PropTypes.string.isRequired,
85 | label: PropTypes.string.isRequired,
86 | propertyName: PropTypes.string.isRequired,
87 | })
88 | ),
89 | }).isRequired,
90 | pickerMapper: PropTypes.object.isRequired,
91 | propertiesMapper: PropTypes.object.isRequired,
92 | cloneWhileDragging: PropTypes.bool,
93 | schema: PropTypes.object,
94 | schemaTemplate: PropTypes.object,
95 | componentMapper: PropTypes.object.isRequired,
96 | openEditor: PropTypes.bool,
97 | children: PropTypes.node,
98 | };
99 |
100 | FormBuilder.defaultProps = {
101 | mode: 'default',
102 | debug: false,
103 | };
104 |
105 | export default FormBuilder;
106 |
--------------------------------------------------------------------------------
/src/form-builder/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './form-builder';
2 | export * from './form-builder';
3 |
--------------------------------------------------------------------------------
/src/helpers/create-export-schema.js:
--------------------------------------------------------------------------------
1 | const ARTIFICIAL_KEYS = ['preview', 'clone', 'initialized', 'id', 'isContainer', 'children', 'container', 'restricted', 'propertyValidation'];
2 |
3 | const sanitizeField = (field) => {
4 | const result = { ...field };
5 | ARTIFICIAL_KEYS.forEach((key) => {
6 | delete result[key];
7 | });
8 | if (result.options) {
9 | result.options = result.options.filter(({ deleted }) => !deleted).map(({ restoreable, ...option }) => option);
10 | }
11 | return result;
12 | };
13 |
14 | export const validateOutput = (fields) => {
15 | const valid = Object.keys(fields).find(
16 | (key) =>
17 | fields[key].propertyValidation &&
18 | Object.keys(fields[key].propertyValidation).length > 0 &&
19 | Object.entries(fields[key].propertyValidation).find(([, value]) => value)
20 | );
21 | return !valid;
22 | };
23 |
24 | const createSchema = (fieldsIds = [], fields) => ({
25 | fields: fieldsIds.map((key) => sanitizeField(fields[key])),
26 | });
27 |
28 | export default createSchema;
29 |
--------------------------------------------------------------------------------
/src/helpers/create-initial-data.js:
--------------------------------------------------------------------------------
1 | export const COMPONENTS_LIST = 'components-list';
2 | export const FORM_LAYOUT = 'form-layout';
3 |
4 | /**
5 | * Returns a flat fields object to be rendered and edited in the editor
6 | * @param {Object} initialFields available field definitions in component chooser
7 | * @param {Object} schema data driven form schema
8 | * @param {Boolean} isSubset if true, options will be only editable to certain degree
9 | * @param {Object} schemaTemplate original from which a subset is created. If not specified, editable boundaries will be created from schema
10 | */
11 | const createInitialData = (initialFields, schema, isSubset, schemaTemplate) => {
12 | const fields = {
13 | ...initialFields,
14 | };
15 | const fieldsIds = [];
16 | if (schema && schema.fields) {
17 | schema.fields.forEach((field) => {
18 | const id = `${field.name}-${Date.now().toString()}`;
19 | fieldsIds.push(id);
20 | fields[id] = {
21 | ...field,
22 | id,
23 | clone: true,
24 | preview: false,
25 | initialized: true,
26 | };
27 | if (isSubset) {
28 | let template;
29 | let templateOptions = fields[id].options && fields[id].options.map((option) => ({ ...option, restoreable: true }));
30 | if (schemaTemplate) {
31 | template = schemaTemplate.fields.find(({ name }) => name === field.name);
32 | }
33 | if (template && template.options) {
34 | template.options.forEach((option) => {
35 | if (!templateOptions.find(({ value }) => value === option.value)) {
36 | templateOptions.push({ ...option, restoreable: true, deleted: true });
37 | }
38 | });
39 | }
40 | fields[id] = {
41 | ...fields[id],
42 | restricted: true,
43 | options: templateOptions,
44 | validate: fields[id].validate
45 | ? fields[id].validate.map((validator, index) => ({
46 | ...validator,
47 | original: template ? { ...template.validate[index] } : { ...validator },
48 | }))
49 | : undefined,
50 | };
51 | }
52 | });
53 | }
54 |
55 | return {
56 | fields,
57 | dropTargets: {
58 | [COMPONENTS_LIST]: {
59 | id: COMPONENTS_LIST,
60 | title: 'Component picker',
61 | fieldsIds: Object.keys(initialFields),
62 | },
63 | [FORM_LAYOUT]: {
64 | id: FORM_LAYOUT,
65 | title: 'Form',
66 | fieldsIds,
67 | },
68 | },
69 | selectedComponent: undefined,
70 | containers: [],
71 | };
72 | };
73 |
74 | export default createInitialData;
75 |
--------------------------------------------------------------------------------
/src/helpers/index.js:
--------------------------------------------------------------------------------
1 | import { default as createInitialData } from './create-initial-data';
2 | import { default as createExportSchema } from './create-export-schema';
3 | export * from './create-export-schema';
4 | export * from './create-initial-data';
5 |
6 | export default { createInitialData, createExportSchema };
7 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export * from './form-builder';
2 |
--------------------------------------------------------------------------------
/src/layout-context/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './layout-context';
2 | export * from './layout-context';
3 |
--------------------------------------------------------------------------------
/src/layout-context/layout-context.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | export const DropTargetContext = createContext({});
4 |
5 | export const ComponentPickerContext = createContext({});
6 |
7 | export default { DropTargetContext, ComponentPickerContext };
8 |
--------------------------------------------------------------------------------
/src/mui-builder-mappers/builder-mapper.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { componentTypes } from '@data-driven-forms/react-form-renderer';
4 |
5 | import clsx from 'clsx';
6 |
7 | import { styled } from '@mui/material/styles';
8 |
9 | import {
10 | Card,
11 | CardContent,
12 | MenuItem,
13 | TextField as TextFieldMUI,
14 | Tabs,
15 | Tab,
16 | CardHeader,
17 | Typography,
18 | Divider,
19 | Box,
20 | Badge,
21 | IconButton,
22 | } from '@mui/material';
23 | import ErrorIcon from '@mui/icons-material/Error';
24 | import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
25 | import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
26 | import ArrowBackIcon from '@mui/icons-material/ArrowBack';
27 | import DeleteIcon from '@mui/icons-material/Delete';
28 | import CloseIcon from '@mui/icons-material/Close';
29 | import { builderComponentTypes } from '../constants';
30 |
31 | import { grey, red, blue } from '@mui/material/colors';
32 |
33 | const snapshotPropType = PropTypes.shape({ isDragging: PropTypes.bool }).isRequired;
34 | const childrenPropType = PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]);
35 |
36 | const prepareLabel = (component, isDragging) =>
37 | ({
38 | [componentTypes.CHECKBOX]: 'Please, provide label',
39 | [componentTypes.PLAIN_TEXT]: 'Please provide a label to plain text component',
40 | [componentTypes.DUAL_LIST_SELECT]: 'Please pick label and options',
41 | [componentTypes.RADIO]: 'Please pick label and options',
42 | }[component] || (isDragging ? component : ''));
43 |
44 | const prepareOptions = (component, options = []) =>
45 | ({
46 | [componentTypes.SELECT]: { options: options.filter(({ deleted }) => !deleted) },
47 | [componentTypes.DUAL_LIST_SELECT]: { options },
48 | [componentTypes.RADIO]: { options },
49 | }[component] || {});
50 |
51 | const StyledComponentWrapper = styled('div')(({ theme }) => ({
52 | '&.componentWrapper': {
53 | position: 'relative',
54 | display: 'flex',
55 | flexGrow: 1,
56 | padding: 8,
57 | },
58 | '&.componentWrapperOverlay': {
59 | '&:after': {
60 | pointerEvents: 'none',
61 | zIndex: 0,
62 | display: 'block',
63 | content: '""',
64 | position: 'absolute',
65 | transitionProperty: 'all',
66 | transitionDuration: theme.transitions.duration.standard,
67 | transitionTimingFunction: theme.transitions.easing.easeInOut,
68 | opacity: 0,
69 | top: 0,
70 | left: 0,
71 | right: 0,
72 | bottom: 0,
73 | },
74 | },
75 | '&.componentWrapperHidden': {
76 | pointerEvents: 'none',
77 | '&:after': {
78 | background: grey[200],
79 | opacity: 0.8,
80 | },
81 | },
82 | '& .hiddenIconIndicator': {
83 | zIndex: 1,
84 | position: 'absolute',
85 | left: '50%',
86 | fontSize: '3rem',
87 | top: 'calc(50% - 3rem / 2)',
88 | opacity: 0,
89 | transitionProperty: 'opacity',
90 | transitionDuration: theme.transitions.duration.standard,
91 | transitionTimingFunction: theme.transitions.easing.easeInOut,
92 | },
93 | '& .showHiddenIndicator': {
94 | opacity: 1,
95 | },
96 | }));
97 |
98 | const ComponentWrapper = ({
99 | innerProps: { hideField, snapshot },
100 | Component,
101 | propertyName,
102 | fieldId,
103 | propertyValidation,
104 | hasPropertyError,
105 | ...props
106 | }) => (
107 |
112 |
117 |
122 |
123 | );
124 |
125 | ComponentWrapper.propTypes = {
126 | Component: PropTypes.elementType,
127 | component: PropTypes.string,
128 | innerProps: PropTypes.shape({
129 | snapshot: snapshotPropType,
130 | hideField: PropTypes.bool,
131 | }).isRequired,
132 | label: PropTypes.string,
133 | preview: PropTypes.bool,
134 | id: PropTypes.string,
135 | initialized: PropTypes.bool,
136 | options: PropTypes.arrayOf(PropTypes.shape({ value: PropTypes.any, label: PropTypes.string })),
137 | propertyName: PropTypes.string,
138 | fieldId: PropTypes.string,
139 | propertyValidation: PropTypes.any,
140 | hasPropertyError: PropTypes.bool,
141 | };
142 |
143 | const StyledFieldLayout = styled('div')(({ theme }) => ({
144 | '&.fieldLayout': {
145 | paddingBottom: 8,
146 | cursor: 'pointer',
147 | position: 'relative',
148 | '&:after': {
149 | display: 'block',
150 | content: '""',
151 | position: 'absolute',
152 | bottom: 8,
153 | top: 0,
154 | left: 0,
155 | right: 0,
156 | borderBottomStyle: 'solid',
157 | borderBottomWidth: 3,
158 | borderBottomColor: theme.palette.primary.main,
159 | transform: 'scaleX(0)',
160 | transitionProperty: 'transform',
161 | transitionDuration: theme.transitions.duration.standard,
162 | transitionTimingFunction: theme.transitions.easing.easeInOut,
163 | },
164 | },
165 | '&.fieldLayoutDragging': {
166 | '& .mui-builder-drag-handle-icon': {
167 | fill: theme.palette.primary.main,
168 | },
169 | },
170 | '&.fieldLayoutSelected': {
171 | '&:after': {
172 | pointerEvents: 'none',
173 | transform: 'scaleX(1)',
174 | },
175 | },
176 | '& .fieldCard': {
177 | overflow: 'unset',
178 | paddingBottom: 0,
179 | display: 'flex',
180 | },
181 | }));
182 |
183 | const FieldLayout = ({ children, disableDrag, dragging, selected }) => (
184 |
191 |
192 | {children}
193 |
194 |
195 | );
196 |
197 | FieldLayout.propTypes = {
198 | children: childrenPropType,
199 | disableDrag: PropTypes.bool,
200 | dragging: PropTypes.bool,
201 | selected: PropTypes.bool,
202 | };
203 |
204 | const StyledBuilderColumn = styled('div')(() => ({
205 | '&.builderColumn': {
206 | margin: 16,
207 | },
208 | }));
209 |
210 | const BuilderColumn = ({ children, isDraggingOver, ...props }) => (
211 |
212 | {children}
213 |
214 | );
215 |
216 | BuilderColumn.propTypes = {
217 | className: PropTypes.string,
218 | children: childrenPropType,
219 | isDraggingOver: PropTypes.bool,
220 | };
221 |
222 | const StyledPropertiesEditor = styled(Card)(() => ({
223 | '&.propertiesContainer': {
224 | 'padding-left': 8,
225 | 'flex-grow': '1',
226 | 'max-width': '30%',
227 | width: '30%',
228 | minHeight: '100vh',
229 | },
230 | '& .tabs': {
231 | marginBottom: 8,
232 | },
233 | '& .warning': {
234 | fill: red[500],
235 | },
236 | '& .form': {
237 | display: 'grid',
238 | 'grid-gap': 16,
239 | },
240 | }));
241 |
242 | const PropertiesEditor = ({
243 | propertiesChildren,
244 | validationChildren,
245 | addValidator,
246 | avaiableValidators,
247 | handleClose,
248 | handleDelete,
249 | hasPropertyError,
250 | disableValidators,
251 | }) => {
252 | const [activeTab, setActiveTab] = useState(0);
253 |
254 | useEffect(() => {
255 | if (activeTab === 1 && disableValidators) {
256 | setActiveTab(0);
257 | }
258 | }, [disableValidators]);
259 |
260 | return (
261 |
262 |
263 |
267 | {handleDelete && (
268 |
269 |
270 |
271 | )}
272 |
273 |
274 |
275 |
276 | }
277 | >
278 | Properties editor
279 |
280 | setActiveTab(tabIndex)}>
281 | }>
284 | Properties
285 |
286 | }
287 | />
288 | {!disableValidators && }
289 |
290 |
291 |
292 |
293 |
294 | addValidator(e.target.value)}
298 | label="Add validator"
299 | placeholder="Choose new validator"
300 | fullWidth
301 | value=""
302 | >
303 | {avaiableValidators.map((option) => (
304 |
307 | ))}
308 |
309 |
310 |
311 |
312 |
313 | );
314 | };
315 |
316 | PropertiesEditor.propTypes = {
317 | propertiesChildren: childrenPropType,
318 | validationChildren: childrenPropType,
319 | avaiableValidators: PropTypes.arrayOf(PropTypes.shape({ label: PropTypes.string, value: PropTypes.string })).isRequired,
320 | addValidator: PropTypes.func.isRequired,
321 | fieldName: PropTypes.string,
322 | handleClose: PropTypes.func.isRequired,
323 | handleDelete: PropTypes.func,
324 | hasPropertyError: PropTypes.array,
325 | disableValidators: PropTypes.bool,
326 | };
327 |
328 | const StyledPropertyGroup = styled('div')(() => ({
329 | '& .form': {
330 | display: 'grid',
331 | 'grid-gap': 16,
332 | },
333 | '& .warning': {
334 | fill: red[500],
335 | },
336 | }));
337 |
338 | const PropertyGroup = ({ className, children, title, handleDelete, ...props }) => (
339 |
340 |
341 |
342 | {title}
343 | {handleDelete && (
344 |
345 |
346 |
347 | )}
348 |
349 |
350 | {children}
351 |
352 |
353 | );
354 |
355 | PropertyGroup.propTypes = {
356 | className: PropTypes.string,
357 | children: childrenPropType,
358 | title: PropTypes.string.isRequired,
359 | handleDelete: PropTypes.func,
360 | };
361 |
362 | const StyledDragHandle = styled('div')(({ theme }) => ({
363 | '&.handle': {
364 | background: grey[300],
365 | textAlign: 'right',
366 | padding: 2,
367 | lineHeight: 0,
368 | display: 'flex',
369 | flexDirection: 'column',
370 | '&:hover svg:last-child': {
371 | fill: theme.palette.primary.main,
372 | },
373 | },
374 | '& .warning': {
375 | fill: red[500],
376 | },
377 | }));
378 |
379 | const DragHandle = ({ dragHandleProps, hasPropertyError, disableDrag }) => {
380 | if (disableDrag && !hasPropertyError) {
381 | return null;
382 | }
383 |
384 | return (
385 |
386 | {hasPropertyError && }
387 | {!disableDrag && }
388 |
389 | );
390 | };
391 |
392 | DragHandle.propTypes = {
393 | dragHandleProps: PropTypes.shape({
394 | 'data-rbd-drag-handle-draggable-id': PropTypes.string.isRequired,
395 | 'data-rbd-drag-handle-context-id': PropTypes.string.isRequired,
396 | 'aria-labelledby': PropTypes.string,
397 | tabIndex: PropTypes.number,
398 | draggable: PropTypes.bool,
399 | onDragStart: PropTypes.func.isRequired,
400 | }),
401 | disableDrag: PropTypes.bool,
402 | hasPropertyError: PropTypes.bool,
403 | };
404 |
405 | const StyledFormContainerDiv = styled('div')(({ theme }) => ({
406 | '&.formContainer': {
407 | 'flex-grow': '1',
408 | padding: 16,
409 | backgroundColor: 'transparent',
410 | transitionProperty: 'background-color',
411 | transitionDuration: theme.transitions.duration.standard,
412 | transitionTimingFunction: theme.transitions.easing.easeInOut,
413 | },
414 | '&.formContainerOver': {
415 | backgroundColor: blue[100],
416 | },
417 | }));
418 |
419 | const FormContainer = ({ children, isDraggingOver }) => (
420 |
425 | {children}
426 |
427 | );
428 |
429 | const StyledBox = styled(Box)(() => ({
430 | '&.emptyTarget': {
431 | height: '100%',
432 | },
433 | }));
434 |
435 | const EmptyTarget = () => (
436 |
437 |
438 |
439 | );
440 |
441 | FormContainer.propTypes = {
442 | children: childrenPropType,
443 | className: PropTypes.string,
444 | isDraggingOver: PropTypes.bool,
445 | };
446 |
447 | const builderMapper = {
448 | FieldLayout,
449 | PropertiesEditor,
450 | FormContainer,
451 | [builderComponentTypes.BUILDER_FIELD]: ComponentWrapper,
452 | BuilderColumn,
453 | PropertyGroup,
454 | DragHandle,
455 | EmptyTarget,
456 | };
457 |
458 | export default builderMapper;
459 |
--------------------------------------------------------------------------------
/src/mui-builder-mappers/builder-template.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { styled } from '@mui/material/styles';
4 |
5 | const Root = styled('div')(() => ({
6 | '&.root': {
7 | display: 'flex',
8 | flexDirection: 'column',
9 | height: '100%',
10 | },
11 | '& .builderLayout': {
12 | display: 'flex',
13 | flexGrow: 1,
14 | },
15 | }));
16 |
17 | const BuilderTemplate = ({ ComponentPicker, PropertiesEditor, DropTarget, children }) => (
18 |
19 | {children}
20 |
25 |
26 | );
27 |
28 | BuilderTemplate.propTypes = {
29 | ComponentPicker: PropTypes.func.isRequired,
30 | PropertiesEditor: PropTypes.func.isRequired,
31 | DropTarget: PropTypes.func.isRequired,
32 | children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]),
33 | };
34 |
35 | export default BuilderTemplate;
36 |
--------------------------------------------------------------------------------
/src/mui-builder-mappers/field-properties.js:
--------------------------------------------------------------------------------
1 | export const LABEL = {
2 | propertyName: 'label',
3 | label: 'Label',
4 | component: 'input',
5 | };
6 |
7 | export const HELPER_TEXT = {
8 | propertyName: 'helperText',
9 | label: 'Helper text',
10 | component: 'input',
11 | };
12 |
13 | export const PLACEHOLDER = {
14 | propertyName: 'placeholder',
15 | label: 'Placeholder',
16 | component: 'input',
17 | };
18 |
19 | export const INPUT_TYPE = {
20 | label: 'Input Type',
21 | propertyName: 'type',
22 | options: ['text', 'number', 'password'],
23 | component: 'select',
24 | };
25 |
26 | export const IS_DISABLED = {
27 | propertyName: 'isDisabled',
28 | label: 'Disabled',
29 | component: 'switch',
30 | };
31 |
32 | export const IS_READ_ONLY = {
33 | propertyName: 'isReadOnly',
34 | label: 'Read only',
35 | component: 'switch',
36 | };
37 |
38 | export const OPTIONS = {
39 | propertyName: 'options',
40 | label: 'Options',
41 | component: 'options',
42 | };
43 |
44 | export const IS_CLEARABLE = {
45 | propertyName: 'isClearable',
46 | label: 'Clearable',
47 | component: 'switch',
48 | };
49 |
50 | export const CLOSE_ON_DAY_SELECT = {
51 | propertyName: 'closeOnDaySelect',
52 | label: 'Close on day select',
53 | component: 'switch',
54 | };
55 |
56 | export const SHOW_TODAY_BUTTON = {
57 | propertyName: 'showTodayButton',
58 | label: 'Show today button',
59 | component: 'switch',
60 | };
61 |
62 | export const TODAY_BUTTON_LABEL = {
63 | propertyName: 'todayButtonLabel',
64 | label: 'Today button label',
65 | component: 'input',
66 | };
67 |
68 | export const MULTI_LINE_LABEL = {
69 | propertyName: 'label',
70 | label: 'Label',
71 | component: 'textarea',
72 | };
73 |
74 | export const TITLE = {
75 | propertyName: 'title',
76 | label: 'Title',
77 | component: 'input',
78 | };
79 |
80 | export const DESCRIPTION = {
81 | propertyName: 'description',
82 | label: 'Description',
83 | component: 'input',
84 | };
85 |
86 | export const HIDE_FIELD = {
87 | propertyName: 'hideField',
88 | label: 'Hidden',
89 | component: 'switch',
90 | };
91 |
--------------------------------------------------------------------------------
/src/mui-builder-mappers/index.js:
--------------------------------------------------------------------------------
1 | export * as fieldProperties from './field-properties';
2 | export { default as builderMapper } from './builder-mapper';
3 | export { default as pickerMapper } from './picker-mapper';
4 | export { default as propertiesMapper } from './properties-mapper';
5 | export { default as BuilderTemplate } from './builder-template';
6 |
--------------------------------------------------------------------------------
/src/mui-builder-mappers/picker-mapper.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { componentTypes } from '@data-driven-forms/react-form-renderer';
4 | import { Button } from '@mui/material';
5 | import TextFieldsIcon from '@mui/icons-material/TextFields';
6 | import CheckBoxIcon from '@mui/icons-material/CheckBox';
7 | import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
8 | import TodayIcon from '@mui/icons-material/Today';
9 | import ChromeReaderModeIcon from '@mui/icons-material/ChromeReaderMode';
10 | import RadioButtonCheckedIcon from '@mui/icons-material/RadioButtonChecked';
11 | import ToggleOffIcon from '@mui/icons-material/ToggleOff';
12 | import LowPriorityIcon from '@mui/icons-material/LowPriority';
13 | import TuneIcon from '@mui/icons-material/Tune';
14 | import { styled } from '@mui/material/styles';
15 | import { builderComponentTypes } from '../constants';
16 |
17 | const Root = styled('div')(() => ({
18 | '&.root': {
19 | '& > *': {
20 | 'margin-bottom': 8,
21 | },
22 | },
23 | '& .label': {
24 | justifyContent: 'end',
25 | },
26 | '& .buttonRoot': {
27 | pointerEvents: 'none',
28 | backgroundImage: 'linear-gradient(135deg, #41108E 0%, rgba(165, 37, 193, 1) 44.76%, #FC9957 100%)',
29 | backgroundRepeat: 'no-repeat',
30 | },
31 | }));
32 |
33 | const labels = {
34 | [componentTypes.TEXT_FIELD]: 'Text field',
35 | [componentTypes.CHECKBOX]: 'Checkbox',
36 | [componentTypes.SELECT]: 'Select',
37 | [componentTypes.DATE_PICKER]: 'Date picker',
38 | [componentTypes.PLAIN_TEXT]: 'Plain text',
39 | [componentTypes.RADIO]: 'Radio',
40 | [componentTypes.SWITCH]: 'Switch',
41 | [componentTypes.TEXTAREA]: 'Textarea',
42 | [componentTypes.SUB_FORM]: 'Sub form',
43 | [componentTypes.DUAL_LIST_SELECT]: 'Dual list select',
44 | [componentTypes.SLIDER]: 'Slider',
45 | };
46 |
47 | const icons = {
48 | [componentTypes.TEXT_FIELD]: ,
49 | [componentTypes.CHECKBOX]: ,
50 | [componentTypes.SELECT]: ,
51 | [componentTypes.DATE_PICKER]: ,
52 | [componentTypes.DUAL_LIST_SELECT]: ,
53 | [componentTypes.PLAIN_TEXT]: ,
54 | [componentTypes.RADIO]: ,
55 | [componentTypes.SWITCH]: ,
56 | [componentTypes.TEXTAREA]: ,
57 | [componentTypes.SUB_FORM]: null,
58 | [componentTypes.SLIDER]: ,
59 | };
60 |
61 | const PickerRoot = ({ component }) => (
62 |
63 |
76 |
77 | );
78 |
79 | PickerRoot.propTypes = {
80 | component: PropTypes.string.isRequired,
81 | };
82 |
83 | const pickerMapper = {
84 | [builderComponentTypes.PICKER_FIELD]: PickerRoot,
85 | };
86 |
87 | export default pickerMapper;
88 |
--------------------------------------------------------------------------------
/src/mui-builder-mappers/properties-mapper.js:
--------------------------------------------------------------------------------
1 | /* eslint react/no-array-index-key: "off" */
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 |
5 | import { styled } from '@mui/material/styles';
6 |
7 | import { TextField, FormLabel, FormControl, FormGroup, FormControlLabel, FormHelperText, Switch, MenuItem, IconButton } from '@mui/material';
8 | import DeleteIcon from '@mui/icons-material/Delete';
9 | import RestoreFromTrashIcon from '@mui/icons-material/RestoreFromTrash';
10 | import AddIcon from '@mui/icons-material/Add';
11 | import { red, blue } from '@mui/material/colors';
12 |
13 | const Input = ({ value, fieldId, onChange, innerProps: { propertyValidation = {} }, isDisabled, helperText, ...rest }) => (
14 | onChange(e.target.value)}
18 | disabled={isDisabled}
19 | helperText={propertyValidation.message || helperText}
20 | error={Boolean(propertyValidation.message)}
21 | {...rest}
22 | />
23 | );
24 |
25 | Input.propTypes = {
26 | label: PropTypes.oneOfType([PropTypes.string]).isRequired,
27 | helperText: PropTypes.node,
28 | value: PropTypes.any,
29 | fieldId: PropTypes.string.isRequired,
30 | innerProps: PropTypes.shape({
31 | propertyValidation: PropTypes.shape({ message: PropTypes.string }),
32 | }).isRequired,
33 | onChange: PropTypes.func,
34 | isDisabled: PropTypes.bool,
35 | };
36 |
37 | Input.defaultProps = {
38 | onChange: () => {},
39 | value: '',
40 | };
41 |
42 | const PropertySwitch = ({ label, value, fieldId, innerProps: { propertyValidation = {} }, helperText, onChange, isDisabled }) => (
43 |
44 |
45 | onChange(value)} value={value} />}
49 | label={label}
50 | />
51 | {propertyValidation.message || helperText}
52 |
53 |
54 | );
55 |
56 | PropertySwitch.propTypes = {
57 | label: PropTypes.string.isRequired,
58 | value: PropTypes.any,
59 | fieldId: PropTypes.string.isRequired,
60 | innerProps: PropTypes.shape({
61 | propertyValidation: PropTypes.shape({ message: PropTypes.string }),
62 | }).isRequired,
63 | onChange: PropTypes.func,
64 | isDisabled: PropTypes.bool,
65 | helperText: PropTypes.node,
66 | };
67 |
68 | PropertySwitch.defaultProps = {
69 | value: false,
70 | };
71 |
72 | const PropertySelect = ({ value, fieldId, onChange, innerProps: { propertyValidation = {} }, isDisabled, helperText, options, ...rest }) => (
73 | onChange(e.target.value)}
78 | disabled={isDisabled}
79 | helperText={propertyValidation.message || helperText}
80 | error={Boolean(propertyValidation.message)}
81 | {...rest}
82 | >
83 | {options.map((option) => (
84 |
87 | ))}
88 |
89 | );
90 |
91 | PropertySelect.propTypes = {
92 | label: PropTypes.oneOfType([PropTypes.string]).isRequired,
93 | helperText: PropTypes.node,
94 | value: PropTypes.any,
95 | fieldId: PropTypes.string.isRequired,
96 | innerProps: PropTypes.shape({
97 | propertyValidation: PropTypes.shape({ message: PropTypes.string }),
98 | }).isRequired,
99 | onChange: PropTypes.func,
100 | isDisabled: PropTypes.bool,
101 | options: PropTypes.arrayOf(PropTypes.string),
102 | };
103 |
104 | PropertySelect.defaultProps = {
105 | onChange: () => {},
106 | options: [],
107 | };
108 |
109 | const Root = styled('div')(() => ({
110 | '& .remove': {
111 | '&:hover': {
112 | color: red[500],
113 | },
114 | },
115 | '& .restore': {
116 | '&:hover': {
117 | color: blue[500],
118 | },
119 | },
120 | '& .cell': {
121 | '&:not(:last-child)': {
122 | 'padding-right': 8,
123 | },
124 | },
125 | }));
126 |
127 | const PropertyOptions = ({ value = [], label, onChange, innerProps: { restricted } }) => {
128 | const handleOptionChange = (option, index, optionKey) =>
129 | onChange(value.map((item, itemIndex) => (index === itemIndex ? { ...item, [optionKey]: option } : item)));
130 |
131 | const handleRemove = (index, restoreable) => {
132 | let options;
133 |
134 | if (restoreable) {
135 | options = value.map((item, itemIndex) =>
136 | itemIndex === index
137 | ? {
138 | ...item,
139 | deleted: !item.deleted,
140 | }
141 | : item
142 | );
143 | } else {
144 | options = value.filter((_item, itemIndex) => itemIndex !== index);
145 | }
146 | return onChange(options.length > 0 ? options : undefined);
147 | };
148 |
149 | return (
150 |
151 |
158 | {label}
159 | {!restricted && (
160 | onChange([...value, { value: '', label: '' }])} aria-label="add option" size="large">
161 |
162 |
163 | )}
164 |
165 |
166 |
167 | {value.map(({ label, value, restoreable, deleted }, index, allOptions) => (
168 |
169 |
170 | handleOptionChange(e.target.value, index, 'label')}
173 | disabled={deleted}
174 | placeholder="Label"
175 | aria-label={`option-label-${index}`}
176 | />
177 | |
178 |
179 | {
181 | if (key === 'Enter' && index === allOptions.length - 1) {
182 | onChange([...allOptions, { value: '', label: '' }]);
183 | }
184 | }}
185 | value={value || ''}
186 | onChange={(e) => handleOptionChange(e.target.value, index, 'value')}
187 | disabled={deleted}
188 | placeholder="Value"
189 | aria-label={`option-value-${index}`}
190 | />
191 | |
192 |
193 | handleRemove(index, restoreable)} variant="plain" aria-label="delete option" size="large">
194 | {deleted ? : }
195 |
196 | |
197 |
198 | ))}
199 |
200 |
201 |
202 | );
203 | };
204 |
205 | PropertyOptions.propTypes = {
206 | value: PropTypes.array,
207 | label: PropTypes.string.isRequired,
208 | onChange: PropTypes.func.isRequired,
209 | innerProps: PropTypes.shape({ restricted: PropTypes.bool }).isRequired,
210 | };
211 |
212 | const Textarea = (props) => ;
213 |
214 | const propertiesMapper = {
215 | input: Input,
216 | switch: PropertySwitch,
217 | select: PropertySelect,
218 | options: PropertyOptions,
219 | textarea: Textarea,
220 | };
221 |
222 | export default propertiesMapper;
223 |
--------------------------------------------------------------------------------
/src/pf4-builder-mappers/builder-mapper.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { componentTypes } from '@data-driven-forms/react-form-renderer';
4 |
5 | import { Button, Card, CardBody, CardHeader, Form, FormGroup, Title, Tab, Tabs } from '@patternfly/react-core';
6 | import { TrashIcon, TimesIcon, GripVerticalIcon, EyeSlashIcon, ExclamationCircleIcon } from '@patternfly/react-icons';
7 | import clsx from 'clsx';
8 | import { InternalSelect } from '@data-driven-forms/pf4-component-mapper/select';
9 | import { builderComponentTypes } from '../constants';
10 |
11 | const prepareLabel = (component, isDragging) =>
12 | ({
13 | [componentTypes.CHECKBOX]: 'Please, provide label',
14 | [componentTypes.PLAIN_TEXT]: 'Please provide a label to plain text component',
15 | [componentTypes.DUAL_LIST_SELECT]: 'Please pick label and options',
16 | [componentTypes.RADIO]: 'Please pick label and options',
17 | }[component] || (isDragging ? component : ''));
18 |
19 | const prepareOptions = (component, options = []) =>
20 | ({
21 | [componentTypes.SELECT]: { options: options.filter(({ deleted }) => !deleted) },
22 | [componentTypes.DUAL_LIST_SELECT]: { options },
23 | [componentTypes.RADIO]: { options },
24 | }[component] || {});
25 |
26 | const ComponentWrapper = ({
27 | innerProps: { hideField, snapshot },
28 | Component,
29 | propertyName,
30 | fieldId,
31 | propertyValidation,
32 | hasPropertyError,
33 | ...props
34 | }) => (
35 |
49 | );
50 |
51 | const snapshotPropType = PropTypes.shape({ isDragging: PropTypes.bool }).isRequired;
52 | const childrenPropType = PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]);
53 |
54 | ComponentWrapper.propTypes = {
55 | Component: PropTypes.elementType,
56 | component: PropTypes.string,
57 | innerProps: PropTypes.shape({
58 | snapshot: snapshotPropType,
59 | hideField: PropTypes.bool,
60 | }).isRequired,
61 | label: PropTypes.string,
62 | preview: PropTypes.bool,
63 | id: PropTypes.string,
64 | initialized: PropTypes.bool,
65 | options: PropTypes.arrayOf(PropTypes.shape({ value: PropTypes.any, label: PropTypes.string })),
66 | propertyName: PropTypes.string,
67 | fieldId: PropTypes.string,
68 | propertyValidation: PropTypes.any,
69 | hasPropertyError: PropTypes.bool,
70 | };
71 |
72 | const FieldLayout = ({ children, disableDrag, selected }) => (
73 |
74 |
80 | {children}
81 |
82 |
83 | );
84 |
85 | FieldLayout.propTypes = {
86 | children: childrenPropType,
87 | disableDrag: PropTypes.bool,
88 | selected: PropTypes.bool,
89 | };
90 |
91 | const BuilderColumn = ({ children, isDraggingOver, ...props }) => (
92 |
93 | {children}
94 |
95 | );
96 |
97 | BuilderColumn.propTypes = {
98 | className: PropTypes.string,
99 | children: childrenPropType,
100 | isDraggingOver: PropTypes.bool,
101 | };
102 |
103 | const PropertiesEditor = ({
104 | propertiesChildren,
105 | validationChildren,
106 | addValidator,
107 | avaiableValidators,
108 | handleClose,
109 | handleDelete,
110 | hasPropertyError,
111 | disableValidators,
112 | }) => {
113 | const [activeTab, setActiveTab] = useState(0);
114 | const Select = InternalSelect;
115 |
116 | useEffect(() => {
117 | if (activeTab === 1 && disableValidators) {
118 | setActiveTab(0);
119 | }
120 | }, [disableValidators]);
121 |
122 | return (
123 |
124 |
125 |
126 |
127 | Properties editor
128 | {handleDelete && (
129 |
132 | )}
133 |
136 |
137 |
138 |
139 |
140 |
141 | setActiveTab(tabIndex)}>
142 | Properties {hasPropertyError && }}
146 | />
147 | {!disableValidators && }
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
171 |
172 |
173 | {validationChildren}
174 |
175 |
176 | );
177 | };
178 |
179 | PropertiesEditor.propTypes = {
180 | propertiesChildren: childrenPropType,
181 | validationChildren: childrenPropType,
182 | avaiableValidators: PropTypes.arrayOf(PropTypes.shape({ label: PropTypes.string, value: PropTypes.string })).isRequired,
183 | addValidator: PropTypes.func.isRequired,
184 | fieldName: PropTypes.string,
185 | handleClose: PropTypes.func.isRequired,
186 | handleDelete: PropTypes.func,
187 | hasPropertyError: PropTypes.array,
188 | disableValidators: PropTypes.bool,
189 | };
190 |
191 | const PropertyGroup = ({ className, children, title, handleDelete, ...props }) => (
192 |
193 |
194 |
195 | {title}
196 | {handleDelete && (
197 |
200 | )}
201 |
202 |
203 |
204 |
207 |
208 |
209 | );
210 |
211 | PropertyGroup.propTypes = {
212 | className: PropTypes.string,
213 | children: childrenPropType,
214 | title: PropTypes.string.isRequired,
215 | handleDelete: PropTypes.func,
216 | };
217 |
218 | const DragHandle = ({ dragHandleProps, hasPropertyError, disableDrag }) => {
219 | if (disableDrag && !hasPropertyError) {
220 | return null;
221 | }
222 | return (
223 |
224 | {hasPropertyError && }
225 | {!disableDrag && }
226 |
227 | );
228 | };
229 |
230 | DragHandle.propTypes = {
231 | dragHandleProps: PropTypes.shape({
232 | 'data-rbd-drag-handle-draggable-id': PropTypes.string,
233 | 'data-rbd-drag-handle-context-id': PropTypes.string,
234 | 'aria-labelledby': PropTypes.string,
235 | tabIndex: PropTypes.number,
236 | draggable: PropTypes.bool,
237 | onDragStart: PropTypes.func,
238 | }),
239 | disableDrag: PropTypes.bool,
240 | hasPropertyError: PropTypes.bool,
241 | };
242 |
243 | const FormContainer = ({ children, className }) => {children}
;
244 |
245 | FormContainer.propTypes = {
246 | children: childrenPropType,
247 | className: PropTypes.string,
248 | };
249 |
250 | const EmptyTarget = () => Pick components from the component picker
;
251 |
252 | const builderMapper = {
253 | FieldLayout,
254 | PropertiesEditor,
255 | FormContainer,
256 | [builderComponentTypes.BUILDER_FIELD]: ComponentWrapper,
257 | BuilderColumn,
258 | PropertyGroup,
259 | DragHandle,
260 | EmptyTarget,
261 | };
262 |
263 | export default builderMapper;
264 |
--------------------------------------------------------------------------------
/src/pf4-builder-mappers/builder-template.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const BuilderTemplate = ({ ComponentPicker, PropertiesEditor, DropTarget, children }) => (
5 |
13 | );
14 |
15 | BuilderTemplate.propTypes = {
16 | ComponentPicker: PropTypes.func.isRequired,
17 | PropertiesEditor: PropTypes.func.isRequired,
18 | DropTarget: PropTypes.func.isRequired,
19 | children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]),
20 | };
21 |
22 | export default BuilderTemplate;
23 |
--------------------------------------------------------------------------------
/src/pf4-builder-mappers/field-properties.js:
--------------------------------------------------------------------------------
1 | export const LABEL = {
2 | propertyName: 'label',
3 | label: 'Label',
4 | component: 'input',
5 | };
6 |
7 | export const HELPER_TEXT = {
8 | propertyName: 'helperText',
9 | label: 'Helper text',
10 | component: 'input',
11 | };
12 |
13 | export const PLACEHOLDER = {
14 | propertyName: 'placeholder',
15 | label: 'Placeholder',
16 | component: 'input',
17 | };
18 |
19 | export const INPUT_TYPE = {
20 | label: 'Input Type',
21 | propertyName: 'type',
22 | options: ['text', 'number', 'password'],
23 | component: 'select',
24 | };
25 |
26 | export const IS_DISABLED = {
27 | propertyName: 'isDisabled',
28 | label: 'Disabled',
29 | component: 'switch',
30 | };
31 |
32 | export const IS_READ_ONLY = {
33 | propertyName: 'isReadOnly',
34 | label: 'Read only',
35 | component: 'switch',
36 | };
37 |
38 | export const OPTIONS = {
39 | propertyName: 'options',
40 | label: 'Options',
41 | component: 'options',
42 | };
43 |
44 | export const IS_CLEARABLE = {
45 | propertyName: 'isClearable',
46 | label: 'Clearable',
47 | component: 'switch',
48 | };
49 |
50 | export const CLOSE_ON_DAY_SELECT = {
51 | propertyName: 'closeOnDaySelect',
52 | label: 'Close on day select',
53 | component: 'switch',
54 | };
55 |
56 | export const SHOW_TODAY_BUTTON = {
57 | propertyName: 'showTodayButton',
58 | label: 'Show today button',
59 | component: 'switch',
60 | };
61 |
62 | export const TODAY_BUTTON_LABEL = {
63 | propertyName: 'todayButtonLabel',
64 | label: 'Today button label',
65 | component: 'input',
66 | };
67 |
68 | export const MULTI_LINE_LABEL = {
69 | propertyName: 'label',
70 | label: 'Label',
71 | component: 'textarea',
72 | };
73 |
74 | export const TITLE = {
75 | propertyName: 'title',
76 | label: 'Title',
77 | component: 'input',
78 | };
79 |
80 | export const DESCRIPTION = {
81 | propertyName: 'description',
82 | label: 'Description',
83 | component: 'input',
84 | };
85 |
86 | export const HIDE_FIELD = {
87 | propertyName: 'hideField',
88 | label: 'Hidden',
89 | component: 'switch',
90 | };
91 |
--------------------------------------------------------------------------------
/src/pf4-builder-mappers/index.js:
--------------------------------------------------------------------------------
1 | export * as fieldProperties from './field-properties';
2 | export { default as builderMapper } from './builder-mapper';
3 | export { default as pickerMapper } from './picker-mapper';
4 | export { default as propertiesMapper } from './properties-mapper';
5 | export { default as BuilderTemplate } from './builder-template';
6 |
--------------------------------------------------------------------------------
/src/pf4-builder-mappers/pf4-mapper-style.css:
--------------------------------------------------------------------------------
1 | .pf4-picker-root {
2 | width: 100%;
3 | margin-bottom: 8px;
4 | }
5 | .pf4-picker-root>button {
6 | width: 100%;
7 | pointer-events: none;
8 | }
9 | .pf4-picker-root:hover > button {
10 | background-color: var(--pf-c-button--m-primary--hover--BackgroundColor);
11 | }
12 | .pf4-builder-form-container {
13 | flex-grow: 1;
14 | width: 30%;
15 | margin-top: 16px;
16 | margin-left: 16px;
17 | margin-right: 16px;
18 | }
19 | .pf4-component-wrapper {
20 | flex-grow: 1;
21 | position: relative;
22 | padding: 8px;
23 | }
24 |
25 | .pf4-field-actions {
26 | display: flex;
27 | flex-direction: column;
28 | justify-content: center;
29 | position: relative;
30 | }
31 | .pf4-danger-color:hover{
32 | background-color: var(--pf-global--danger-color--100);
33 | }
34 | button:hover>svg.pf4-danger-color {
35 | fill: var(--pf-global--danger-color--100);
36 | }
37 | .pf4-options-propery-editor-cell:not(:last-child) {
38 | padding-right: 8px;
39 | }
40 | .pf4-properties-editor-title {
41 | display: flex;
42 | }
43 | .pf4-properties-editor-title .editor-header-button:first-child {
44 | margin-left: auto
45 | }
46 | .pf4-validators-validator-title {
47 | font-weight: 600;
48 | display: block;
49 | justify-content: space-between;
50 | align-items: center;
51 | }
52 | .pf4-validators-validator-title:first-letter {
53 | text-transform: uppercase;
54 | }
55 | .pf4-tabs-container {
56 | padding: 0 !important;
57 | }
58 | .pf4-field-layout.selected {
59 | border-bottom: 3px solid var(--pf-global--primary-color--100);
60 | transition: border-color .3s linear;
61 | }
62 | .pf4-validators-property-group:not(:last-child) {
63 | border-bottom: 2px solid #ebebeb;
64 | }
65 | .pf4-field-layout {
66 | cursor: pointer;
67 | background-color: #fff;
68 | flex-grow: 1;
69 | box-shadow: var(--pf-global--BoxShadow--sm);
70 | display: flex;
71 | }
72 | .pf4-field-layout.drag-disabled {
73 | border-right: 1px solid lightgray;
74 | }
75 | .pf4-drag-handle {
76 | position: relative;
77 | padding: 4px;
78 | padding-top: 8px;
79 | border-left: none;
80 | background-color: var(--pf-global--disabled-color--300);
81 | display: flex;
82 | flex-direction: column;
83 | min-width: 25px;
84 | }
85 | .pf4-drag-handle .pf4-drag-handle-icon {
86 | fill: #B9B9B9;
87 | transition: fill .3s linear;
88 | }
89 | .pf4-field-layout.selected .pf4-drag-handle .pf4-drag-handle-icon, .pf4-field-layout:hover .pf4-drag-handle .pf4-drag-handle-icon {
90 | fill: var(--pf-global--primary-color--100);
91 | }
92 | .pf4-properties-editor-container {
93 | width: 33%;
94 | min-width: 400px;
95 | box-shadow: var(--pf-global--BoxShadow--md);
96 | min-height: 100%;
97 | background-color: #fff;
98 | }
99 | .pf4-properties-editor-container .pf-c-card {
100 | box-shadow: none;
101 | }
102 | .pf4-options-property-editor-table tr:not(:last-child) td {
103 | padding-bottom: 8px;
104 | }
105 | button:hover>svg.pf4-success-color{
106 | fill: var(--pf-global--link--Color);
107 | }
108 | @keyframes overlay-transition {
109 | from {
110 | background-color: rgba(200,200,200,.0);
111 | }
112 | to {
113 | background-color: rgba(200,200,200,.6);
114 | }
115 | }
116 |
117 | .pf4-hidefield-overlay::before {
118 | position: absolute;
119 | content: "";
120 | width: 100%;
121 | height: 100%;
122 | top: 0;
123 | left: 0;
124 | transition: background-color linear .3s;
125 | background-color: transparent;
126 | pointer-events: none;
127 | }
128 | .pf4-component-wrapper.hidden .pf4-hidefield-overlay::before {
129 | background-color: rgba(200,200,200,.6);
130 | display: flex;
131 | justify-content: center;
132 | align-items: center;
133 | pointer-events: fill;
134 | }
135 | .pf4-component-wrapper svg.hide-indicator{
136 | position: absolute;
137 | left: calc(100% / 2);
138 | top: calc(100% / 2 - 1.5em);
139 | opacity: 0;
140 | pointer-events: none;
141 | transition: opacity linear .3s;
142 | }
143 |
144 | .pf4-component-wrapper.hidden svg.hide-indicator{
145 | opacity: 1;
146 | }
147 |
148 | .pf4-property-error-icon {
149 | fill: var(--pf-global--danger-color--100)
150 | }
151 |
152 | .icon-spacer-bottom {
153 | margin-bottom: 8px;
154 | }
155 |
156 | .pf4-form-builder-layout-template {
157 | display: flex;
158 | flex-direction: column;
159 | height: 100%;
160 | }
161 | .pf4-form-builder-components {
162 | display: flex;
163 | flex-grow: 1;
164 | }
--------------------------------------------------------------------------------
/src/pf4-builder-mappers/picker-mapper.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { componentTypes } from '@data-driven-forms/react-form-renderer';
4 | import { Button } from '@patternfly/react-core';
5 |
6 | import './pf4-mapper-style.css';
7 | import { builderComponentTypes } from '../constants';
8 |
9 | const labels = {
10 | [componentTypes.TEXT_FIELD]: 'Text field',
11 | [componentTypes.CHECKBOX]: 'Checkbox',
12 | [componentTypes.SELECT]: 'Select',
13 | [componentTypes.DATE_PICKER]: 'Date picker',
14 | [componentTypes.PLAIN_TEXT]: 'Plain text',
15 | [componentTypes.RADIO]: 'Radio',
16 | [componentTypes.SWITCH]: 'Switch',
17 | [componentTypes.TEXTAREA]: 'Textarea',
18 | [componentTypes.SUB_FORM]: 'Sub form',
19 | [componentTypes.DUAL_LIST_SELECT]: 'Dual list select',
20 | [componentTypes.SLIDER]: 'Slider',
21 | };
22 |
23 | const PickerRoot = ({ component }) => (
24 |
25 |
28 |
29 | );
30 |
31 | PickerRoot.propTypes = {
32 | component: PropTypes.string.isRequired,
33 | };
34 |
35 | const pickerMapper = {
36 | [builderComponentTypes.PICKER_FIELD]: PickerRoot,
37 | };
38 |
39 | export default pickerMapper;
40 |
--------------------------------------------------------------------------------
/src/pf4-builder-mappers/properties-mapper.js:
--------------------------------------------------------------------------------
1 | /* eslint react/no-array-index-key: "off" */
2 | import React, { Fragment } from 'react';
3 | import PropTypes from 'prop-types';
4 | import { Switch, Button, FormGroup, TextArea, TextInput } from '@patternfly/react-core';
5 | import { TrashIcon, PlusIcon, TrashRestoreIcon } from '@patternfly/react-icons';
6 | import { InternalSelect } from '@data-driven-forms/pf4-component-mapper/select';
7 |
8 | const FormGroupWrapper = ({ propertyValidation: { message }, children, ...props }) => (
9 |
10 | {children}
11 |
12 | );
13 |
14 | FormGroupWrapper.propTypes = {
15 | propertyValidation: PropTypes.shape({ message: PropTypes.string }),
16 | children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
17 | };
18 |
19 | FormGroupWrapper.defaultProps = {
20 | propertyValidation: {},
21 | };
22 |
23 | const Input = ({ label, value, fieldId, innerProps: { propertyValidation }, ...rest }) => {
24 | return (
25 |
26 |
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | Input.propTypes = {
34 | label: PropTypes.oneOfType([PropTypes.string]).isRequired,
35 | value: PropTypes.any,
36 | fieldId: PropTypes.string.isRequired,
37 | innerProps: PropTypes.shape({
38 | propertyValidation: PropTypes.shape({ message: PropTypes.string }),
39 | }).isRequired,
40 | };
41 |
42 | Input.defaultProps = {
43 | onChange: () => {},
44 | value: '',
45 | };
46 |
47 | const PropertySwitch = ({ value, fieldId, innerProps: { propertyValidation }, ...rest }) => (
48 |
49 |
50 |
51 | );
52 |
53 | PropertySwitch.propTypes = {
54 | label: PropTypes.string.isRequired,
55 | value: PropTypes.any,
56 | fieldId: PropTypes.string.isRequired,
57 | innerProps: PropTypes.shape({
58 | propertyValidation: PropTypes.shape({ message: PropTypes.string }),
59 | }).isRequired,
60 | };
61 |
62 | PropertySwitch.defaultProps = {
63 | value: false,
64 | };
65 |
66 | const PropertySelect = ({ label, options, fieldId, innerProps: { propertyValidation }, ...rest }) => (
67 |
68 | ({ value: option, label: option }))} {...rest} />
69 |
70 | );
71 |
72 | PropertySelect.propTypes = {
73 | label: PropTypes.string.isRequired,
74 | options: PropTypes.arrayOf(PropTypes.string).isRequired,
75 | fieldId: PropTypes.string.isRequired,
76 | innerProps: PropTypes.shape({
77 | propertyValidation: PropTypes.shape({ message: PropTypes.string }),
78 | }).isRequired,
79 | };
80 |
81 | PropertySelect.defaultProps = {
82 | onChange: () => {},
83 | };
84 |
85 | const PropertyOptions = ({ value = [], label, onChange, innerProps: { restricted } }) => {
86 | const handleOptionChange = (option, index, optionKey) =>
87 | onChange(value.map((item, itemIndex) => (index === itemIndex ? { ...item, [optionKey]: option } : item)));
88 | const handleRemove = (index, restoreable) => {
89 | let options;
90 | if (restoreable) {
91 | options = value.map((item, itemIndex) =>
92 | itemIndex === index
93 | ? {
94 | ...item,
95 | deleted: !item.deleted,
96 | }
97 | : item
98 | );
99 | } else {
100 | options = value.filter((_item, itemIndex) => itemIndex !== index);
101 | }
102 | return onChange(options.length > 0 ? options : undefined);
103 | };
104 | return (
105 |
106 |
113 | {label}
114 | {!restricted && (
115 |
118 | )}
119 |
120 |
121 |
122 | {value.map(({ label, value, restoreable, deleted }, index, allOptions) => (
123 |
124 |
125 | handleOptionChange(value, index, 'label')}
130 | value={label || ''}
131 | type="text"
132 | />
133 | |
134 |
135 | {
138 | if (key === 'Enter' && index === allOptions.length - 1) {
139 | onChange([...allOptions, { value: '', label: '' }]);
140 | }
141 | }}
142 | placeholder="Value"
143 | isDisabled={deleted || restricted}
144 | onChange={(value) => handleOptionChange(value, index, 'value')}
145 | value={value || ''}
146 | type="text"
147 | />
148 | |
149 |
150 |
153 | |
154 |
155 | ))}
156 |
157 |
158 |
159 | );
160 | };
161 |
162 | PropertyOptions.propTypes = {
163 | value: PropTypes.array,
164 | label: PropTypes.string.isRequired,
165 | onChange: PropTypes.func.isRequired,
166 | innerProps: PropTypes.shape({ restricted: PropTypes.bool }).isRequired,
167 | };
168 |
169 | const Textarea = ({ value, fieldId, innerProps: { propertyValidation }, ...rest }) => {
170 | return (
171 |
172 |
173 |
174 | );
175 | };
176 |
177 | Textarea.propTypes = {
178 | value: PropTypes.string,
179 | fieldId: PropTypes.string.isRequired,
180 | innerProps: PropTypes.shape({
181 | propertyValidation: PropTypes.shape({ message: PropTypes.string }),
182 | }).isRequired,
183 | };
184 |
185 | Textarea.defaultProps = {
186 | onChange: () => {},
187 | value: '',
188 | };
189 |
190 | const propertiesMapper = {
191 | input: Input,
192 | switch: PropertySwitch,
193 | select: PropertySelect,
194 | options: PropertyOptions,
195 | textarea: Textarea,
196 | };
197 |
198 | export default propertiesMapper;
199 |
--------------------------------------------------------------------------------
/src/picker-field/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './picker-field';
2 | export * from './picker-field';
3 |
--------------------------------------------------------------------------------
/src/picker-field/picker-field.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, Fragment, memo } from 'react';
2 | import { Draggable } from 'react-beautiful-dnd';
3 | import PropTypes from 'prop-types';
4 | import ComponentsContext from '../components-context';
5 | import { builderComponentTypes } from '../constants';
6 |
7 | const PickerField = memo(
8 | ({ field, index }) => {
9 | const { pickerMapper, builderMapper, componentMapper } = useContext(ComponentsContext);
10 | const Component = pickerMapper[field.component] || pickerMapper[builderComponentTypes.PICKER_FIELD];
11 | const Clone = builderMapper[field.component] || builderMapper[builderComponentTypes.BUILDER_FIELD];
12 | return (
13 |
14 | {(provided, snapshot) => (
15 |
16 |
17 | {snapshot.isDragging && field.clone ? (
18 |
26 | ) : (
27 |
28 | )}
29 |
30 | {snapshot.isDragging && }
31 |
32 | )}
33 |
34 | );
35 | },
36 | (prevProps, nextProps) => prevProps.index === nextProps.index
37 | );
38 |
39 | PickerField.propTypes = {
40 | index: PropTypes.number.isRequired,
41 | field: PropTypes.shape({
42 | id: PropTypes.string.isRequired,
43 | component: PropTypes.string.isRequired,
44 | clone: PropTypes.bool,
45 | }).isRequired,
46 | };
47 |
48 | export default PickerField;
49 |
--------------------------------------------------------------------------------
/src/properties-editor/convert-initial-value.js:
--------------------------------------------------------------------------------
1 | import { dataTypes } from '@data-driven-forms/react-form-renderer';
2 |
3 | const castToBoolean = (value) => {
4 | if (typeof value === 'boolean') {
5 | return value;
6 | }
7 |
8 | return value === 'true';
9 | };
10 |
11 | const convertType = (dataType, value) =>
12 | ({
13 | [dataTypes.INTEGER]: !isNaN(Number(value)) && parseInt(value),
14 | [dataTypes.FLOAT]: !isNaN(Number(value)) && parseFloat(value),
15 | [dataTypes.NUMBER]: Number(value),
16 | [dataTypes.BOOLEAN]: castToBoolean(value),
17 | }[dataType] || value);
18 |
19 | const convertInitialValue = (initialValue, dataType) => {
20 | if (initialValue === undefined || !dataType) {
21 | return initialValue;
22 | }
23 |
24 | if (Array.isArray(initialValue)) {
25 | return initialValue.map((value) =>
26 | typeof value === 'object'
27 | ? {
28 | ...value,
29 | value: Object.prototype.hasOwnProperty.call(value, 'value') ? convertType(dataType, value.value) : value,
30 | }
31 | : convertType(dataType, value)
32 | );
33 | }
34 |
35 | return convertType(dataType, initialValue);
36 | };
37 |
38 | export default convertInitialValue;
39 |
--------------------------------------------------------------------------------
/src/properties-editor/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './properties-editor';
2 | export * from './properties-editor';
3 |
--------------------------------------------------------------------------------
/src/properties-editor/initial-value-checker.js:
--------------------------------------------------------------------------------
1 | const propertyStrings = {
2 | isRequired: 'required',
3 | isDisabled: 'disabled',
4 | isReadOnly: 'read only',
5 | hideField: 'hidden',
6 | };
7 |
8 | const initialValueCheckMessage = ({ isDisabled, isReadOnly, hideField }) => {
9 | return `Initial value must be set if field is required and at the same time ${Object.entries({
10 | isDisabled,
11 | isReadOnly,
12 | hideField,
13 | })
14 | .filter(([, value]) => value)
15 | .map(([key]) => propertyStrings[key])
16 | .join(' or ')}.`;
17 | };
18 |
19 | const initialValueCheck = ({ initialValue, isRequired, isDisabled, isReadOnly, hideField }) =>
20 | !initialValue && isRequired && (isDisabled || isReadOnly || hideField)
21 | ? {
22 | initialValue: {
23 | message: initialValueCheckMessage({
24 | isDisabled,
25 | isReadOnly,
26 | hideField,
27 | }),
28 | code: 'errors.initialValue',
29 | codeDependencies: {
30 | isRequired,
31 | isDisabled,
32 | isReadOnly,
33 | hideField,
34 | },
35 | },
36 | }
37 | : {
38 | initialValue: undefined,
39 | };
40 |
41 | const propertyValidationMapper = {
42 | isDisabled: initialValueCheck,
43 | isReadOnly: initialValueCheck,
44 | hideField: initialValueCheck,
45 | initialValue: initialValueCheck,
46 | };
47 |
48 | const propertiesValidation = (type) => {
49 | const validation = propertyValidationMapper[type];
50 | return validation ? validation : () => ({});
51 | };
52 |
53 | export default propertiesValidation;
54 |
--------------------------------------------------------------------------------
/src/properties-editor/memoized-property.js:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { useSelector, shallowEqual } from 'react-redux';
4 |
5 | const PropertyComponent = memo(
6 | ({
7 | Component,
8 | property: { label, value, options, ...property },
9 | selectedComponent,
10 | handlePropertyChange,
11 | restricted,
12 | propertyValidation,
13 | ...props
14 | }) => {
15 | const innerProps = {
16 | property,
17 | restricted,
18 | propertyValidation,
19 | selectedComponent,
20 | };
21 | return (
22 | handlePropertyChange(value, property.propertyName)}
30 | />
31 | );
32 | },
33 | (prevProps, nextProps) =>
34 | prevProps.value === nextProps.value &&
35 | prevProps.restricted === nextProps.restricted &&
36 | prevProps.selectedComponent === nextProps.selectedComponent &&
37 | shallowEqual(prevProps.propertyValidation, nextProps.propertyValidation)
38 | );
39 |
40 | PropertyComponent.propTypes = {
41 | Component: PropTypes.oneOfType([PropTypes.node, PropTypes.func, PropTypes.element]).isRequired,
42 | field: PropTypes.shape({
43 | restricted: PropTypes.bool,
44 | }),
45 | handlePropertyChange: PropTypes.func.isRequired,
46 | property: PropTypes.shape({
47 | propertyName: PropTypes.string.isRequired,
48 | label: PropTypes.string,
49 | value: PropTypes.any,
50 | options: PropTypes.array,
51 | }).isRequired,
52 | restricted: PropTypes.bool,
53 | propertyValidation: PropTypes.shape({
54 | propertyValidation: PropTypes.object,
55 | }),
56 | selectedComponent: PropTypes.string,
57 | };
58 |
59 | const MemoizedProperty = (props) => {
60 | const { value, restricted, propertyValidation } = useSelector(({ fields, selectedComponent }) => {
61 | const field = fields[selectedComponent];
62 | return {
63 | value: field[props.property.propertyName],
64 | restricted: field.restricted,
65 | propertyValidation: field.propertyValidation && field.propertyValidation[props.property.propertyName],
66 | };
67 | }, shallowEqual);
68 | return ;
69 | };
70 |
71 | MemoizedProperty.propTypes = {
72 | property: PropTypes.shape({
73 | propertyName: PropTypes.string.isRequired,
74 | }).isRequired,
75 | };
76 |
77 | export default MemoizedProperty;
78 |
--------------------------------------------------------------------------------
/src/properties-editor/memozied-validator.js:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import PropTypes from 'prop-types';
3 | import ValidatorProperty from './validator-property';
4 | import { useSelector, shallowEqual } from 'react-redux';
5 |
6 | const ValidatorComponent = memo(
7 | ({ handleValidatorChange, property, original, validator, restricted, index }) => (
8 |
18 | ),
19 | (prevProps, nextProps) => {
20 | const key = nextProps.property.propertyName;
21 | return prevProps.validator[key] === nextProps.validator[key];
22 | }
23 | );
24 |
25 | ValidatorComponent.propTypes = {
26 | handleValidatorChange: PropTypes.func.isRequired,
27 | property: PropTypes.shape({
28 | propertyName: PropTypes.string.isRequired,
29 | }).isRequired,
30 | original: PropTypes.object,
31 | validator: PropTypes.object,
32 | index: PropTypes.number.isRequired,
33 | restricted: PropTypes.bool,
34 | };
35 |
36 | const MemoizedValidator = (props) => {
37 | const restricted = useSelector(({ fields, selectedComponent }) => fields[selectedComponent].restricted, shallowEqual);
38 | return ;
39 | };
40 |
41 | export default MemoizedValidator;
42 |
--------------------------------------------------------------------------------
/src/properties-editor/properties-editor.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState, useEffect, Fragment } from 'react';
2 | import { validatorTypes } from '@data-driven-forms/react-form-renderer';
3 | import { useForm } from 'react-final-form';
4 | import ComponentsContext from '../components-context';
5 | import validatorsProperties from '../validators-properties';
6 | import MemoizedProperty from './memoized-property';
7 | import MemoizedValidator from './memozied-validator';
8 | import { useDispatch, useSelector, shallowEqual } from 'react-redux';
9 | import { SET_FIELD_VALIDATOR, SET_FIELD_PROPERTY, SET_SELECTED_COMPONENT, REMOVE_COMPONENT } from '../builder-store';
10 | import convertInitialValue from './convert-initial-value';
11 | import { FORM_LAYOUT } from '../helpers';
12 |
13 | const validatorOptions = Object.keys(validatorTypes)
14 | .filter((key) => validatorTypes[key] !== validatorTypes.REQUIRED)
15 | .map((key) => ({ value: validatorTypes[key], label: validatorTypes[key] }));
16 |
17 | const checkRequiredDisabled = (field) => {
18 | return !!(field.restricted && !!field.validate && !!field.validate.find(({ type, original }) => original && type === validatorTypes.REQUIRED));
19 | };
20 |
21 | const PropertiesEditor = () => {
22 | const form = useForm();
23 | const dispatch = useDispatch();
24 | const selectedComponent = useSelector(({ selectedComponent }) => selectedComponent, shallowEqual);
25 | const { field, dropTargets } = useSelector(
26 | ({ selectedComponent, fields, dropTargets }) => ({ field: fields[selectedComponent], dropTargets }),
27 | shallowEqual
28 | );
29 | const {
30 | builderMapper: { PropertiesEditor, PropertyGroup },
31 | componentProperties,
32 | propertiesMapper,
33 | debug,
34 | openEditor,
35 | } = useContext(ComponentsContext);
36 | const [requiredDisabled, setRequiredDisabled] = useState(true);
37 | useEffect(() => {
38 | if (selectedComponent) {
39 | setRequiredDisabled(() => checkRequiredDisabled(field));
40 | }
41 | }, [selectedComponent, field]);
42 |
43 | useEffect(() => {
44 | if (!selectedComponent && openEditor && dropTargets[FORM_LAYOUT].fieldsIds[0]) {
45 | dispatch({ type: SET_SELECTED_COMPONENT, payload: dropTargets[FORM_LAYOUT].fieldsIds[0] });
46 | }
47 | }, []);
48 |
49 | if (!selectedComponent) {
50 | return null;
51 | }
52 | const registeredFields = form?.getRegisteredFields();
53 | const interactiveField = registeredFields.includes(field.name || field.id);
54 |
55 | const properties = componentProperties[field.component].attributes;
56 | const disableInitialValue = !interactiveField || componentProperties[field.component].disableInitialValue;
57 | const disableValidators = !interactiveField || componentProperties[field.component].disableValidators;
58 |
59 | const validate = field.validate || [];
60 | const NameComponent = propertiesMapper.input;
61 | const InitialValueComponent = propertiesMapper.input;
62 | const MessageComponent = propertiesMapper.input;
63 | const IsRequiredComponent = propertiesMapper.switch;
64 |
65 | const handlePropertyChange = (value, propertyName) =>
66 | dispatch({
67 | type: SET_FIELD_PROPERTY,
68 | payload: {
69 | value: convertInitialValue(value, field.dataType),
70 | propertyName,
71 | fieldId: field.id,
72 | },
73 | });
74 |
75 | const handleValidatorChange = (value = {}, action, index) =>
76 | dispatch({
77 | type: SET_FIELD_VALIDATOR,
78 | payload: {
79 | ...value,
80 | fieldId: field.id,
81 | index,
82 | action,
83 | },
84 | });
85 |
86 | const requiredIndex = validate.reduce((acc, curr, index) => (curr.type === validatorTypes.REQUIRED ? index : acc), 0);
87 |
88 | const hasPropertyError = field.propertyValidation && Object.entries(field.propertyValidation).find(([, value]) => value);
89 |
90 | return (
91 |
92 | handleValidatorChange({ type }, 'add')}
97 | handleClose={() => dispatch({ type: SET_SELECTED_COMPONENT })}
98 | handleDelete={
99 | !field.restricted
100 | ? () =>
101 | dispatch({
102 | type: REMOVE_COMPONENT,
103 | payload: field.id,
104 | })
105 | : undefined
106 | }
107 | disableInitialValue={disableInitialValue}
108 | disableValidators={disableValidators}
109 | propertiesChildren={
110 |
111 |
119 | {!disableInitialValue && (
120 |
127 | )}
128 | {properties.map((property) => {
129 | const Component = propertiesMapper[property.component];
130 | return (
131 |
138 | );
139 | })}
140 |
141 | }
142 | validationChildren={
143 | disableValidators ? null : (
144 |
145 |
146 |
153 | handleValidatorChange(
154 | {
155 | type: validatorTypes.REQUIRED,
156 | },
157 | value ? 'add' : 'remove',
158 | requiredIndex
159 | )
160 | }
161 | />
162 | {field.isRequired && (
163 | type === validatorTypes.REQUIRED).message || ''}
168 | onChange={(value) =>
169 | handleValidatorChange(
170 | {
171 | message: value,
172 | },
173 | 'modify',
174 | requiredIndex
175 | )
176 | }
177 | />
178 | )}
179 |
180 | {validate.map(({ type, original, ...rest }, index) =>
181 | type !== validatorTypes.REQUIRED ? (
182 | handleValidatorChange({}, 'remove', index) : undefined}
185 | key={`${type}-${index}`}
186 | >
187 | {validatorsProperties[type].map((property, propertyIndex) => (
188 |
196 | ))}
197 |
198 | ) : null
199 | )}
200 |
201 | )
202 | }
203 | />
204 | {debug && {JSON.stringify(field, null, 2)}
}
205 |
206 | );
207 | };
208 |
209 | export default PropertiesEditor;
210 |
--------------------------------------------------------------------------------
/src/properties-editor/validator-property.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import PropTypes from 'prop-types';
3 | import ComponentsContext from '../components-context';
4 |
5 | const restrictionHandler = {
6 | min: (value, defaultValue) => (isNaN(value) ? defaultValue : value < defaultValue ? defaultValue : value),
7 | max: (value, defaultValue) => (isNaN(value) ? defaultValue : value > defaultValue ? defaultValue : value),
8 | };
9 |
10 | const validatorChangeValue = (property, value) => {
11 | let result = property.type === 'number' ? Number(value) : value;
12 | if (property.restriction) {
13 | result = restrictionHandler[property.restriction.inputAttribute](value, property.original[property.restriction.validatorAttribute]);
14 | }
15 | return {
16 | [property.propertyName]: result,
17 | };
18 | };
19 |
20 | const ValidatorProperty = ({ property, onChange, value, index, restricted }) => {
21 | const { propertiesMapper } = useContext(ComponentsContext);
22 | const Component = propertiesMapper[property.component];
23 | const { isDisabled, restrictionProperty } =
24 | property.restriction && property.original
25 | ? {
26 | isDisabled: property.restriction.lock,
27 | [property.restriction.inputAttribute]: property.original[property.restriction.validatorAttribute],
28 | }
29 | : {};
30 |
31 | const innerProps = {
32 | property: restrictionProperty,
33 | restricted,
34 | };
35 | return (
36 | property.propertyName !== 'message' && restricted && onChange(validatorChangeValue(property, value), 'modify', index)}
41 | fieldId={`${property.propertyName}-${index}`}
42 | onChange={(value) =>
43 | onChange(
44 | {
45 | [property.propertyName]: property.type === 'number' ? Number(value) : value,
46 | },
47 | 'modify',
48 | index
49 | )
50 | }
51 | label={property.label}
52 | innerProps={innerProps}
53 | />
54 | );
55 | };
56 |
57 | ValidatorProperty.propTypes = {
58 | restricted: PropTypes.bool,
59 | property: PropTypes.shape({
60 | original: PropTypes.object,
61 | propertyName: PropTypes.string.isRequired,
62 | component: PropTypes.string.isRequired,
63 | type: PropTypes.string,
64 | label: PropTypes.string.isRequired,
65 | restriction: PropTypes.shape({
66 | inputAttribute: PropTypes.string,
67 | validatorAttribute: PropTypes.string,
68 | lock: PropTypes.bool,
69 | }),
70 | }),
71 | onChange: PropTypes.func.isRequired,
72 | value: PropTypes.any,
73 | index: PropTypes.number.isRequired,
74 | };
75 |
76 | export default ValidatorProperty;
77 |
--------------------------------------------------------------------------------
/src/tests/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "jest": true,
4 | "es6": true
5 | },
6 | "rules": {
7 | "react/prop-types": "off"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/tests/__mocks__/builder-fields.js:
--------------------------------------------------------------------------------
1 | export const fields = {
2 | 'initial-should-not-be-in-output': {
3 | key: 'value',
4 | },
5 | 'should-remove-all-attributes': {
6 | name: 'only-name',
7 | preview: 'value',
8 | clone: 'value',
9 | initialized: 'value',
10 | id: 'value',
11 | isContainer: 'value',
12 | children: 'value',
13 | container: 'value',
14 | restricted: 'value',
15 | },
16 | '123-text-field': {
17 | name: 'foo',
18 | label: 'Text field',
19 | isRequired: true,
20 | validate: [{ type: 'required-validator' }],
21 | },
22 | };
23 |
24 | export const schemaWithDeletedOption = {
25 | 'deleted-option-field': {
26 | name: 'deleted-option-field',
27 | options: [
28 | {
29 | value: 'foo',
30 | label: 'foo',
31 | deleted: true,
32 | },
33 | {
34 | value: 'bar',
35 | label: 'bar',
36 | },
37 | ],
38 | },
39 | };
40 |
41 | export const invalidSchema = {
42 | 'invalid-field': {
43 | name: 'invalid-field',
44 | propertyValidation: {
45 | invalidProperty: {
46 | message: 'this field is invalid',
47 | },
48 | },
49 | },
50 | };
51 |
52 | export const initialBuilderFields = {
53 | 'text-field': {
54 | attributes: [
55 | {
56 | component: 'input',
57 | label: 'Label',
58 | propertyName: 'label',
59 | },
60 | ],
61 | },
62 | 'options-component': {
63 | attributes: [
64 | {
65 | component: 'options',
66 | label: 'Options',
67 | propertyName: 'options',
68 | },
69 | ],
70 | },
71 | };
72 |
73 | export const initialDDFSchema = {
74 | fields: [
75 | {
76 | name: 'first-name',
77 | component: 'text-field',
78 | validate: [
79 | {
80 | type: 'required-validator',
81 | message: 'I am required',
82 | },
83 | ],
84 | },
85 | {
86 | name: 'options-select',
87 | component: 'options-component',
88 | validate: [
89 | {
90 | type: 'required-validator',
91 | message: 'I am required',
92 | },
93 | ],
94 | options: [
95 | {
96 | label: 'First option',
97 | value: 1,
98 | },
99 | ],
100 | },
101 | ],
102 | };
103 |
104 | export const initialDDFSchemaTemplate = {
105 | fields: [
106 | {
107 | name: 'options-select',
108 | component: 'options-component',
109 | validate: [
110 | {
111 | type: 'required-validator',
112 | message: 'I am required',
113 | },
114 | ],
115 | options: [
116 | {
117 | label: 'First option',
118 | value: 1,
119 | },
120 | {
121 | label: 'Second deleted option',
122 | value: 2,
123 | },
124 | ],
125 | },
126 | ],
127 | };
128 |
--------------------------------------------------------------------------------
/src/tests/builder-store/builder-reducer.test.js:
--------------------------------------------------------------------------------
1 | import builderReducer, {
2 | SET_COLUMNS,
3 | SET_SELECTED_COMPONENT,
4 | REMOVE_COMPONENT,
5 | DRAG_START,
6 | SET_FIELD_PROPERTY,
7 | SET_FIELD_VALIDATOR,
8 | INITIALIZE,
9 | UNINITIALIZE,
10 | } from '../../builder-store/builder-reducer';
11 | import propertiesValidation from '../../properties-editor/initial-value-checker';
12 | import { FORM_LAYOUT } from '../../helpers/create-initial-data';
13 | import { validatorTypes } from '@data-driven-forms/react-form-renderer';
14 |
15 | describe('builderReducer', () => {
16 | let initialState;
17 |
18 | const containerId = '54546421684';
19 |
20 | beforeEach(() => {
21 | initialState = { initialized: false };
22 | });
23 |
24 | it('returns default', () => {
25 | expect(builderReducer(initialState, {})).toEqual(initialState);
26 | });
27 |
28 | describe(INITIALIZE, () => {
29 | it('sets initialized to true', () => {
30 | expect(
31 | builderReducer(initialState, {
32 | type: INITIALIZE,
33 | payload: { custom: 'custom_1' },
34 | })
35 | ).toEqual({
36 | custom: 'custom_1',
37 | initialized: true,
38 | });
39 | });
40 | });
41 |
42 | describe(UNINITIALIZE, () => {
43 | it('sets initialized to false', () => {
44 | initialState = { initialized: true, fields: ['A'] };
45 | expect(
46 | builderReducer(initialState, {
47 | type: UNINITIALIZE,
48 | })
49 | ).toEqual({
50 | initialized: false,
51 | });
52 | });
53 | });
54 |
55 | describe(SET_COLUMNS, () => {
56 | it('when no destination do nothing', () => {
57 | const payload = {
58 | destination: undefined,
59 | source: {
60 | droppableId: 'dropId',
61 | },
62 | draggableId: 'dragId',
63 | };
64 |
65 | expect(
66 | builderReducer(initialState, {
67 | type: SET_COLUMNS,
68 | payload,
69 | })
70 | ).toEqual(initialState);
71 | });
72 |
73 | it('when moving into itself do nothing', () => {
74 | const payload = {
75 | destination: {
76 | droppableId: 'dropId',
77 | index: 2,
78 | },
79 | source: {
80 | droppableId: 'dropId',
81 | index: 2,
82 | },
83 | draggableId: 'dropId',
84 | };
85 |
86 | expect(
87 | builderReducer(initialState, {
88 | type: SET_COLUMNS,
89 | payload,
90 | })
91 | ).toEqual(initialState);
92 | });
93 |
94 | it('when nesting containers do nothing', () => {
95 | initialState = {
96 | fields: {
97 | 'draggable-field': { isContainer: true },
98 | },
99 | containers: [{ id: containerId, boundaries: [15, 22] }],
100 | dropTargets: {
101 | dropId: [],
102 | },
103 | };
104 |
105 | const payload = {
106 | destination: {
107 | droppableId: 'dropId',
108 | index: 16,
109 | },
110 | source: {
111 | droppableId: 'dropId',
112 | index: 3,
113 | },
114 | draggableId: 'draggable-field',
115 | };
116 |
117 | expect(
118 | builderReducer(initialState, {
119 | type: SET_COLUMNS,
120 | payload,
121 | })
122 | ).toEqual(initialState);
123 | });
124 |
125 | it('From root to root', () => {
126 | initialState = {
127 | fields: {
128 | 'draggable-field': {},
129 | field2: {},
130 | },
131 | containers: [],
132 | dropTargets: {
133 | dropId: { fieldsIds: ['draggable-field', 'field2'] },
134 | },
135 | };
136 |
137 | const payload = {
138 | destination: {
139 | droppableId: 'dropId',
140 | index: 1,
141 | },
142 | source: {
143 | droppableId: 'dropId',
144 | index: 0,
145 | },
146 | draggableId: 'draggable-field',
147 | };
148 |
149 | expect(
150 | builderReducer(initialState, {
151 | type: SET_COLUMNS,
152 | payload,
153 | })
154 | ).toEqual({
155 | ...initialState,
156 | dropTargets: {
157 | dropId: { fieldsIds: ['field2', 'draggable-field'] },
158 | },
159 | });
160 | });
161 |
162 | it('Move inside container', () => {
163 | initialState = {
164 | fields: {
165 | 'draggable-field': { container: 'container' },
166 | field2: { container: 'container' },
167 | },
168 | containers: [{ id: 'container', boundaries: [0, 3] }],
169 | dropTargets: {
170 | dropId: { fieldsIds: ['container', 'draggable-field', 'field2', 'container-end'] },
171 | },
172 | };
173 |
174 | const payload = {
175 | destination: {
176 | droppableId: 'dropId',
177 | index: 2,
178 | },
179 | source: {
180 | droppableId: 'dropId',
181 | index: 1,
182 | },
183 | draggableId: 'draggable-field',
184 | };
185 |
186 | expect(
187 | builderReducer(initialState, {
188 | type: SET_COLUMNS,
189 | payload,
190 | })
191 | ).toEqual({
192 | ...initialState,
193 | dropTargets: {
194 | dropId: { fieldsIds: ['container', 'field2', 'draggable-field', 'container-end'] },
195 | },
196 | });
197 | });
198 |
199 | // Does not update boundaries, fields
200 | it.skip('Move into container from root, field was not in container before', () => {
201 | initialState = {
202 | fields: {
203 | 'draggable-field': { container: undefined },
204 | container: { children: [], isContainer: true },
205 | },
206 | containers: [{ id: 'container', boundaries: [1, 2] }],
207 | dropTargets: {
208 | dropId: { fieldsIds: ['draggable-field', 'container', 'container-end'] },
209 | },
210 | };
211 |
212 | const payload = {
213 | destination: {
214 | droppableId: 'dropId',
215 | index: 1,
216 | },
217 | source: {
218 | droppableId: 'dropId',
219 | index: 0,
220 | },
221 | draggableId: 'draggable-field',
222 | };
223 |
224 | expect(
225 | builderReducer(initialState, {
226 | type: SET_COLUMNS,
227 | payload,
228 | })
229 | ).toEqual({
230 | ...initialState,
231 | dropTargets: {
232 | dropId: { fieldsIds: ['container', 'draggable-field', 'container-end'] },
233 | fields: {
234 | 'draggable-field': { container: 'container' },
235 | container: { children: ['draggable-field'], isContainer: true },
236 | },
237 | containers: [{ id: 'container', boundaries: [0, 2] }],
238 | },
239 | });
240 | });
241 |
242 | // should also move boundaries
243 | it.skip('outside of container to root', () => {
244 | initialState = {
245 | fields: {
246 | 'draggable-field': { container: 'container' },
247 | container: { children: ['draggable-field'], isContainer: true },
248 | },
249 | containers: [{ id: 'container', boundaries: [0, 2] }],
250 | dropTargets: {
251 | dropId: { fieldsIds: ['container', 'draggable-field', 'container-end'] },
252 | },
253 | };
254 |
255 | const payload = {
256 | destination: {
257 | droppableId: 'dropId',
258 | index: 0,
259 | },
260 | source: {
261 | droppableId: 'dropId',
262 | index: 1,
263 | },
264 | draggableId: 'draggable-field',
265 | };
266 |
267 | expect(
268 | builderReducer(initialState, {
269 | type: SET_COLUMNS,
270 | payload,
271 | })
272 | ).toEqual({
273 | ...initialState,
274 | dropTargets: {
275 | dropId: { fieldsIds: ['draggable-field', 'container', 'container-end'] },
276 | },
277 | fields: {
278 | 'draggable-field': {},
279 | container: { children: [], isContainer: true },
280 | },
281 | containers: [{ id: 'container', boundaries: [1, 2] }],
282 | draggingContainer: undefined,
283 | });
284 | });
285 |
286 | // Does not update fields, containers
287 | it.skip('Move field between containers', () => {
288 | initialState = {
289 | fields: {
290 | 'draggable-field': { container: 'container1' },
291 | container1: { children: ['draggable-field'], isContainer: true },
292 | container2: { children: [], isContainer: true },
293 | },
294 | containers: [
295 | { id: 'container1', boundaries: [0, 2] },
296 | { id: 'container2', boundaries: [3, 4] },
297 | ],
298 | dropTargets: {
299 | dropId: { id: 'dropId', fieldsIds: ['container1', 'draggable-field', 'container1-end', 'container2', 'container2-end'] },
300 | },
301 | };
302 |
303 | const payload = {
304 | destination: {
305 | droppableId: 'dropId',
306 | index: 3,
307 | },
308 | source: {
309 | droppableId: 'dropId',
310 | index: 1,
311 | },
312 | draggableId: 'draggable-field',
313 | };
314 |
315 | expect(
316 | builderReducer(initialState, {
317 | type: SET_COLUMNS,
318 | payload,
319 | })
320 | ).toEqual({
321 | ...initialState,
322 | draggingContainer: undefined,
323 | dropTargets: {
324 | dropId: { id: 'dropId', fieldsIds: ['container1', 'container1-end', 'container2', 'draggable-field', 'container2-end'] },
325 | },
326 | fields: {
327 | 'draggable-field': { container: 'container2' },
328 | container1: { children: [], isContainer: true },
329 | container2: { children: ['draggable-field'], isContainer: true },
330 | },
331 | containers: [
332 | { id: 'container1', boundaries: [0, 1] },
333 | { id: 'container2', boundaries: [2, 4] },
334 | ],
335 | });
336 | });
337 |
338 | it('Copy to column', () => {
339 | const _Date = Date.now;
340 |
341 | const newId = '876786646465980968';
342 | Date.now = () => ({
343 | toString: () => newId,
344 | });
345 |
346 | initialState = {
347 | fields: {
348 | 'draggable-field': {},
349 | },
350 | containers: [],
351 | dropTargets: {
352 | dropId: { id: 'dropId', fieldsIds: [] },
353 | templates: { id: 'templates', fieldsIds: [] },
354 | },
355 | };
356 |
357 | const payload = {
358 | destination: {
359 | droppableId: 'dropId',
360 | index: 0,
361 | },
362 | source: {
363 | droppableId: 'templates',
364 | index: 3,
365 | },
366 | draggableId: 'draggable-field',
367 | };
368 |
369 | expect(
370 | builderReducer(initialState, {
371 | type: SET_COLUMNS,
372 | payload,
373 | })
374 | ).toEqual({
375 | ...initialState,
376 | draggingContainer: undefined,
377 | selectedComponent: newId,
378 | fields: {
379 | 'draggable-field': {},
380 | [newId]: {
381 | children: undefined,
382 | container: undefined,
383 | id: newId,
384 | initialized: false,
385 | name: undefined,
386 | preview: false,
387 | },
388 | },
389 | dropTargets: {
390 | dropId: { id: 'dropId', fieldsIds: [newId] },
391 | templates: { id: 'templates', fieldsIds: [] },
392 | },
393 | });
394 |
395 | Date.now = _Date;
396 | });
397 |
398 | it('Copy to column - is container', () => {
399 | const _Date = Date.now;
400 |
401 | const newId = '876786646465980968';
402 | Date.now = () => ({
403 | toString: () => newId,
404 | });
405 |
406 | initialState = {
407 | fields: {
408 | 'draggable-field': { isContainer: true },
409 | },
410 | containers: [],
411 | dropTargets: {
412 | dropId: { id: 'dropId', fieldsIds: [] },
413 | templates: { id: 'templates', fieldsIds: [] },
414 | },
415 | };
416 |
417 | const payload = {
418 | destination: {
419 | droppableId: 'dropId',
420 | index: 0,
421 | },
422 | source: {
423 | droppableId: 'templates',
424 | index: 3,
425 | },
426 | draggableId: 'draggable-field',
427 | };
428 |
429 | expect(
430 | builderReducer(initialState, {
431 | type: SET_COLUMNS,
432 | payload,
433 | })
434 | ).toEqual({
435 | ...initialState,
436 | draggingContainer: undefined,
437 | selectedComponent: newId,
438 | fields: {
439 | 'draggable-field': { isContainer: true },
440 | [newId]: {
441 | children: [],
442 | container: undefined,
443 | id: newId,
444 | initialized: false,
445 | name: undefined,
446 | preview: false,
447 | isContainer: true,
448 | },
449 | [`${newId}-end`]: {
450 | component: 'container-end',
451 | id: `${newId}-end`,
452 | },
453 | },
454 | dropTargets: {
455 | dropId: { id: 'dropId', fieldsIds: [newId, `${newId}-end`] },
456 | templates: { id: 'templates', fieldsIds: [] },
457 | },
458 | containers: [{ id: newId, boundaries: [payload.destination.index, payload.destination.index + 1] }],
459 | });
460 |
461 | Date.now = _Date;
462 | });
463 |
464 | it('Copy to column - to container', () => {
465 | const _Date = Date.now;
466 |
467 | const newId = '876786646465980968';
468 | Date.now = () => ({
469 | toString: () => newId,
470 | });
471 |
472 | initialState = {
473 | fields: {
474 | 'draggable-field': {},
475 | container1: { children: [] },
476 | },
477 | containers: [{ id: 'container1', boundaries: [0, 1] }],
478 | dropTargets: {
479 | dropId: { id: 'dropId', fieldsIds: ['container1', 'container1-end'] },
480 | templates: { id: 'templates', fieldsIds: [] },
481 | },
482 | };
483 |
484 | const payload = {
485 | destination: {
486 | droppableId: 'dropId',
487 | index: 1,
488 | },
489 | source: {
490 | droppableId: 'templates',
491 | index: 3,
492 | },
493 | draggableId: 'draggable-field',
494 | };
495 |
496 | expect(
497 | builderReducer(initialState, {
498 | type: SET_COLUMNS,
499 | payload,
500 | })
501 | ).toEqual({
502 | ...initialState,
503 | draggingContainer: undefined,
504 | selectedComponent: newId,
505 | fields: {
506 | 'draggable-field': {},
507 | [newId]: {
508 | children: undefined,
509 | container: 'container1',
510 | id: newId,
511 | initialized: false,
512 | name: undefined,
513 | preview: false,
514 | },
515 | container1: { children: [newId] },
516 | },
517 | dropTargets: {
518 | dropId: { id: 'dropId', fieldsIds: ['container1', newId, 'container1-end'] },
519 | templates: { id: 'templates', fieldsIds: [] },
520 | },
521 | containers: [{ id: 'container1', boundaries: [0, 2] }],
522 | });
523 |
524 | Date.now = _Date;
525 | });
526 | });
527 |
528 | describe(SET_SELECTED_COMPONENT, () => {
529 | it('sets selected component', () => {
530 | const selectedComponent = { id: '126536' };
531 |
532 | expect(
533 | builderReducer(initialState, {
534 | type: SET_SELECTED_COMPONENT,
535 | payload: selectedComponent,
536 | })
537 | ).toEqual({
538 | ...initialState,
539 | selectedComponent,
540 | });
541 | });
542 | });
543 |
544 | describe(REMOVE_COMPONENT, () => {
545 | it('removes selected component', () => {
546 | const selectedComponent = '126536';
547 |
548 | initialState = {
549 | ...initialState,
550 | fields: {
551 | [selectedComponent]: { name: 'delete me' },
552 | 7989854: { name: 'do not remove me' },
553 | },
554 | containers: [],
555 | dropTargets: {
556 | [FORM_LAYOUT]: { fieldsIds: [] },
557 | },
558 | };
559 |
560 | expect(
561 | builderReducer(initialState, {
562 | type: REMOVE_COMPONENT,
563 | payload: selectedComponent,
564 | })
565 | ).toEqual({
566 | ...initialState,
567 | fields: {
568 | 7989854: { name: 'do not remove me' },
569 | },
570 | });
571 | });
572 |
573 | it('removes selected component from container', () => {
574 | const selectedComponent = '126536';
575 |
576 | initialState = {
577 | ...initialState,
578 | fields: {
579 | [selectedComponent]: { name: 'delete me', container: containerId },
580 | },
581 | containers: [{ id: containerId, boundaries: [15, 22] }],
582 | dropTargets: {
583 | [FORM_LAYOUT]: { fieldsIds: [selectedComponent, '234'] },
584 | },
585 | };
586 |
587 | expect(
588 | builderReducer(initialState, {
589 | type: REMOVE_COMPONENT,
590 | payload: selectedComponent,
591 | })
592 | ).toEqual({
593 | ...initialState,
594 | selectedComponent: undefined,
595 | fields: {},
596 | containers: [{ id: containerId, boundaries: [15, 21] }],
597 | dropTargets: {
598 | [FORM_LAYOUT]: { fieldsIds: ['234'] },
599 | },
600 | });
601 | });
602 | });
603 |
604 | describe(DRAG_START, () => {
605 | it('sets dragging container', () => {
606 | initialState = {
607 | initialized: true,
608 | fields: {
609 | 125: { name: 'cosi', isContainer: true },
610 | },
611 | };
612 |
613 | expect(
614 | builderReducer(initialState, {
615 | type: DRAG_START,
616 | payload: { draggableId: '125' },
617 | })
618 | ).toEqual({
619 | ...initialState,
620 | draggingContainer: '125',
621 | });
622 | });
623 |
624 | it("does not set dragging container when it's not container", () => {
625 | initialState = {
626 | initialized: true,
627 | fields: {
628 | 125: { name: 'cosi', isContainer: false },
629 | },
630 | };
631 |
632 | expect(
633 | builderReducer(initialState, {
634 | type: DRAG_START,
635 | payload: { draggableId: '125' },
636 | })
637 | ).toEqual(initialState);
638 | });
639 |
640 | it('does not set dragging container when id is initial', () => {
641 | initialState = {
642 | initialized: true,
643 | fields: {
644 | 'initial-151': { name: 'cosi', isContainer: true },
645 | },
646 | };
647 |
648 | expect(
649 | builderReducer(initialState, {
650 | type: DRAG_START,
651 | payload: { draggableId: 'initial-151' },
652 | })
653 | ).toEqual(initialState);
654 | });
655 | });
656 |
657 | describe(SET_FIELD_PROPERTY, () => {
658 | it('sets field property according to fieldId', () => {
659 | initialState = {
660 | initialized: true,
661 | fields: {
662 | 125: { name: 'do not change me 1' },
663 | 1515: { name: 'change me' },
664 | },
665 | };
666 |
667 | expect(
668 | builderReducer(initialState, {
669 | type: SET_FIELD_PROPERTY,
670 | payload: { fieldId: 1515, propertyName: 'custom', value: 'customValue' },
671 | })
672 | ).toEqual({
673 | ...initialState,
674 | fields: {
675 | 125: { name: 'do not change me 1' },
676 | 1515: {
677 | name: 'change me',
678 | initialized: true,
679 | custom: 'customValue',
680 | propertyValidation: {},
681 | },
682 | },
683 | });
684 | });
685 |
686 | it('sets field property according to fieldId and validate property', () => {
687 | initialState = {
688 | initialized: true,
689 | fields: {
690 | 125: { name: 'do not change me 1' },
691 | 1515: { name: 'change me' },
692 | },
693 | };
694 |
695 | expect(
696 | builderReducer(initialState, {
697 | type: SET_FIELD_PROPERTY,
698 | payload: { fieldId: 1515, propertyName: 'isDisabled', value: true },
699 | })
700 | ).toEqual({
701 | ...initialState,
702 | fields: {
703 | 125: { name: 'do not change me 1' },
704 | 1515: {
705 | name: 'change me',
706 | initialized: true,
707 | isDisabled: true,
708 | propertyValidation: propertiesValidation('isDisabled')({
709 | name: 'change me',
710 | }),
711 | },
712 | },
713 | });
714 | });
715 | });
716 |
717 | describe(SET_FIELD_VALIDATOR, () => {
718 | it('adds field validator according to fieldId', () => {
719 | initialState = {
720 | initialized: true,
721 | fields: {
722 | 125: { name: 'do not change me 1' },
723 | 1515: { name: 'change me' },
724 | 1: { name: 'do not change me 2' },
725 | },
726 | };
727 |
728 | expect(
729 | builderReducer(initialState, {
730 | type: SET_FIELD_VALIDATOR,
731 | payload: { fieldId: 1515, action: 'add', type: 'length' },
732 | })
733 | ).toEqual({
734 | ...initialState,
735 | fields: {
736 | 125: { name: 'do not change me 1' },
737 | 1515: { name: 'change me', validate: [{ type: 'length' }] },
738 | 1: { name: 'do not change me 2' },
739 | },
740 | });
741 | });
742 |
743 | it('adds required field validator - appends isRequired', () => {
744 | initialState = {
745 | initialized: true,
746 | fields: {
747 | 125: { name: 'do not change me 1' },
748 | 1515: { name: 'change me' },
749 | 1: { name: 'do not change me 2' },
750 | },
751 | };
752 |
753 | expect(
754 | builderReducer(initialState, {
755 | type: SET_FIELD_VALIDATOR,
756 | payload: { fieldId: 1515, action: 'add', type: validatorTypes.REQUIRED },
757 | })
758 | ).toEqual({
759 | ...initialState,
760 | fields: {
761 | 125: { name: 'do not change me 1' },
762 | 1515: { name: 'change me', validate: [{ type: validatorTypes.REQUIRED }], isRequired: true },
763 | 1: { name: 'do not change me 2' },
764 | },
765 | });
766 | });
767 |
768 | it('removes field validator according to fieldId', () => {
769 | initialState = {
770 | initialized: true,
771 | fields: {
772 | 125: { name: 'do not change me 1' },
773 | 1: { name: 'do not change me 2' },
774 | 1515: {
775 | name: 'change me',
776 | validate: [{ type: 'required' }, { type: 'remove me' }, { type: 'leave me alone' }],
777 | },
778 | },
779 | };
780 |
781 | expect(
782 | builderReducer(initialState, {
783 | type: SET_FIELD_VALIDATOR,
784 | payload: { fieldId: 1515, action: 'remove', index: 1 },
785 | })
786 | ).toEqual({
787 | ...initialState,
788 | fields: {
789 | 125: { name: 'do not change me 1' },
790 | 1: { name: 'do not change me 2' },
791 | 1515: {
792 | name: 'change me',
793 | validate: [{ type: 'required' }, { type: 'leave me alone' }],
794 | },
795 | },
796 | });
797 | });
798 |
799 | it('modifes field validator according to fieldId', () => {
800 | initialState = {
801 | initialized: true,
802 | fields: {
803 | 125: { name: 'do not change me 1' },
804 | 1: { name: 'do not change me 2' },
805 | 1515: {
806 | name: 'change me',
807 | validate: [{ type: 'please i want to be changed' }, { type: 'i am not here' }, { type: 'leave me alone' }],
808 | },
809 | },
810 | };
811 |
812 | expect(
813 | builderReducer(initialState, {
814 | type: SET_FIELD_VALIDATOR,
815 | payload: { fieldId: 1515, action: 'modify', index: 0, asYouWish: true },
816 | })
817 | ).toEqual({
818 | ...initialState,
819 | fields: {
820 | 125: { name: 'do not change me 1' },
821 | 1: { name: 'do not change me 2' },
822 | 1515: {
823 | name: 'change me',
824 | validate: [{ type: 'please i want to be changed', asYouWish: true }, { type: 'i am not here' }, { type: 'leave me alone' }],
825 | },
826 | },
827 | });
828 | });
829 | });
830 | });
831 |
--------------------------------------------------------------------------------
/src/tests/builder-store/builder-store.test.js:
--------------------------------------------------------------------------------
1 | import builderStore from '../../builder-store/builder-store';
2 |
3 | describe('builderStore', () => {
4 | it('creates store', () => {
5 | expect(builderStore).toEqual(
6 | expect.objectContaining({
7 | dispatch: expect.any(Function),
8 | getState: expect.any(Function),
9 | replaceReducer: expect.any(Function),
10 | subscribe: expect.any(Function),
11 | })
12 | );
13 | expect(builderStore.getState()).toEqual({ initialized: false });
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/tests/helpers/create-export-schema.test.js:
--------------------------------------------------------------------------------
1 | import createSchema, { validateOutput } from '../../helpers/create-export-schema';
2 | import { fields, invalidSchema, schemaWithDeletedOption } from '../__mocks__/builder-fields';
3 |
4 | describe('create export schema', () => {
5 | it('should create data driven from schema from flat object', () => {
6 | const expectedSchema = {
7 | fields: [
8 | {
9 | name: 'only-name',
10 | },
11 | {
12 | name: 'foo',
13 | label: 'Text field',
14 | isRequired: true,
15 | validate: [{ type: 'required-validator' }],
16 | },
17 | ],
18 | };
19 | expect(createSchema(['should-remove-all-attributes', '123-text-field'], fields)).toEqual(expectedSchema);
20 | });
21 |
22 | it('should check for builder validation errors', () => {
23 | expect(validateOutput(fields)).toEqual(true);
24 | expect(validateOutput(invalidSchema)).toEqual(false);
25 | });
26 |
27 | it('should sanitize deleted options', () => {
28 | const expectedSchema = {
29 | fields: [
30 | {
31 | name: 'deleted-option-field',
32 | options: [
33 | {
34 | value: 'bar',
35 | label: 'bar',
36 | },
37 | ],
38 | },
39 | ],
40 | };
41 | expect(createSchema(['deleted-option-field'], schemaWithDeletedOption)).toEqual(expectedSchema);
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/src/tests/helpers/create-initial-data.test.js:
--------------------------------------------------------------------------------
1 | import createInitialData from '../../helpers/create-initial-data';
2 | import { initialBuilderFields, initialDDFSchema, initialDDFSchemaTemplate } from '../__mocks__/builder-fields';
3 |
4 | describe('create unitial data', () => {
5 | jest.spyOn(Date, 'now').mockImplementation(() => 123);
6 | it('should create initial builder state', () => {
7 | const expectedState = {
8 | containers: [],
9 | dropTargets: {
10 | 'components-list': {
11 | fieldsIds: ['text-field', 'options-component'],
12 | id: 'components-list',
13 | title: 'Component picker',
14 | },
15 | 'form-layout': {
16 | fieldsIds: ['first-name-123', 'options-select-123'],
17 | id: 'form-layout',
18 | title: 'Form',
19 | },
20 | },
21 | fields: {
22 | 'first-name-123': {
23 | clone: true,
24 | component: 'text-field',
25 | id: 'first-name-123',
26 | initialized: true,
27 | name: 'first-name',
28 | preview: false,
29 | validate: [
30 | {
31 | type: 'required-validator',
32 | message: 'I am required',
33 | },
34 | ],
35 | },
36 | 'text-field': {
37 | attributes: [
38 | {
39 | component: 'input',
40 | label: 'Label',
41 | propertyName: 'label',
42 | },
43 | ],
44 | },
45 | 'options-component': {
46 | attributes: [
47 | {
48 | component: 'options',
49 | label: 'Options',
50 | propertyName: 'options',
51 | },
52 | ],
53 | },
54 | 'options-select-123': {
55 | clone: true,
56 | component: 'options-component',
57 | id: 'options-select-123',
58 | initialized: true,
59 | name: 'options-select',
60 | validate: [
61 | {
62 | message: 'I am required',
63 | type: 'required-validator',
64 | },
65 | ],
66 | options: [
67 | {
68 | label: 'First option',
69 | value: 1,
70 | },
71 | ],
72 | preview: false,
73 | },
74 | },
75 | selectedComponent: undefined,
76 | };
77 |
78 | expect(createInitialData(initialBuilderFields, initialDDFSchema)).toEqual(expectedState);
79 | });
80 |
81 | it('should create initial schema in subset mode without template', () => {
82 | const expectedState = expect.objectContaining({
83 | fields: expect.objectContaining({
84 | 'first-name-123': {
85 | clone: true,
86 | component: 'text-field',
87 | id: 'first-name-123',
88 | initialized: true,
89 | name: 'first-name',
90 | options: undefined,
91 | preview: false,
92 | restricted: true,
93 | validate: [
94 | {
95 | type: 'required-validator',
96 | message: 'I am required',
97 | original: {
98 | type: 'required-validator',
99 | message: 'I am required',
100 | },
101 | },
102 | ],
103 | },
104 | }),
105 | });
106 | expect(createInitialData(initialBuilderFields, initialDDFSchema, true)).toEqual(expectedState);
107 | });
108 | it('should create initial schema in subset mode with template', () => {
109 | const expectedState = expect.objectContaining({
110 | fields: {
111 | 'first-name-123': {
112 | clone: true,
113 | component: 'text-field',
114 | id: 'first-name-123',
115 | initialized: true,
116 | name: 'first-name',
117 | options: undefined,
118 | preview: false,
119 | restricted: true,
120 | validate: [
121 | {
122 | message: 'I am required',
123 | original: {
124 | message: 'I am required',
125 | type: 'required-validator',
126 | },
127 | type: 'required-validator',
128 | },
129 | ],
130 | },
131 | 'options-component': {
132 | attributes: [
133 | {
134 | component: 'options',
135 | label: 'Options',
136 | propertyName: 'options',
137 | },
138 | ],
139 | },
140 | 'options-select-123': {
141 | clone: true,
142 | component: 'options-component',
143 | id: 'options-select-123',
144 | initialized: true,
145 | name: 'options-select',
146 | validate: [
147 | {
148 | type: 'required-validator',
149 | message: 'I am required',
150 | original: {
151 | type: 'required-validator',
152 | message: 'I am required',
153 | },
154 | },
155 | ],
156 | options: [
157 | {
158 | label: 'First option',
159 | restoreable: true,
160 | value: 1,
161 | },
162 | {
163 | deleted: true,
164 | label: 'Second deleted option',
165 | restoreable: true,
166 | value: 2,
167 | },
168 | ],
169 | preview: false,
170 | restricted: true,
171 | },
172 | 'text-field': {
173 | attributes: [
174 | {
175 | component: 'input',
176 | label: 'Label',
177 | propertyName: 'label',
178 | },
179 | ],
180 | },
181 | },
182 | });
183 | expect(createInitialData(initialBuilderFields, initialDDFSchema, true, initialDDFSchemaTemplate)).toEqual(expectedState);
184 | });
185 | });
186 |
--------------------------------------------------------------------------------
/src/tests/picker-field.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 | import { DragDropContext, Droppable } from 'react-beautiful-dnd';
4 | import ComponentsContext from '../components-context';
5 | import PickerField from '../picker-field';
6 |
7 | describe('PickerField', () => {
8 | let initialProps;
9 | let pokusPicker;
10 | let pokusComponent;
11 |
12 | beforeEach(() => {
13 | initialProps = {
14 | field: {
15 | id: 'draggable-id',
16 | component: 'pokus',
17 | },
18 | index: 0,
19 | };
20 |
21 | pokusPicker = ({ innerProps, ...props }) => ;
22 | pokusComponent = (props) => This is pokus
;
23 | });
24 |
25 | it('renders picker component', () => {
26 | const wrapper = mount(
27 |
35 | console.log('draguji')} onDragEnd={jest.fn()}>
36 |
37 | {(provided) => (
38 |
41 | )}
42 |
43 |
44 |
45 | );
46 |
47 | expect(wrapper.find(pokusPicker)).toHaveLength(1);
48 | expect(wrapper.find(pokusComponent)).toHaveLength(0);
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/src/tests/properties-editor/initial-value-checker.test.js:
--------------------------------------------------------------------------------
1 | import propertiesValidation from '../../properties-editor/initial-value-checker';
2 |
3 | describe('initial value checker', () => {
4 | it('should return an empty object for wrong validation type', () => {
5 | const field = {};
6 | const validator = propertiesValidation('non-sense');
7 | expect(validator(field)).toEqual({});
8 | });
9 | it('should return empty initial value key', () => {
10 | const field = {};
11 | const validator = propertiesValidation('isDisabled');
12 | expect(validator(field)).toEqual({ initialValue: undefined });
13 | });
14 |
15 | it('should return an error when field is required, disabled, read-only and hidden and has no initial value', () => {
16 | const conflictingAttributes = ['isDisabled', 'isReadOnly', 'hideField'];
17 | const propertyStrings = {
18 | isRequired: 'required',
19 | isDisabled: 'disabled',
20 | isReadOnly: 'read only',
21 | hideField: 'hidden',
22 | };
23 | conflictingAttributes.forEach((attribute) => {
24 | const field = {
25 | isRequired: true,
26 | [attribute]: true,
27 | };
28 | const expectedError = {
29 | initialValue: {
30 | code: 'errors.initialValue',
31 | codeDependencies: {
32 | [attribute]: true,
33 | isRequired: true,
34 | },
35 | message: `Initial value must be set if field is required and at the same time ${propertyStrings[attribute]}.`,
36 | },
37 | };
38 | const validator = propertiesValidation(attribute);
39 | expect(validator(field)).toEqual(expectedError);
40 | });
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/src/tests/properties-editor/memoized-property.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import configureStore from 'redux-mock-store';
3 | import { Provider } from 'react-redux';
4 | import { mount } from 'enzyme';
5 |
6 | import MemoizedProperty from '../../properties-editor/memoized-property';
7 |
8 | const Component = ({ onChange, value }) => ;
9 |
10 | const ComponentWrapper = ({ store, ...props }) => (
11 |
12 |
13 |
14 | );
15 |
16 | describe('', () => {
17 | const mockStore = configureStore();
18 | const initialState = {
19 | fields: {
20 | 'selected-field': {
21 | label: 'Label',
22 | restricted: false,
23 | },
24 | },
25 | selectedComponent: 'selected-field',
26 | };
27 | it('should mount and render correct component', () => {
28 | const store = mockStore(initialState);
29 | const wrapper = mount(
30 |
31 | );
32 | expect(wrapper.find(Component)).toHaveLength(1);
33 | });
34 |
35 | it('should call handlePropertyChange', () => {
36 | const store = mockStore(initialState);
37 | const handlePropertyChange = jest.fn();
38 | const wrapper = mount(
39 |
40 | );
41 | expect(wrapper.find(Component)).toHaveLength(1);
42 | wrapper.find('input').simulate('change', { target: { value: 'New label' } });
43 | expect(handlePropertyChange).toHaveBeenCalledWith(
44 | expect.objectContaining({
45 | target: expect.objectContaining({ value: 'New label' }),
46 | }),
47 | 'label'
48 | );
49 | });
50 |
51 | it('should update if propertyValidation prop was changed', () => {
52 | let store = mockStore(initialState);
53 | const newState = {
54 | ...initialState,
55 | fields: {
56 | 'selected-field': {
57 | label: 'Label',
58 | restricted: false,
59 | propertyValidation: { label: { message: 'Foo' } },
60 | },
61 | },
62 | };
63 | const wrapper = mount(
64 |
65 | );
66 | expect(wrapper.find(Component).props().innerProps.propertyValidation).toEqual();
67 | wrapper.setProps({ store: mockStore(newState) });
68 | wrapper.update();
69 | expect(wrapper.find(Component).props().innerProps.propertyValidation).toEqual({
70 | message: 'Foo',
71 | });
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/src/tests/properties-editor/memoized-validator.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import configureStore from 'redux-mock-store';
3 | import { Provider } from 'react-redux';
4 | import { mount } from 'enzyme';
5 |
6 | import ComponentsContext from '../../components-context';
7 | import MemoizedValidator from '../../properties-editor/memozied-validator';
8 |
9 | const ContextComponent = ({ onChange, value }) => ;
10 |
11 | const ComponentWrapper = ({ store, ...props }) => (
12 |
13 |
20 |
21 |
22 |
23 | );
24 |
25 | describe('', () => {
26 | const mockStore = configureStore();
27 | const initialState = {
28 | fields: {
29 | 'selected-field': {},
30 | },
31 | selectedComponent: 'selected-field',
32 | };
33 |
34 | it('should mount MemoizedValidator and pick component from context', () => {
35 | const store = mockStore(initialState);
36 | const wrapper = mount(
37 |
50 | );
51 | expect(wrapper.find(ContextComponent)).toHaveLength(1);
52 | });
53 |
54 | it('should trigger render only if validator had changed', () => {
55 | const store = mockStore(initialState);
56 | const wrapper = mount(
57 |
70 | );
71 | expect(wrapper.find('input').props().value).toEqual('Yay');
72 | wrapper.setProps({ validator: { 'validate-me': 'Nay' } });
73 | expect(wrapper.find('input').props().value).toEqual('Nay');
74 | });
75 | });
76 |
--------------------------------------------------------------------------------
/src/tests/properties-editor/properties-editor.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import configureStore from 'redux-mock-store';
3 | import { Provider } from 'react-redux';
4 | import { mount } from 'enzyme';
5 | import { validatorTypes, Form, useFieldApi, RendererContext } from '@data-driven-forms/react-form-renderer';
6 |
7 | import PropertiesEditor from '../../properties-editor';
8 | import ComponentsContext from '../../components-context';
9 | import { SET_SELECTED_COMPONENT, REMOVE_COMPONENT } from '../../builder-store/builder-reducer';
10 |
11 | const Field = (props) => {
12 | useFieldApi(props);
13 | return null;
14 | };
15 |
16 | const AddValidatorComponent = ({ addValidator }) => (
17 |
20 | );
21 |
22 | const PropertiesEditorWrapper = ({ propertiesChildren, validationChildren, addValidator, handleClose, handleDelete }) => (
23 |
24 |
Properties editor
25 |
28 |
31 |
{propertiesChildren}
32 |
33 |
{validationChildren}
34 |
35 | );
36 |
37 | const PropertyGroup = ({ children, handleDelete }) => (
38 |
39 |
Property group
40 |
43 |
{children}
44 |
45 | );
46 |
47 | const TextField = ({ value, onChange, fieldId, type }) => (
48 | onChange(value)} />
49 | );
50 | const Switch = ({ value, onChange, fieldId, isDisabled }) => (
51 | onChange(checked)} checked={value} />
52 | );
53 |
54 | const ComponentWrapper = ({ store, ...props }) => (
55 |
92 | );
93 |
94 | describe('', () => {
95 | const mockStore = configureStore();
96 | const initialState = {
97 | fields: {
98 | 'selected-component': {
99 | id: 'selected-component-id',
100 | name: 'selected-component',
101 | component: 'text-field',
102 | isRequired: true,
103 | validate: [{ type: 'required' }],
104 | },
105 | },
106 | selectedComponent: 'selected-component',
107 | };
108 | it('should mount and render PropertiesEditor and PropertyGroup', () => {
109 | const store = mockStore(initialState);
110 | const wrapper = mount();
111 | /**
112 | * reder whole wrapper
113 | */
114 | expect(wrapper.find(PropertiesEditor)).toHaveLength(1);
115 | /**
116 | * one required switch
117 | */
118 | expect(wrapper.find(Switch)).toHaveLength(1);
119 | expect(wrapper.find(Switch).props().value).toEqual(true);
120 | /**
121 | * render 3 fields, name, initialValue are mandatory and label and required-message are from attributes
122 | */
123 | expect(wrapper.find(TextField)).toHaveLength(4);
124 | expect(wrapper.find(TextField).at(0).props().fieldId).toEqual('name');
125 | expect(wrapper.find(TextField).at(1).props().fieldId).toEqual('initialValue');
126 | expect(wrapper.find(TextField).at(2).props().fieldId).toEqual('label');
127 | expect(wrapper.find(TextField).at(3).props().fieldId).toEqual('required-message');
128 | });
129 |
130 | it('should set disableValidators and disableInitialValue when not field component', () => {
131 | const initialStateNoFieldComponent = {
132 | fields: {
133 | 'selected-component-second': {
134 | id: 'selected-component-id',
135 | name: 'selected-component-second',
136 | component: 'text-field',
137 | },
138 | },
139 | selectedComponent: 'selected-component-second',
140 | };
141 |
142 | const store = mockStore(initialStateNoFieldComponent);
143 | const wrapper = mount();
144 |
145 | expect(wrapper.find(PropertiesEditorWrapper).prop('disableInitialValue')).toEqual(true);
146 | expect(wrapper.find(PropertiesEditorWrapper).prop('disableValidators')).toEqual(true);
147 | });
148 |
149 | it('should add new validator to a list', () => {
150 | const expectedActions = [
151 | {
152 | type: 'setFieldValidator',
153 | payload: {
154 | action: 'add',
155 | fieldId: 'selected-component-id',
156 | type: 'min-length',
157 | },
158 | },
159 | ];
160 | const store = mockStore(initialState);
161 | const wrapper = mount();
162 | wrapper.find('#add').simulate('click');
163 | expect(store.getActions()).toEqual(expectedActions);
164 | });
165 |
166 | it('should turn off required validator and turn on', () => {
167 | const store = mockStore(initialState);
168 | const expectedActions = [
169 | {
170 | type: 'setFieldValidator',
171 | payload: {
172 | action: 'remove',
173 | fieldId: 'selected-component-id',
174 | index: 0,
175 | type: 'required',
176 | },
177 | },
178 | {
179 | type: 'setFieldValidator',
180 | payload: {
181 | action: 'add',
182 | fieldId: 'selected-component-id',
183 | index: 0,
184 | type: 'required',
185 | },
186 | },
187 | ];
188 | const wrapper = mount();
189 | wrapper.find('#required-validator').simulate('change', { target: { checked: false } });
190 | wrapper.find('#required-validator').simulate('change', { target: { checked: true } });
191 | expect(store.getActions()).toEqual(expectedActions);
192 | });
193 |
194 | it('should prevent turning off required validator on restricted field', () => {
195 | const store = mockStore({
196 | ...initialState,
197 | fields: {
198 | 'selected-component': {
199 | ...initialState.fields['selected-component'],
200 | restricted: true,
201 | validate: [{ type: 'required', original: {} }],
202 | },
203 | },
204 | });
205 | const wrapper = mount();
206 | expect(wrapper.find('input#required-validator').props().disabled).toEqual(true);
207 | });
208 |
209 | it('should modify required validator message', () => {
210 | const store = mockStore(initialState);
211 | const expectedActions = [
212 | {
213 | type: 'setFieldValidator',
214 | payload: {
215 | action: 'modify',
216 | fieldId: 'selected-component-id',
217 | index: 0,
218 | message: 'New required message',
219 | },
220 | },
221 | ];
222 | const wrapper = mount();
223 | wrapper.find('#required-message').simulate('change', { target: { value: 'New required message' } });
224 | expect(store.getActions()).toEqual(expectedActions);
225 | });
226 |
227 | it('should change the label property', () => {
228 | const store = mockStore(initialState);
229 | const expectedActions = [
230 | {
231 | type: 'setFieldProperty',
232 | payload: {
233 | fieldId: 'selected-component-id',
234 | propertyName: 'label',
235 | value: 'New label',
236 | },
237 | },
238 | ];
239 | const wrapper = mount();
240 | wrapper.find('#label').simulate('change', { target: { value: 'New label' } });
241 | expect(store.getActions()).toEqual(expectedActions);
242 | });
243 |
244 | it('should modify non required validator', () => {
245 | const store = mockStore({
246 | ...initialState,
247 | fields: {
248 | 'selected-component': {
249 | ...initialState.fields['selected-component'],
250 | validate: [...initialState.fields['selected-component'].validate, { type: validatorTypes.MIN_LENGTH, threshold: 5 }],
251 | },
252 | },
253 | });
254 | const expectedActions = [
255 | {
256 | payload: {
257 | action: 'modify',
258 | fieldId: 'selected-component-id',
259 | index: 1,
260 | threshold: 10,
261 | },
262 | type: 'setFieldValidator',
263 | },
264 | {
265 | payload: {
266 | action: 'modify',
267 | fieldId: 'selected-component-id',
268 | index: 1,
269 | message: 'Field must have atleast 10 characters',
270 | },
271 | type: 'setFieldValidator',
272 | },
273 | ];
274 | const wrapper = mount();
275 | expect(wrapper.find('input[type="number"]#threshold-1')).toHaveLength(1);
276 | expect(wrapper.find('input#message-1')).toHaveLength(1);
277 | wrapper.find('input[type="number"]#threshold-1').simulate('change', { target: { value: '10' } });
278 | wrapper.find('input#message-1').simulate('change', {
279 | target: { value: 'Field must have atleast 10 characters' },
280 | });
281 | expect(store.getActions()).toEqual(expectedActions);
282 | });
283 |
284 | it('should delete non required validator', () => {
285 | const store = mockStore({
286 | ...initialState,
287 | fields: {
288 | 'selected-component': {
289 | ...initialState.fields['selected-component'],
290 | validate: [...initialState.fields['selected-component'].validate, { type: validatorTypes.MIN_LENGTH, threshold: 5 }],
291 | },
292 | },
293 | });
294 | const expectedActions = [
295 | {
296 | payload: {
297 | action: 'remove',
298 | fieldId: 'selected-component-id',
299 | index: 1,
300 | },
301 | type: 'setFieldValidator',
302 | },
303 | ];
304 | const wrapper = mount();
305 | wrapper.find('button.delete').last().simulate('click');
306 | expect(store.getActions()).toEqual(expectedActions);
307 | });
308 |
309 | it('should prevent deletion of restricted non required validator', () => {
310 | const store = mockStore({
311 | ...initialState,
312 | fields: {
313 | 'selected-component': {
314 | ...initialState.fields['selected-component'],
315 | validate: [...initialState.fields['selected-component'].validate, { type: validatorTypes.MIN_LENGTH, threshold: 5, original: {} }],
316 | },
317 | },
318 | });
319 | const expectedActions = [];
320 | const wrapper = mount();
321 | wrapper.find('button.delete').last().simulate('click');
322 | expect(store.getActions()).toEqual(expectedActions);
323 | });
324 |
325 | it('should call the handle close action on properties editor', () => {
326 | const store = mockStore(initialState);
327 | const expectedActions = [
328 | {
329 | type: SET_SELECTED_COMPONENT,
330 | },
331 | ];
332 | const wrapper = mount();
333 | wrapper.find('button#close-properties-editor').simulate('click');
334 | expect(store.getActions()).toEqual(expectedActions);
335 | });
336 |
337 | it('should call the handle delete field action', () => {
338 | const store = mockStore(initialState);
339 | const expectedActions = [
340 | {
341 | type: REMOVE_COMPONENT,
342 | payload: 'selected-component-id',
343 | },
344 | ];
345 | const wrapper = mount();
346 | wrapper.find('button#delete-field').simulate('click');
347 | expect(store.getActions()).toEqual(expectedActions);
348 | });
349 |
350 | it('should prevent call handle delete field action on restricted field', () => {
351 | const store = mockStore({
352 | ...initialState,
353 | fields: {
354 | 'selected-component': {
355 | ...initialState.fields['selected-component'],
356 | restricted: true,
357 | },
358 | },
359 | });
360 | const expectedActions = [];
361 | const wrapper = mount();
362 | wrapper.find('button#delete-field').simulate('click');
363 | expect(store.getActions()).toEqual(expectedActions);
364 | });
365 |
366 | it('should set hasPropertyError if field has error', () => {
367 | const store = mockStore({
368 | ...initialState,
369 | fields: {
370 | 'selected-component': {
371 | ...initialState.fields['selected-component'],
372 | propertyValidation: { prop: 'Error on prop' },
373 | },
374 | },
375 | });
376 | const wrapper = mount();
377 | expect(wrapper.find(PropertiesEditorWrapper).props().hasPropertyError).toBeTruthy();
378 | });
379 | });
380 |
--------------------------------------------------------------------------------
/src/tests/properties-editor/validator-property.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { mount } from 'enzyme';
3 |
4 | import ComponentsContext from '../../components-context';
5 | import ValidatorProperty from '../../properties-editor/validator-property';
6 |
7 | describe('', () => {
8 | const PropertyComponent = ({ onChange, onBlur, value, type }) => (
9 |
10 |
11 |
12 | );
13 | const property = {
14 | component: 'some-property-component',
15 | propertyName: 'some-property-name',
16 | label: 'Some property label',
17 | };
18 |
19 | afterEach(() => {});
20 |
21 | it('should mount and render corrrect component from ComponentsContext', () => {
22 | const wrapper = mount(
23 |
30 |
31 |
32 | );
33 | expect(wrapper.find(PropertyComponent)).toHaveLength(1);
34 | });
35 |
36 | it('should generate isDisabled prop and pass it to mapped component', () => {
37 | const wrapper = mount(
38 |
45 |
60 |
61 | );
62 | expect(wrapper.find(PropertyComponent).props().isDisabled).toEqual(true);
63 | });
64 |
65 | it('should reset property on blur if max value is out of bounds', () => {
66 | const onChange = jest.fn();
67 | const wrapper = mount(
68 |
75 |
94 |
95 | );
96 | const input = wrapper.find('input');
97 | expect(input.props().value).toEqual(55);
98 | input.simulate('blur');
99 | wrapper.update();
100 | expect(onChange).toHaveBeenCalledWith({ 'some-property-name': 10 }, 'modify', 0);
101 | });
102 |
103 | it('should reset property on blur if min value is out of bounds', () => {
104 | const onChange = jest.fn();
105 | const wrapper = mount(
106 |
113 |
132 |
133 | );
134 | const input = wrapper.find('input');
135 | expect(input.props().value).toEqual(10);
136 | input.simulate('blur');
137 | wrapper.update();
138 | expect(onChange).toHaveBeenCalledWith({ 'some-property-name': 55 }, 'modify', 0);
139 | });
140 |
141 | it('should not reset value on blur if there is no reset value', () => {
142 | const onChange = jest.fn();
143 | const wrapper = mount(
144 |
151 |
169 |
170 | );
171 | const input = wrapper.find('input');
172 | expect(input.props().value).toEqual(13);
173 | input.simulate('blur');
174 | wrapper.update();
175 | expect(onChange).toHaveBeenCalledWith({ 'some-property-name': 13 }, 'modify', 0);
176 | });
177 |
178 | it('should not use default value on blur if value is 0', () => {
179 | const onChange = jest.fn();
180 | const wrapper = mount(
181 |
188 |
207 |
208 | );
209 | const input = wrapper.find('input');
210 | expect(input.props().value).toEqual(0);
211 | input.simulate('blur');
212 | wrapper.update();
213 | expect(onChange).toHaveBeenCalledWith({ 'some-property-name': 0 }, 'modify', 0);
214 | });
215 |
216 | it('should call onChange callback', () => {
217 | const onChange = jest.fn();
218 | let wrapper = mount(
219 |
226 |
227 |
228 | );
229 | wrapper.find('input').simulate('change', { target: { value: 'Yay' } });
230 | expect(onChange).toHaveBeenCalledWith(
231 | {
232 | 'some-property-name': expect.objectContaining({
233 | target: expect.objectContaining({ value: 'Yay' }),
234 | }),
235 | },
236 | 'modify',
237 | 0
238 | );
239 | });
240 | });
241 |
--------------------------------------------------------------------------------
/src/validators-properties/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './validators-properties';
2 | export * from './validators-properties';
3 |
--------------------------------------------------------------------------------
/src/validators-properties/validators-properties.js:
--------------------------------------------------------------------------------
1 | import { validatorTypes } from '@data-driven-forms/react-form-renderer';
2 |
3 | const messageType = {
4 | label: 'Message',
5 | component: 'input',
6 | propertyName: 'message',
7 | };
8 |
9 | const thresholdType = {
10 | propertyName: 'threshold',
11 | label: 'Threshold',
12 | component: 'input',
13 | type: 'number',
14 | };
15 |
16 | const includeThresholdType = {
17 | propertyName: 'includeThreshold',
18 | label: 'Include threshold',
19 | component: 'switch',
20 | };
21 |
22 | const patternType = {
23 | label: 'Pattern',
24 | component: 'input',
25 | propertyName: 'pattern',
26 | };
27 |
28 | const urlOptions = ['emptyProtocol', 'protocolIdentifier', 'basicAuth', 'local', 'ipv4', 'ipv6', 'host', 'port', 'path', 'search', 'hash'];
29 |
30 | const urlTypes = urlOptions.map((option) => ({
31 | propertyName: option,
32 | label: option
33 | .split(/(?=[A-Z])/)
34 | .join(' ')
35 | .replace(/^./, option[0].toUpperCase()),
36 | component: 'switch',
37 | }));
38 |
39 | export default {
40 | [validatorTypes.MAX_LENGTH]: [
41 | {
42 | ...thresholdType,
43 | label: 'Max length',
44 | restriction: {
45 | inputAttribute: 'max',
46 | validatorAttribute: thresholdType.propertyName,
47 | },
48 | },
49 | messageType,
50 | ],
51 | [validatorTypes.MIN_LENGTH]: [
52 | {
53 | ...thresholdType,
54 | label: 'Min length',
55 | restriction: {
56 | inputAttribute: 'min',
57 | validatorAttribute: thresholdType.propertyName,
58 | },
59 | },
60 | messageType,
61 | ],
62 | [validatorTypes.EXACT_LENGTH]: [{ ...thresholdType, label: 'Exact length' }, messageType],
63 | [validatorTypes.MAX_NUMBER_VALUE]: [
64 | {
65 | ...thresholdType,
66 | label: 'Maximum value',
67 | propertyName: 'value',
68 | restriction: {
69 | inputAttribute: 'max',
70 | validatorAttribute: 'value',
71 | },
72 | },
73 | {
74 | ...includeThresholdType,
75 | label: 'Include value',
76 | },
77 | messageType,
78 | ],
79 | [validatorTypes.MIN_NUMBER_VALUE]: [
80 | {
81 | ...thresholdType,
82 | label: 'Minimum value',
83 | propertyName: 'value',
84 | restriction: {
85 | inputAttribute: 'min',
86 | validatorAttribute: 'value',
87 | },
88 | },
89 | {
90 | ...includeThresholdType,
91 | label: 'Include value',
92 | },
93 | messageType,
94 | ],
95 | [validatorTypes.MIN_ITEMS]: [{ ...thresholdType, label: 'Minimum number of items' }, messageType],
96 | [validatorTypes.PATTERN]: [{ ...patternType, restriction: { lock: true } }, messageType],
97 | [validatorTypes.URL]: [messageType, ...urlTypes],
98 | };
99 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const HtmlWebPackPlugin = require('html-webpack-plugin');
2 | const path = require('path');
3 |
4 | module.exports = {
5 | entry: path.resolve(__dirname, './demo/index.js'),
6 | devtool: 'eval-source-map',
7 | resolve: {
8 | alias: {
9 | react: path.resolve(__dirname, './node_modules/react'),
10 | 'react-dom': path.resolve(__dirname, './node_modules/react-dom'),
11 | },
12 | },
13 | module: {
14 | rules: [
15 | {
16 | test: /\.css$/i,
17 | use: ['style-loader', 'css-loader'],
18 | },
19 | {
20 | test: /\.(png|jpg|gif|svg|woff|ttf|eot)/,
21 | type: 'asset/resource',
22 | },
23 | {
24 | test: /\.(js|jsx)$/,
25 | exclude: /node_modules/,
26 | use: {
27 | loader: 'babel-loader',
28 | },
29 | },
30 | ],
31 | },
32 | plugins: [
33 | new HtmlWebPackPlugin({
34 | template: './demo/index.html',
35 | filename: './index.html',
36 | }),
37 | ],
38 | };
39 |
--------------------------------------------------------------------------------