├── .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 | [![codecov](https://codecov.io/gh/data-driven-forms/form-builder/branch/master/graph/badge.svg)](https://codecov.io/gh/data-driven-forms/form-builder) 2 | [![CircleCI](https://circleci.com/gh/data-driven-forms/react-forms/tree/master.svg?style=svg)](https://circleci.com/gh/data-driven-forms/form-builder/tree/master) 3 | [![npm version](https://badge.fury.io/js/%40data-driven-forms%2Fform-builder.svg)](https://badge.fury.io/js/%40data-driven-forms%2Fform-builder) 4 | [![Tweet](https://img.shields.io/twitter/url/https/github.com/tterb/hyde.svg?style=social)](https://twitter.com/intent/tweet?text=Check%20DataDrivenForms%20React%20library%21%20https%3A%2F%2Fdata-driven-forms.org%2F&hashtags=react,opensource,datadrivenforms) 5 | [![Twitter Follow](https://img.shields.io/twitter/follow/DataDrivenForms.svg?style=social)](https://twitter.com/DataDrivenForms) 6 | 7 | [![Data Driven Form logo](https://raw.githubusercontent.com/data-driven-forms/react-forms/master/images/logo.png)](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 | ![image](https://user-images.githubusercontent.com/32869456/95745136-d6995c00-0c94-11eb-9f86-88fe1417e743.png) 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 |
{}}> 57 | {() => ( 58 | null, renderForm: () => null, internalUnRegisterField: () => null } }} 60 | > 61 | 62 | 67 | {children} 68 | 69 | 70 | 71 | )} 72 |
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 | 293 | 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 |
21 | 22 | 23 | 24 |
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 | 85 | {option} 86 | 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 | 178 | 192 | 197 | 198 | ))} 199 | 200 |
170 | handleOptionChange(e.target.value, index, 'label')} 173 | disabled={deleted} 174 | placeholder="Label" 175 | aria-label={`option-label-${index}`} 176 | /> 177 | 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 | 193 | handleRemove(index, restoreable)} variant="plain" aria-label="delete option" size="large"> 194 | {deleted ? : } 195 | 196 |
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 |
40 |
41 | 42 | 47 |
48 |
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 | <Button className="editor-header-button" variant="plain" onClick={handleDelete} isDisabled={!handleDelete} aria-label="delete field"> 130 | <TrashIcon /> 131 | </Button> 132 | )} 133 | <Button className="editor-header-button" variant="plain" aria-label="close properties editor" onClick={handleClose}> 134 | <TimesIcon /> 135 | </Button> 136 | 137 | 138 | 139 | 140 | 141 | setActiveTab(tabIndex)}> 142 | Properties {hasPropertyError && }} 146 | /> 147 | {!disableValidators && } 148 | 149 | 150 | 151 | 158 |