├── .circleci └── config.yml ├── .editorconfig ├── .gitattributes ├── .github └── issue_template.md ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── config ├── .eslintrc ├── babel-config.js ├── mocha-config.js ├── webpack.config.build.js ├── webpack.config.client.js ├── webpack.config.common.js └── webpack.config.showroom.js ├── docs ├── DataFlow.png └── ViewStatesTransitions.png ├── operationsProposal.md ├── package.json ├── scripts └── gh-pages │ ├── build.sh │ └── deploy.sh ├── src ├── components │ ├── ConfirmDialog │ │ ├── ConfirmDialog.spec.js │ │ ├── ConfirmUnsavedChanges.js │ │ ├── index.js │ │ └── styles.less │ ├── CreateMain │ │ └── index.js │ ├── DeferValueSyncHOC │ │ └── index.js │ ├── EditField │ │ ├── index.js │ │ └── styles.less │ ├── EditHeading │ │ └── index.js │ ├── EditMain │ │ └── index.js │ ├── EditSection │ │ ├── index.js │ │ └── styles.less │ ├── EditTab │ │ ├── index.js │ │ └── styles.less │ ├── ErrorMain │ │ └── index.js │ ├── FieldBoolean │ │ ├── FieldBoolean.spec.js │ │ └── index.js │ ├── FieldDate │ │ ├── FieldDate.spec.js │ │ └── index.js │ ├── FieldErrors │ │ ├── FieldErrorLabel.js │ │ └── WithFieldErrorsHOC.js │ ├── FieldString │ │ ├── FieldString.spec.js │ │ └── index.js │ ├── FormGrid │ │ └── index.js │ ├── GenericInput │ │ ├── GenericInput.DOCUMENTATION.md │ │ ├── GenericInput.SCOPE.react.js │ │ ├── GenericInput.react.js │ │ └── index.js │ ├── OperationsBar │ │ ├── Operation.js │ │ └── index.js │ ├── RangeInput │ │ ├── RangeInput.DOCUMENTATION.md │ │ ├── RangeInput.SCOPE.react.js │ │ ├── RangeInput.less │ │ ├── RangeInput.react.js │ │ ├── components │ │ │ ├── DateRangeInput │ │ │ │ ├── DateRangeInput.DOCUMENTATION.md │ │ │ │ ├── DateRangeInput.SCOPE.react.js │ │ │ │ ├── DateRangeInput.react.js │ │ │ │ └── index.js │ │ │ └── StringRangeInput │ │ │ │ ├── StringRangeInput.DOCUMENTATION.md │ │ │ │ ├── StringRangeInput.SCOPE.react.js │ │ │ │ ├── StringRangeInput.less │ │ │ │ ├── StringRangeInput.react.js │ │ │ │ └── index.js │ │ └── index.js │ ├── ResizableGrid │ │ ├── ResizableGrid.DOCUMENTATION.md │ │ ├── ResizableGrid.SCOPE.react.js │ │ ├── ResizableGrid.less │ │ ├── ResizableGrid.react.js │ │ ├── index.js │ │ ├── integrationWithCrudEditorStyles.less │ │ └── store │ │ │ └── localStore.js │ ├── SearchBulkOperationsPanel │ │ ├── SearchBulkOperationsPanel.less │ │ └── index.js │ ├── SearchForm │ │ ├── SearchForm.less │ │ └── index.js │ ├── SearchMain │ │ ├── SearchMain.less │ │ └── index.js │ ├── SearchPaginationPanel │ │ ├── PaginationPanel.js │ │ ├── SearchPaginationPanel.less │ │ └── index.js │ ├── SearchResult │ │ ├── SearchResult.less │ │ └── index.js │ ├── SearchResultListing │ │ ├── SearchResultButtons.js │ │ ├── index.js │ │ └── styles.less │ ├── ShowMain │ │ └── index.js │ ├── Spinner │ │ ├── SpinnerOverlay.less │ │ ├── SpinnerOverlayHOC.js │ │ ├── spinner.svg │ │ └── spinner2.svg │ ├── lib.js │ └── lib.spec.js ├── crudeditor-lib │ ├── check-model │ │ ├── formLayout.js │ │ ├── index.js │ │ ├── lib.js │ │ ├── modelDefinition.js │ │ └── searchUi.js │ ├── common │ │ ├── actions.js │ │ ├── constants.js │ │ ├── reducer.js │ │ ├── scenario.js │ │ └── workerSagas │ │ │ ├── adjacent.js │ │ │ ├── delete.js │ │ │ ├── delete.spec.js │ │ │ ├── redirect.js │ │ │ ├── redirect.spec.js │ │ │ ├── save.js │ │ │ └── validate.js │ ├── components │ │ ├── Main │ │ │ └── index.js │ │ ├── ViewSwitcher │ │ │ └── index.js │ │ └── WithAlertsHOC │ │ │ ├── index.js │ │ │ └── styles.css │ ├── i18n │ │ ├── da.js │ │ ├── de.js │ │ ├── en.js │ │ ├── exceptions │ │ │ ├── da.js │ │ │ ├── de.js │ │ │ ├── en.js │ │ │ ├── fi.js │ │ │ ├── no.js │ │ │ ├── ru.js │ │ │ └── sv.js │ │ ├── fi.js │ │ ├── index.js │ │ ├── no.js │ │ ├── ru.js │ │ └── sv.js │ ├── index.js │ ├── lib.js │ ├── lib.spec.js │ ├── middleware │ │ ├── appStateChangeDetect.js │ │ └── notifications │ │ │ ├── ExpandableNotice.less │ │ │ ├── ExpandableNotice.react.js │ │ │ └── index.js │ ├── rootReducer.js │ ├── rootSaga.js │ ├── selectorWrapper.js │ └── views │ │ ├── create │ │ ├── actions.js │ │ ├── constants.js │ │ ├── container.js │ │ ├── index.js │ │ ├── index.spec.js │ │ ├── reducer.js │ │ ├── scenario.js │ │ ├── scenario.spec.js │ │ ├── selectors.js │ │ └── workerSagas │ │ │ ├── save.js │ │ │ └── save.spec.js │ │ ├── edit │ │ ├── actions.js │ │ ├── constants.js │ │ ├── container.js │ │ ├── index.js │ │ ├── index.spec.js │ │ ├── reducer.js │ │ ├── scenario.js │ │ ├── scenario.spec.js │ │ ├── selectors.js │ │ └── workerSagas │ │ │ ├── delete.js │ │ │ ├── delete.spec.js │ │ │ ├── edit.js │ │ │ ├── save.js │ │ │ └── save.spec.js │ │ ├── error │ │ ├── constants.js │ │ ├── container.js │ │ ├── index.js │ │ ├── reducer.js │ │ ├── scenario.js │ │ ├── scenario.spec.js │ │ └── selectors.js │ │ ├── lib.js │ │ ├── lib.spec.js │ │ ├── search │ │ ├── actions.js │ │ ├── constants.js │ │ ├── container.js │ │ ├── index.js │ │ ├── index.spec.js │ │ ├── lib.js │ │ ├── lib.spec.js │ │ ├── reducer.js │ │ ├── reducer.spec.js │ │ ├── scenario.js │ │ ├── scenario.spec.js │ │ ├── selectors.js │ │ └── workerSagas │ │ │ ├── customBulkOperation.js │ │ │ ├── delete.js │ │ │ ├── delete.spec.js │ │ │ ├── search.js │ │ │ └── search.spec.js │ │ └── show │ │ ├── actions.js │ │ ├── constants.js │ │ ├── container.js │ │ ├── index.js │ │ ├── index.spec.js │ │ ├── reducer.js │ │ ├── scenario.js │ │ ├── scenario.spec.js │ │ ├── selectors.js │ │ └── workerSagas │ │ ├── show.js │ │ └── show.spec.js ├── data-types-lib │ ├── constants.js │ ├── fieldTypes │ │ ├── boolean │ │ │ ├── booleanUiType.js │ │ │ ├── booleanUiType.spec.js │ │ │ ├── decimalUiType.js │ │ │ ├── decimalUiType.spec.js │ │ │ ├── index.js │ │ │ ├── index.spec.js │ │ │ ├── integerUiType.js │ │ │ ├── integerUiType.spec.js │ │ │ ├── stringUiType.js │ │ │ └── stringUiType.spec.js │ │ ├── decimal │ │ │ ├── decimalUiType.js │ │ │ ├── decimalUiType.spec.js │ │ │ ├── index.js │ │ │ ├── index.spec.js │ │ │ ├── stringUiType.js │ │ │ └── stringUiType.spec.js │ │ ├── decimalRange │ │ │ └── index.js │ │ ├── index.js │ │ ├── integer │ │ │ ├── index.js │ │ │ ├── index.spec.js │ │ │ ├── integerUiType.js │ │ │ ├── integerUiType.spec.js │ │ │ ├── stringUiType.js │ │ │ └── stringUiType.spec.js │ │ ├── integerRange │ │ │ └── index.js │ │ ├── lib.js │ │ ├── lib.spec.js │ │ ├── string │ │ │ ├── decimalUiType.js │ │ │ ├── decimalUiType.spec.js │ │ │ ├── index.js │ │ │ ├── index.spec.js │ │ │ ├── integerUiType.js │ │ │ ├── integerUiType.spec.js │ │ │ ├── stringUiType.js │ │ │ └── stringUiType.spec.js │ │ ├── stringDate │ │ │ ├── dateUiType.js │ │ │ ├── dateUiType.spec.js │ │ │ ├── index.js │ │ │ ├── index.spec.js │ │ │ ├── stringUiType.js │ │ │ └── stringUiType.spec.js │ │ ├── stringDateOnly │ │ │ ├── dateUiType.js │ │ │ ├── dateUiType.spec.js │ │ │ ├── index.js │ │ │ ├── index.spec.js │ │ │ ├── stringUiType.js │ │ │ └── stringUiType.spec.js │ │ ├── stringDateRange │ │ │ └── index.js │ │ ├── stringDecimal │ │ │ ├── decimalUiType.js │ │ │ ├── index.js │ │ │ ├── index.spec.js │ │ │ └── stringUiType.js │ │ ├── stringDecimalRange │ │ │ └── index.js │ │ ├── stringInteger │ │ │ ├── index.js │ │ │ ├── integerUiType.js │ │ │ └── stringUiType.js │ │ └── stringIntegerRange │ │ │ └── index.js │ ├── index.js │ ├── index.spec.js │ └── uiTypes │ │ ├── boolean.js │ │ ├── date.js │ │ ├── dateRangeObject.js │ │ ├── decimal.js │ │ ├── decimalRangeObject.js │ │ ├── index.js │ │ ├── integer.js │ │ ├── integerRangeObject.js │ │ ├── lib.js │ │ ├── string.js │ │ ├── stringRangeObject.js │ │ └── uiTypes.spec.js └── demo │ ├── client │ ├── components │ │ ├── CrudWrapper │ │ │ ├── index.js │ │ │ └── lib.js │ │ ├── Home │ │ │ └── index.js │ │ └── Revisions │ │ │ └── index.js │ ├── index.html │ ├── index.js │ └── routes.js │ ├── global-styles.less │ ├── models │ ├── contracts │ │ ├── api │ │ │ ├── api.js │ │ │ ├── api.spec.js │ │ │ ├── data.js │ │ │ └── index.js │ │ ├── components │ │ │ ├── ContractReferenceSearch │ │ │ │ ├── ReferenceSearchService.js │ │ │ │ ├── i18n │ │ │ │ │ ├── en.js │ │ │ │ │ └── index.js │ │ │ │ └── index.js │ │ │ ├── CustomSpinner.js │ │ │ ├── CustomTabComponent │ │ │ │ └── index.js │ │ │ ├── DateRangeCellRender │ │ │ │ ├── DateRangeCellRender.spec.js │ │ │ │ └── index.js │ │ │ └── StatusField │ │ │ │ ├── StatusField.spec.js │ │ │ │ └── index.js │ │ ├── i18n │ │ │ ├── de.js │ │ │ ├── en.js │ │ │ ├── index.js │ │ │ └── ru.js │ │ ├── index.js │ │ └── index.spec.js │ ├── index.js │ └── second-model │ │ ├── api │ │ ├── api.js │ │ ├── api.spec.js │ │ ├── data.js │ │ └── index.js │ │ ├── components │ │ └── CustomTabComponent │ │ │ └── index.js │ │ ├── i18n │ │ ├── de.js │ │ ├── en.js │ │ ├── index.js │ │ └── ru.js │ │ └── index.js │ └── showroom │ ├── ContractEditor.DOCUMENTATION.md │ ├── ContractEditor.SCOPE.react.js │ ├── ContractEditor.react.js │ └── ContractEditorScope.less └── www ├── index-page.js └── index.html /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings for all text-based files 2 | # http://git-scm.com/docs/gitattributes#_end_of_line_conversion 3 | * text=lf 4 | 5 | # Declare files that will always have LF line endings on checkout. 6 | *.js text eol=lf 7 | *.sh text eol=lf 8 | *.css text eol=lf 9 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 2 | Meta-Info | Value 3 | -- | -- 4 | ExtProjectId | ??? 5 | Original Estimation | ???h 6 | Remaining Estimation | ???h 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.swp 3 | *.kate-swp 4 | *.out 5 | *.iml 6 | *.lock 7 | package-lock.json 8 | 9 | public 10 | .gh-pages-tmp 11 | test-results.xml 12 | 13 | /resources/jcatalog/less/jquery-ui-bundle.css 14 | /resources/jcatalog/less/jcatalog-bootstrap-bundle.css 15 | /resources/jcatalog/less/jcatalog-bootstrap-extensions-bundle.css 16 | 17 | /db.config.json 18 | /src/server/data.json.backup 19 | 20 | .idea 21 | /target 22 | /build 23 | /dist 24 | /docs 25 | /esdoc 26 | /lib 27 | /apidoc 28 | 29 | # All the below is taken from 30 | # https://github.com/github/gitignore/blob/master/Node.gitignore 31 | # except for some /node_modules subfolders. 32 | 33 | # Logs 34 | logs 35 | *.log 36 | npm-debug.log* 37 | 38 | # Runtime data 39 | pids 40 | *.pid 41 | *.seed 42 | *.pid.lock 43 | 44 | # Directory for instrumented libs generated by jscoverage/JSCover 45 | lib-cov 46 | 47 | # Coverage directory used by tools like istanbul 48 | coverage 49 | 50 | # nyc test coverage 51 | .nyc_output 52 | 53 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 54 | .grunt 55 | 56 | # node-waf configuration 57 | .lock-wscript 58 | 59 | # Compiled binary addons (http://nodejs.org/api/addons.html) 60 | build/Release 61 | 62 | # Dependency directories 63 | jspm_packages/ 64 | /node_modules/* 65 | 66 | # Optional npm cache directory 67 | .npm 68 | 69 | # Optional eslint cache 70 | .eslintcache 71 | 72 | # Optional REPL history 73 | .node_repl_history 74 | .DS_Store 75 | -------------------------------------------------------------------------------- /config/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "opuscapita", 3 | "env": { 4 | "jasmine": true, 5 | "browser": true, 6 | "node": true, 7 | "es6": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /config/babel-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | babelrc: false, 3 | presets: [ 4 | ['env', { 5 | // TODO: remove "targets" key after babel 7.0 is out 6 | // because external config in package.json or browserslist will be supported in 7.0 7 | // For more details see 8 | // https://github.com/browserslist/browserslist 9 | targets: { 10 | browsers: [ 11 | 'chrome >= 64', 12 | 'firefox ESR', 13 | 'ie >= 11', 14 | 'safari >= 11' 15 | ] 16 | }, 17 | modules: process.env.NODE_ENV === 'test' ? 'commonjs' : false 18 | }], 19 | 'stage-3', 20 | 'react' 21 | ], 22 | plugins: [ 23 | // make sure that 'transform-decorators' comes before 'transform-class-properties' 24 | 'transform-decorators-legacy', 25 | 'transform-class-properties', 26 | ['transform-runtime', { 'polyfill': false }], 27 | 'transform-export-extensions' 28 | ], 29 | env: { 30 | test: { 31 | plugins: ['istanbul'] 32 | } 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /config/mocha-config.js: -------------------------------------------------------------------------------- 1 | const babelConfig = require('./babel-config'); 2 | require('babel-register')(babelConfig); 3 | 4 | const JSDOM = require('jsdom').JSDOM; 5 | global.document = new JSDOM(''); 6 | global.window = global.document.window; 7 | global.document = window.document; 8 | global.navigator = global.window.navigator; 9 | global.self = global.window; 10 | -------------------------------------------------------------------------------- /config/webpack.config.build.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | const merge = require('webpack-merge'); 3 | const common = require('./webpack.config.common'); 4 | const nodeExternals = require('webpack-node-externals'); 5 | 6 | module.exports = [ 7 | merge(common, { 8 | entry: [ 9 | './crudeditor-lib/index.js' 10 | ], 11 | output: { 12 | path: resolve(__dirname, '../lib'), 13 | filename: 'index.js', 14 | library: `ReactCrudEditor`, 15 | libraryTarget: 'umd' 16 | }, 17 | externals: [ 18 | nodeExternals({ 19 | modulesFromFile: true 20 | }) 21 | ] 22 | }), 23 | merge(common, { 24 | name: 'ResizableGrid', 25 | entry: './components/ResizableGrid/index.js', 26 | output: { 27 | path: resolve(__dirname, '../lib'), 28 | filename: 'ResizableGrid.js', 29 | library: 'ResizableGrid', 30 | libraryTarget: 'umd' 31 | }, 32 | externals: [ 33 | nodeExternals({ 34 | modulesFromFile: true 35 | }) 36 | ] 37 | }) 38 | ]; 39 | -------------------------------------------------------------------------------- /config/webpack.config.client.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | const merge = require('webpack-merge'); 3 | const common = require('./webpack.config.common'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | module.exports = merge(common, { 7 | plugins: [ 8 | new HtmlWebpackPlugin({ 9 | template: './demo/client/index.html', 10 | inject: "body" 11 | }) 12 | ], 13 | entry: [ 14 | './demo/client/index.js' 15 | ], 16 | devtool: 'inline-source-map', 17 | output: { 18 | path: resolve(__dirname, '../public'), 19 | filename: 'bundle.js', 20 | publicPath: '/' 21 | }, 22 | devServer: { 23 | contentBase: './public', 24 | historyApiFallback: true, 25 | inline: false 26 | } 27 | }); 28 | 29 | -------------------------------------------------------------------------------- /config/webpack.config.showroom.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const merge = require('webpack-merge'); 4 | const common = require('./webpack.config.common'); 5 | 6 | module.exports = merge(common, { 7 | plugins: [ 8 | new HtmlWebpackPlugin({ 9 | template: '../www/index.html', 10 | }) 11 | ], 12 | entry: [ 13 | '../www/index-page.js' 14 | ], 15 | output: { 16 | path: resolve(__dirname, '../.gh-pages-tmp'), 17 | filename: 'bundle.js' 18 | }, 19 | devServer: { 20 | contentBase: './.gh-pages-tmp', 21 | historyApiFallback: true, 22 | inline: false 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.md$/, 28 | use: ['raw-loader'] 29 | } 30 | ] 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /docs/DataFlow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpusCapita/react-crudeditor/d156871e90ee1d8a983fca297dfc50688ebba255/docs/DataFlow.png -------------------------------------------------------------------------------- /docs/ViewStatesTransitions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpusCapita/react-crudeditor/d156871e90ee1d8a983fca297dfc50688ebba255/docs/ViewStatesTransitions.png -------------------------------------------------------------------------------- /scripts/gh-pages/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf .gh-pages-tmp && 4 | node node_modules/@opuscapita/react-showroom-server/src/bin/showroom-scan.js src && 5 | node node_modules/webpack/bin/webpack.js --config config/webpack.config.showroom.js 6 | -------------------------------------------------------------------------------- /scripts/gh-pages/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # ideas used from https://gist.github.com/motemen/8595451 3 | 4 | # abort the script if there is a non-zero error 5 | set -e 6 | set -x 7 | 8 | # show where we are on the machine 9 | pwd 10 | 11 | BASEDIR=$(dirname "$0") 12 | SITE_SOURCE="$1" 13 | 14 | if [ ! -d "$SITE_SOURCE" ] 15 | then 16 | echo "Usage: $0 " 17 | exit 1 18 | fi 19 | 20 | # get current git branch name 21 | GIT_BRANCH=`git rev-parse --abbrev-ref HEAD` 22 | 23 | # replace "/", "#", etc. in current git branch name 24 | urlencode() { 25 | node -e "console.log('${*}'.replace('/', '(slash)').replace('#', '(hash)'))" 26 | } 27 | 28 | SAFE_GIT_BRANCH=`urlencode $GIT_BRANCH` 29 | echo "Current branch is $SAFE_GIT_BRANCH" 30 | 31 | # now lets setup a new repo so we can update the gh-pages branch 32 | git config --global user.email "$GH_MAIL" > /dev/null 2>&1 33 | git config --global user.name "$GH_NAME" > /dev/null 2>&1 34 | 35 | # switch into the the gh-pages branch 36 | if git rev-parse --verify origin/gh-pages > /dev/null 2>&1 37 | then 38 | git checkout gh-pages 39 | git pull 40 | else 41 | git checkout --orphan gh-pages 42 | fi 43 | 44 | # delete any old site as we are going to replace it 45 | rm -rf "./branches/$SAFE_GIT_BRANCH" 46 | mkdir -p "./branches/$SAFE_GIT_BRANCH" 47 | 48 | # copy over or recompile the new site 49 | cp -r ./$SITE_SOURCE/* "./branches/$SAFE_GIT_BRANCH" 50 | 51 | # stage any changes and new files 52 | git add -A 53 | # now commit, ignoring branch gh-pages doesn't seem to work, so trying skip 54 | git commit --allow-empty -m "Deploy to GitHub pages [ci skip]" 55 | # and push, but send any output to /dev/null to hide anything sensitive 56 | git push --force --quiet origin gh-pages > /dev/null 2>&1 57 | 58 | echo "Finished Deployment!" -------------------------------------------------------------------------------- /src/components/ConfirmDialog/ConfirmDialog.spec.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Enzyme from "enzyme"; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | import { expect } from 'chai'; 5 | import sinon from 'sinon'; 6 | import ConfirmDialog from './'; 7 | import { I18nManager } from '@opuscapita/i18n'; 8 | 9 | Enzyme.configure({ adapter: new Adapter() }); 10 | 11 | const context = { 12 | i18n: new I18nManager() 13 | } 14 | 15 | describe("ConfirmDialog", _ => { 16 | it("should properly render", () => { 17 | const onClick = sinon.spy(); 18 | const showDialogInner = sinon.spy(); 19 | const showDialog = _ => { 20 | showDialogInner(); 21 | return true 22 | } 23 | const child = _ => (); 24 | const wrapper = Enzyme.mount({child}, { 25 | context 26 | }); 27 | expect(wrapper).to.exist; // eslint-disable-line no-unused-expressions 28 | }); 29 | 30 | it("should render array of children into a span", () => { 31 | const onClick = sinon.spy(); 32 | const showDialogInner = sinon.spy(); 33 | const showDialog = _ => { 34 | showDialogInner(); 35 | return true 36 | } 37 | const child = _ => (); 38 | const children = [child, child, child]; 39 | const wrapper = Enzyme.mount({children}, { 40 | context 41 | }); 42 | 43 | /* eslint-disable no-unused-expressions */ 44 | expect(wrapper.getDOMNode()).to.have.property('className').equal('confirm-dialog-span'); 45 | /* eslint-enable no-unused-expressions */ 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/components/ConfirmDialog/ConfirmUnsavedChanges.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ConfirmDialog from './'; 4 | 5 | export default class ConfirmUnsavedChanges extends PureComponent { 6 | static propTypes = { 7 | trigger: PropTypes.string, 8 | showDialog: PropTypes.func 9 | } 10 | 11 | static contextTypes = { 12 | i18n: PropTypes.object.isRequired 13 | } 14 | 15 | render() { 16 | const { children, ...rest } = this.props; 17 | const { i18n } = this.context; 18 | 19 | return ( 20 | 26 | {children} 27 | 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/ConfirmDialog/styles.less: -------------------------------------------------------------------------------- 1 | .btn-toolbar > .confirm-dialog-span > button { 2 | margin-left: 5px; 3 | border-collapse: collapse; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /src/components/CreateMain/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Heading from '../EditHeading'; 4 | import Tab from '../EditTab'; 5 | import WithFieldErrors from '../FieldErrors/WithFieldErrorsHOC'; 6 | import WithSpinner from '../Spinner/SpinnerOverlayHOC'; 7 | import { VIEW_NAME } from '../../crudeditor-lib/views/create/constants'; 8 | 9 | const CreateMain = ({ model, toggledFieldErrors, toggleFieldErrors }) => { 10 | const ActiveTabComponent = model.data.activeTab && model.data.activeTab.component; 11 | 12 | return (
13 | 14 | {ActiveTabComponent ? 15 | : 16 | 17 | } 18 |
) 19 | } 20 | 21 | CreateMain.propTypes = { 22 | model: PropTypes.shape({ 23 | data: PropTypes.shape({ 24 | viewName: PropTypes.oneOf([VIEW_NAME]).isRequired, 25 | persistentInstance: PropTypes.object, 26 | activeTab: PropTypes.array 27 | }).isRequired 28 | }).isRequired, 29 | toggledFieldErrors: PropTypes.object.isRequired, 30 | toggleFieldErrors: PropTypes.func.isRequired 31 | } 32 | 33 | export default WithSpinner(WithFieldErrors(CreateMain)); 34 | -------------------------------------------------------------------------------- /src/components/DeferValueSyncHOC/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { findDOMNode } from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | 5 | export default WrappedComponent => class DeferValueSyncHOC extends PureComponent { 6 | static propTypes = { 7 | value: PropTypes.any, 8 | onChange: PropTypes.func, 9 | onBlur: PropTypes.func 10 | } 11 | 12 | static defaultProps = { 13 | value: null 14 | } 15 | 16 | constructor(...args) { 17 | super(...args); 18 | 19 | this.state = { 20 | value: this.props.value 21 | } 22 | } 23 | 24 | componentDidMount() { 25 | this.me = findDOMNode(this); 26 | this.me.addEventListener('keydown', this.handleEnterKey) 27 | } 28 | 29 | componentWillUnmount() { 30 | this.me.removeEventListener('keydown', this.handleEnterKey) 31 | } 32 | 33 | syncPropsAndState = callback => this.setState({ value: this.props.value }, callback); 34 | 35 | handleEnterKey = e => { 36 | const key = e.key === 'Enter' ? 13 : e.keyCode || e.charCode; 37 | 38 | if (key === 13) { 39 | this.syncPropsAndState() 40 | } 41 | } 42 | 43 | handleChange = value => this.setState({ value }, _ => this.props.onChange && this.props.onChange(value)); 44 | 45 | handleBlur = _ => this.syncPropsAndState(_ => this.props.onBlur && this.props.onBlur()); 46 | 47 | render() { 48 | const { children, onChange, onBlur, ...props } = this.props; 49 | 50 | const newProps = { 51 | ...props, 52 | value: this.me && ( 53 | this.me === document.activeElement || 54 | this.me.hasChildNodes() && 55 | Array.prototype.some.call(this.me.children, el => el === document.activeElement) 56 | ) ? 57 | this.state.value : 58 | this.props.value, 59 | ...(onChange && { onChange: this.handleChange }), 60 | ...(onBlur && { onBlur: this.handleBlur }) 61 | } 62 | 63 | return ( 64 | 65 | {children} 66 | 67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/components/EditField/styles.less: -------------------------------------------------------------------------------- 1 | .hint { 2 | font-weight: normal; 3 | font-size: 100%; 4 | border: none !important 5 | } 6 | 7 | .field-tooltip { 8 | cursor: pointer; 9 | position: absolute; 10 | left: -7px; 11 | top: 9px; 12 | } 13 | -------------------------------------------------------------------------------- /src/components/EditMain/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Heading from '../EditHeading'; 4 | import Tab from '../EditTab'; 5 | import WithFieldErrors from '../FieldErrors/WithFieldErrorsHOC'; 6 | import WithSpinner from '../Spinner/SpinnerOverlayHOC'; 7 | import { VIEW_NAME } from '../../crudeditor-lib/views/edit/constants'; 8 | 9 | const EditMain = ({ model, toggledFieldErrors, toggleFieldErrors }) => { 10 | const ActiveTabComponent = model.data.activeTab && model.data.activeTab.component; 11 | 12 | return (
13 | 14 | {ActiveTabComponent ? 15 | : 16 | 17 | } 18 |
); 19 | }; 20 | 21 | EditMain.propTypes = { 22 | model: PropTypes.shape({ 23 | data: PropTypes.shape({ 24 | activeTab: PropTypes.array, 25 | viewName: PropTypes.oneOf([VIEW_NAME]).isRequired, 26 | persistentInstance: PropTypes.object 27 | }).isRequired 28 | }).isRequired, 29 | toggledFieldErrors: PropTypes.object.isRequired, 30 | toggleFieldErrors: PropTypes.func.isRequired 31 | } 32 | 33 | export default WithSpinner(WithFieldErrors(EditMain)); 34 | -------------------------------------------------------------------------------- /src/components/EditSection/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Collapse from 'react-bootstrap/lib/Collapse'; 4 | import { getModelMessage, titleCase } from '../lib'; 5 | import './styles.less'; 6 | 7 | export default class EditSelection extends Component { 8 | static propTypes = { 9 | title: PropTypes.string.isRequired 10 | } 11 | 12 | static contextTypes = { 13 | i18n: PropTypes.object.isRequired 14 | } 15 | 16 | state = { 17 | collapsed: false 18 | } 19 | 20 | handleSelect = _ => this.setState({ 21 | collapsed: !this.state.collapsed 22 | }) 23 | 24 | render() { 25 | const { 26 | title, 27 | children: fields 28 | } = this.props; 29 | 30 | const { collapsed } = this.state; 31 | 32 | return ( 33 |
34 |

38 | 39 | 43 | {getModelMessage({ 44 | i18n: this.context.i18n, 45 | key: `model.section.${title}.label`, 46 | defaultMessage: titleCase(title) 47 | })} 48 | 49 |

50 | 51 |
52 | {fields} 53 |
54 |
55 |
56 | 57 | ); 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /src/components/EditSection/styles.less: -------------------------------------------------------------------------------- 1 | .panel-default { 2 | border: none; 3 | } 4 | 5 | .panel-default .panel-body { 6 | padding: 0; 7 | } -------------------------------------------------------------------------------- /src/components/EditTab/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Form from 'react-bootstrap/lib/Form'; 4 | import FormGroup from 'react-bootstrap/lib/FormGroup'; 5 | import Col from 'react-bootstrap/lib/Col'; 6 | import ButtonToolbar from 'react-bootstrap/lib/ButtonToolbar'; 7 | import OperationsBar from '../OperationsBar'; 8 | import FormGrid from '../FormGrid'; 9 | import './styles.less'; 10 | 11 | export default class EditTab extends PureComponent { 12 | static propTypes = { 13 | model: PropTypes.shape({ 14 | operations: PropTypes.any 15 | }).isRequired, 16 | toggleFieldErrors: PropTypes.func, 17 | toggledFieldErrors: PropTypes.object 18 | } 19 | 20 | handleSubmit = e => { 21 | e.preventDefault(); 22 | this.props.toggleFieldErrors(true); 23 | } 24 | 25 | render() { 26 | const { 27 | model: { operations }, 28 | toggledFieldErrors, 29 | toggleFieldErrors 30 | } = this.props; 31 | 32 | return ( 33 | 34 | {buttons => ( 35 |
36 | 37 | 42 | 43 | 44 | 45 |
46 | 47 | { buttons } 48 | 49 |
50 | 51 |
52 | 53 | )} 54 |
55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/components/EditTab/styles.less: -------------------------------------------------------------------------------- 1 | .crud--search-result-listing__action-buttons { 2 | display: flex !important; 3 | justify-content: flex-end; 4 | } 5 | 6 | .dropdown.btn-group { 7 | display: flex !important; 8 | } 9 | 10 | ul.dropdown-menu { 11 | text-align: left; 12 | left: auto; 13 | right: 0; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/ErrorMain/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from 'react-bootstrap/lib/Button'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const ErrorMain = ({ 6 | model: { 7 | data: { 8 | errors 9 | }, 10 | actions: { 11 | goHome 12 | }, 13 | uiConfig: { 14 | headerLevel = 1 15 | } 16 | } 17 | }) => { 18 | const H = 'h' + headerLevel; 19 | 20 | return ( 21 |
22 | { 23 | errors.length === 0 ? 24 | Unknown Error : 25 | errors.map(({ code, payload }, index) => ( 26 |
27 | Error {code} 28 | { payload ? payload.message || JSON.stringify(payload) : null } 29 |
30 |
31 | )) 32 | } 33 | { 34 | goHome && 35 | 38 | } 39 |
40 | ); 41 | }; 42 | 43 | ErrorMain.propTypes = { 44 | model: PropTypes.shape({ 45 | actions: PropTypes.shape({ 46 | goHome: PropTypes.func 47 | }), 48 | data: PropTypes.shape({ 49 | errors: PropTypes.arrayOf(PropTypes.object).isRequired 50 | }), 51 | uiConfig: PropTypes.object.isRequired 52 | }).isRequired 53 | } 54 | 55 | export default ErrorMain; 56 | -------------------------------------------------------------------------------- /src/components/FieldBoolean/FieldBoolean.spec.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Enzyme from "enzyme"; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | import { expect } from 'chai'; 5 | import sinon from 'sinon'; 6 | import FieldBoolean from "./"; 7 | import Checkbox from 'react-bootstrap/lib/Checkbox'; 8 | 9 | Enzyme.configure({ adapter: new Adapter() }); 10 | 11 | describe("FieldBoolean", _ => { 12 | it("should properly render", () => { 13 | const props = { 14 | value: true 15 | }; 16 | const wrapper = Enzyme.mount(); 17 | expect(wrapper.find(Checkbox).prop('checked')).to.be.true; // eslint-disable-line no-unused-expressions 18 | }); 19 | 20 | it("should render a checkbox and pass handlers", () => { 21 | const onChange = sinon.spy(); 22 | const onBlur = sinon.spy(); 23 | const props = { 24 | readOnly: false, 25 | value: false, 26 | onChange, 27 | onBlur 28 | }; 29 | const wrapper = Enzyme.mount(); 30 | const checkbox = wrapper.find(Checkbox) 31 | checkbox.prop('onChange')() 32 | checkbox.prop('onBlur')() 33 | expect(onChange.calledOnce).to.be.true; // eslint-disable-line no-unused-expressions 34 | expect(onBlur.calledOnce).to.be.true; // eslint-disable-line no-unused-expressions 35 | expect(onChange.calledWith(true)).to.be.true; // eslint-disable-line no-unused-expressions 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/components/FieldBoolean/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Checkbox from 'react-bootstrap/lib/Checkbox'; 4 | import { noop } from '../lib'; 5 | 6 | export default class FieldBoolean extends React.PureComponent { 7 | static propTypes = { 8 | readOnly: PropTypes.bool, 9 | value: PropTypes.bool, 10 | onChange: PropTypes.func, 11 | onBlur: PropTypes.func 12 | } 13 | 14 | static defaultProps = { 15 | readOnly: false, 16 | onChange: noop, 17 | onBlur: noop 18 | } 19 | 20 | constructor(...args) { 21 | super(...args); 22 | 23 | this.handleChange = this.props.readOnly ? 24 | noop : 25 | _ => this.props.onChange(!this.props.value); 26 | 27 | this.handleBlur = this.props.readOnly ? 28 | noop : 29 | this.props.onBlur; 30 | } 31 | 32 | render = _ => 33 | () 40 | } 41 | -------------------------------------------------------------------------------- /src/components/FieldDate/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { DateInput } from '@opuscapita/react-dates'; 4 | import { noop } from '../lib'; 5 | 6 | export default class FieldDate extends PureComponent { 7 | static propTypes = { 8 | readOnly: PropTypes.bool, 9 | value: PropTypes.instanceOf(Date), 10 | onChange: PropTypes.func, 11 | onBlur: PropTypes.func 12 | } 13 | 14 | static contextTypes = { 15 | i18n: PropTypes.object.isRequired 16 | } 17 | 18 | static defaultProps = { 19 | readOnly: false, 20 | value: null 21 | } 22 | 23 | constructor(...args) { 24 | super(...args); 25 | this.handleChange = !this.props.readOnly ? 26 | value => { 27 | // see description in render() function 28 | if (this.props.onChange) { 29 | this.props.onChange(value); 30 | if (this.props.onBlur) { 31 | this.props.onBlur() 32 | } 33 | } 34 | } : 35 | noop // prevents a propTypes warning about missing onChange handler for DateInput component 36 | } 37 | 38 | render = _ => 39 | // in DateInput component onBlur fires defore onChange 40 | // it break fields parsing logic 41 | // we won't listen to onBlur on the component 42 | // instead we'll manually call props.onBlur in onChange listener 43 | // right after we call props.onChange 44 | // Filed issue: https://github.com/OpusCapita/react-dates/issues/32 45 | () 55 | } 56 | -------------------------------------------------------------------------------- /src/components/FieldErrors/FieldErrorLabel.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Label from 'react-bootstrap/lib/Label'; 4 | import Fade from 'react-bootstrap/lib/Fade'; 5 | import { getFieldErrorMessage } from '../lib'; 6 | 7 | export default class FieldErrorLabel extends PureComponent { 8 | static propTypes = { 9 | errors: PropTypes.arrayOf(PropTypes.shape({ 10 | id: PropTypes.string, 11 | code: PropTypes.number, 12 | message: PropTypes.string, 13 | payload: PropTypes.string 14 | })), 15 | fieldName: PropTypes.string.isRequired 16 | } 17 | 18 | static contextTypes = { 19 | i18n: PropTypes.object.isRequired 20 | }; 21 | 22 | getErrorMessage = error => { 23 | const { i18n } = this.context; 24 | const { fieldName } = this.props; 25 | return getFieldErrorMessage({ error, i18n, fieldName }); 26 | } 27 | 28 | render() { 29 | const { errors } = this.props; 30 | 31 | return ( 32 |
33 | 34 | 37 | 38 |
39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/FieldString/FieldString.spec.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Enzyme from "enzyme"; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | import { expect } from 'chai'; 5 | import sinon from 'sinon'; 6 | import FieldString from "./"; 7 | import FormControl from 'react-bootstrap/lib/FormControl'; 8 | 9 | Enzyme.configure({ adapter: new Adapter() }); 10 | 11 | describe("FieldString", _ => { 12 | it("should properly render a FormControl", () => { 13 | const props = { 14 | value: 'some string' 15 | }; 16 | const wrapper = Enzyme.mount(); 17 | expect(wrapper.find(FormControl).prop('value')).to.equal(props.value); // eslint-disable-line no-unused-expressions 18 | }); 19 | 20 | it("should pass an empty string value for null/undefined value prop", () => { 21 | const props = { 22 | value: null 23 | }; 24 | const wrapper = Enzyme.mount(); 25 | expect(wrapper.find(FormControl).prop('value')).to.equal(''); // eslint-disable-line no-unused-expressions 26 | }); 27 | 28 | it("should render a FormControl and pass handlers", () => { 29 | const onChange = sinon.spy(); 30 | const onBlur = sinon.spy(); 31 | const props = { 32 | readOnly: false, 33 | value: 'some string', 34 | onChange, 35 | onBlur 36 | }; 37 | const wrapper = Enzyme.mount(); 38 | const fc = wrapper.find(FormControl) 39 | fc.prop('onChange')({ target: { value: 'new string' } }) 40 | fc.prop('onBlur')() 41 | expect(onChange.calledOnce).to.be.true; // eslint-disable-line no-unused-expressions 42 | expect(onBlur.calledOnce).to.be.true; // eslint-disable-line no-unused-expressions 43 | expect(onChange.calledWith('new string')).to.be.true; // eslint-disable-line no-unused-expressions 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/components/FieldString/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import FormControl from 'react-bootstrap/lib/FormControl'; 4 | import { noop } from '../lib'; 5 | 6 | export default class FieldString extends PureComponent { 7 | static propTypes = { 8 | readOnly: PropTypes.bool, 9 | value: PropTypes.string, 10 | onChange: PropTypes.func, 11 | onBlur: PropTypes.func 12 | } 13 | 14 | static defaultProps = { 15 | readOnly: false, 16 | value: '', 17 | onChange: noop, 18 | onBlur: noop 19 | } 20 | 21 | handleChange = ({ target: { value } }) => this.props.onChange(value); 22 | 23 | render = _ => 24 | () 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/components/GenericInput/GenericInput.DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | # GenericInput 2 | 3 | ## Synopsis 4 | 5 | Generic input component. 6 | 7 | ### Props Reference 8 | 9 | | Name | Type | Description | 10 | | ------------------------------ | :---------------------- | ----------------------------------------------------------- | 11 | | type | string | one of: string (default), checkbox, integer, decimal, date | 12 | 13 | ## Details 14 | 15 | ... 16 | 17 | ## Code Example 18 | 19 | ```js 20 | 27 | ``` 28 | 29 | ## Contributors 30 | 31 | Egor Stambakio 32 | 33 | ## Component Name 34 | 35 | GenericInput 36 | 37 | ## License 38 | 39 | Licensed by © 2017 OpusCapita -------------------------------------------------------------------------------- /src/components/GenericInput/GenericInput.SCOPE.react.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { showroomScopeDecorator } from '@opuscapita/react-showroom-client'; 4 | import { I18nManager } from '@opuscapita/i18n'; 5 | import translations from '../../crudeditor-lib/i18n'; 6 | 7 | // This @showroomScopeDecorator modify React.Component prototype by adding _renderChildren() method. 8 | export default 9 | @showroomScopeDecorator 10 | class GenericInputScope extends PureComponent { 11 | static childContextTypes = { 12 | i18n: PropTypes.object 13 | }; 14 | 15 | constructor(...args) { 16 | super(...args); 17 | 18 | this.i18n = new I18nManager({ locale: 'en' }); 19 | this.i18n.register('RangeInput', translations); 20 | } 21 | 22 | state = {} 23 | 24 | getChildContext() { 25 | return { i18n: this.i18n } 26 | } 27 | 28 | handleChange = value => this.setState({ value }, _ => { 29 | console.log(this.state.value) 30 | }) 31 | 32 | handleFocus = _ => console.log('FOCUS') 33 | 34 | handleBlur = _ => console.log('BLUR') 35 | 36 | render() { 37 | return ( 38 |
39 | {this._renderChildren()} 40 |
41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/GenericInput/GenericInput.react.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import FieldString from '../FieldString'; 4 | import FieldDate from '../FieldDate'; 5 | import FieldBoolean from '../FieldBoolean'; 6 | 7 | export default class GenericInput extends PureComponent { 8 | static propTypes = { 9 | type: PropTypes.oneOf([ 10 | 'checkbox', 11 | 'date', 12 | 'string' 13 | ]) 14 | } 15 | 16 | render() { 17 | const { type, children, ...props } = this.props; 18 | 19 | let Component = null; 20 | 21 | switch (type) { 22 | case 'date': 23 | Component = FieldDate; 24 | break; 25 | case 'checkbox': 26 | Component = FieldBoolean; 27 | break; 28 | default: 29 | Component = FieldString; 30 | } 31 | 32 | return ( 33 | 34 | {children} 35 | 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/GenericInput/index.js: -------------------------------------------------------------------------------- 1 | import GenericInput from './GenericInput.react'; 2 | 3 | export default GenericInput; 4 | -------------------------------------------------------------------------------- /src/components/RangeInput/RangeInput.DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | # RangeInput 2 | 3 | ## Synopsis 4 | 5 | String range input component. 6 | 7 | ### Props Reference 8 | 9 | ... 10 | 11 | ## Details 12 | 13 | ... 14 | 15 | ## Code Example 16 | 17 | ```js 18 | 25 | ``` 26 | 27 | ## Contributors 28 | 29 | Egor Stambakio 30 | 31 | ## Component Name 32 | 33 | RangeInput 34 | 35 | ## License 36 | 37 | Licensed by © 2017 OpusCapita -------------------------------------------------------------------------------- /src/components/RangeInput/RangeInput.SCOPE.react.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { showroomScopeDecorator } from '@opuscapita/react-showroom-client'; 4 | import { I18nManager } from '@opuscapita/i18n'; 5 | import translations from '../../crudeditor-lib/i18n'; 6 | 7 | // This @showroomScopeDecorator modify React.Component prototype by adding _renderChildren() method. 8 | export default 9 | @showroomScopeDecorator 10 | class RangeInputScope extends PureComponent { 11 | static childContextTypes = { 12 | i18n: PropTypes.object 13 | }; 14 | 15 | constructor(...args) { 16 | super(...args); 17 | 18 | this.i18n = new I18nManager({ locale: 'en' }); 19 | this.i18n.register('RangeInput', translations); 20 | } 21 | 22 | state = {} 23 | 24 | getChildContext() { 25 | return { i18n: this.i18n } 26 | } 27 | 28 | handleChange = value => this.setState({ value }, _ => { 29 | console.log(this.state.value) 30 | }) 31 | 32 | handleFocus = _ => console.log('FOCUS') 33 | 34 | handleBlur = _ => console.log('BLUR') 35 | 36 | render() { 37 | return ( 38 |
39 | {this._renderChildren()} 40 |
41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/RangeInput/RangeInput.less: -------------------------------------------------------------------------------- 1 | .crud--range-input { 2 | .form-control { 3 | text-align: center; 4 | } 5 | .input-group-addon { 6 | border-width: 1px 0; 7 | } 8 | } 9 | 10 | .unselectable { 11 | -webkit-touch-callout: none; 12 | -webkit-user-select: none; 13 | -khtml-user-select: none; 14 | -moz-user-select: none; 15 | -ms-user-select: none; 16 | user-select: none; 17 | } -------------------------------------------------------------------------------- /src/components/RangeInput/RangeInput.react.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import StringRangeInput from './components/StringRangeInput'; 4 | import DateRangeInput from './components/DateRangeInput'; 5 | 6 | export default class RangeInput extends PureComponent { 7 | static propTypes = { 8 | type: PropTypes.oneOf([ 9 | 'date', 10 | 'string' 11 | ]) 12 | } 13 | 14 | render() { 15 | const { type, ...props } = this.props; 16 | 17 | switch (type) { 18 | case 'date': 19 | return 20 | default: 21 | return 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/RangeInput/components/DateRangeInput/DateRangeInput.DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | # DateRangeInput 2 | 3 | ## Synopsis 4 | 5 | Date range input component. 6 | 7 | ### Props Reference 8 | 9 | | Name | Type | Description | 10 | | ------------------------------ | :---------------------- | ----------------------------------------------------------- | 11 | | readOnly | bool | true/false | 12 | | onChange | func | | 13 | | onBlur | func | | 14 | | onFocus | func | | 15 | | value | Object | { from: , to: } | 16 | 17 | ## Details 18 | 19 | ... 20 | 21 | ## Code Example 22 | 23 | ```js 24 | 30 | ``` 31 | 32 | ## Contributors 33 | 34 | Egor Stambakio 35 | 36 | ## Component Name 37 | 38 | DateRangeInput 39 | 40 | ## License 41 | 42 | Licensed by © 2017 OpusCapita -------------------------------------------------------------------------------- /src/components/RangeInput/components/DateRangeInput/DateRangeInput.SCOPE.react.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { showroomScopeDecorator } from '@opuscapita/react-showroom-client'; 4 | import { I18nManager } from '@opuscapita/i18n'; 5 | 6 | // This @showroomScopeDecorator modify React.Component prototype by adding _renderChildren() method. 7 | export default 8 | @showroomScopeDecorator 9 | class DateRangeInputScope extends PureComponent { 10 | static childContextTypes = { 11 | i18n: PropTypes.object 12 | }; 13 | 14 | constructor(...args) { 15 | super(...args); 16 | this.i18n = new I18nManager({ locale: 'en' }); 17 | } 18 | 19 | state = { 20 | value: { 21 | from: new Date(), 22 | to: new Date() 23 | } 24 | } 25 | 26 | getChildContext() { 27 | return { i18n: this.i18n } 28 | } 29 | 30 | handleChange = value => this.setState({ value }); 31 | 32 | handleFocus = _ => console.log('FOCUS') 33 | 34 | handleBlur = _ => console.log('BLUR') 35 | 36 | render() { 37 | return ( 38 |
39 | {this._renderChildren()} 40 |
41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/RangeInput/components/DateRangeInput/DateRangeInput.react.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { DateRangeInput as OCDateRangeInput } from '@opuscapita/react-dates'; 4 | import { noop } from '../../../lib'; 5 | 6 | const array2range = arr => ({ from: arr[0], to: arr[1] }); 7 | const range2array = range => [range.from, range.to]; 8 | 9 | export default class DateRangeInput extends PureComponent { 10 | static propTypes = { 11 | value: PropTypes.shape({ 12 | from: PropTypes.instanceOf(Date), 13 | to: PropTypes.instanceOf(Date) 14 | }), 15 | onChange: PropTypes.func, 16 | onBlur: PropTypes.func, 17 | onFocus: PropTypes.func, 18 | readOnly: PropTypes.bool 19 | } 20 | 21 | static contextTypes = { 22 | i18n: PropTypes.object 23 | } 24 | 25 | static defaultProps = { 26 | value: { from: null, to: null }, 27 | onChange: noop, 28 | onBlur: noop, 29 | onFocus: noop, 30 | readOnly: false 31 | } 32 | 33 | handleChange = value => this.props.onChange(array2range(value)) 34 | 35 | render() { 36 | const { 37 | value, 38 | onFocus, 39 | onBlur, 40 | readOnly 41 | } = this.props; 42 | const { i18n } = this.context; 43 | 44 | return ( 45 | 55 | ) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/components/RangeInput/components/DateRangeInput/index.js: -------------------------------------------------------------------------------- 1 | import DateRangeInput from './DateRangeInput.react'; 2 | 3 | export default DateRangeInput; 4 | -------------------------------------------------------------------------------- /src/components/RangeInput/components/StringRangeInput/StringRangeInput.DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | # StringRangeInput 2 | 3 | ## Synopsis 4 | 5 | String range input component. 6 | 7 | ### Props Reference 8 | 9 | | Name | Type | Description | 10 | | ------------------------------ | :---------------------- | ----------------------------------------------------------- | 11 | | type | string | 'integer' or 'decimal' | 12 | | readOnly | bool | true/false | 13 | | onChange | func | | 14 | | onBlur | func | | 15 | | onFocus | func | | 16 | | value | Object | { from: , to: } | 17 | 18 | ## Details 19 | 20 | ... 21 | 22 | ## Code Example 23 | 24 | ```js 25 | 31 | ``` 32 | 33 | ## Contributors 34 | 35 | Egor Stambakio 36 | 37 | ## Component Name 38 | 39 | StringRangeInput 40 | 41 | ## License 42 | 43 | Licensed by © 2017 OpusCapita -------------------------------------------------------------------------------- /src/components/RangeInput/components/StringRangeInput/StringRangeInput.SCOPE.react.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { showroomScopeDecorator } from '@opuscapita/react-showroom-client'; 4 | import { I18nManager } from '@opuscapita/i18n'; 5 | import translations from '../../../../crudeditor-lib/i18n'; 6 | 7 | // This @showroomScopeDecorator modify React.Component prototype by adding _renderChildren() method. 8 | export default 9 | @showroomScopeDecorator 10 | class StringRangeInputScope extends PureComponent { 11 | static childContextTypes = { 12 | i18n: PropTypes.object 13 | }; 14 | 15 | constructor(...args) { 16 | super(...args); 17 | 18 | this.i18n = new I18nManager({ locale: 'en' }); 19 | this.i18n.register('RangeInput', translations); 20 | } 21 | 22 | state = {} 23 | 24 | getChildContext() { 25 | return { i18n: this.i18n } 26 | } 27 | 28 | handleChange = value => this.setState({ value }, _ => { 29 | console.log(this.state.value) 30 | }) 31 | 32 | handleFocus = _ => console.log('FOCUS') 33 | 34 | handleBlur = _ => console.log('BLUR') 35 | 36 | render() { 37 | return ( 38 |
39 | {this._renderChildren()} 40 |
41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/RangeInput/components/StringRangeInput/StringRangeInput.less: -------------------------------------------------------------------------------- 1 | .crud--range-input { 2 | .form-control { 3 | text-align: center; 4 | } 5 | .input-group-addon { 6 | border-width: 1px 0; 7 | } 8 | } 9 | 10 | .unselectable { 11 | -webkit-touch-callout: none; 12 | -webkit-user-select: none; 13 | -khtml-user-select: none; 14 | -moz-user-select: none; 15 | -ms-user-select: none; 16 | user-select: none; 17 | } -------------------------------------------------------------------------------- /src/components/RangeInput/components/StringRangeInput/index.js: -------------------------------------------------------------------------------- 1 | import StringRangeInput from './StringRangeInput.react'; 2 | 3 | export default StringRangeInput; 4 | -------------------------------------------------------------------------------- /src/components/RangeInput/index.js: -------------------------------------------------------------------------------- 1 | import RangeInput from './RangeInput.react'; 2 | 3 | export default RangeInput; 4 | -------------------------------------------------------------------------------- /src/components/ResizableGrid/ResizableGrid.DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | # ResizableGrid 2 | 3 | ## Synopsis 4 | 5 | Applies resize functionality to child's table DOM element. 6 | 7 | ### Props Reference 8 | 9 | | Name | Type | Description | 10 | |-------------------------------|:-----------------------|-------------------------------------------------| 11 | | store | object | Store contains getValue/setValue 12 | 13 | ## Details 14 | 15 | ... 16 | 17 | ## Code Example 18 | 19 | ```js 20 | `${window.location.host}/test`, { version: 1, state: [1/2, 0.25, 0.10, 0.15] })} 22 | > 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
Column 1Column 2Column 3Column 4
Value 1 1Value 1 2Value 1 3Value 1 4
Value 2 1Value 2 2Value 2 3Value 2 4
47 |
48 | ``` 49 | 50 | ## Contributors 51 | 52 | Alexey Zinchenko 53 | 54 | ## Component Name 55 | 56 | ResizableGrid 57 | 58 | ## License 59 | 60 | Licensed by © 2022 OpusCapita 61 | -------------------------------------------------------------------------------- /src/components/ResizableGrid/ResizableGrid.SCOPE.react.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { showroomScopeDecorator } from '@opuscapita/react-showroom-client'; 4 | import { I18nManager } from '@opuscapita/i18n'; 5 | import localStore from "./store/localStore"; 6 | 7 | // This @showroomScopeDecorator modifies React.Component prototype by adding _renderChildren() method. 8 | export default 9 | @showroomScopeDecorator 10 | class ResizableGridScope extends PureComponent { 11 | static childContextTypes = { 12 | i18n: PropTypes.object 13 | }; 14 | 15 | constructor(...args) { 16 | super(...args); 17 | this.i18n = new I18nManager({ locale: 'en' }); 18 | } 19 | 20 | getChildContext() { 21 | return { i18n: this.i18n } 22 | } 23 | 24 | createLocalStore = (storeIdProvider, defaultValue) => { 25 | const store = localStore(storeIdProvider, defaultValue); 26 | // setInterval(() => { 27 | // store.reset(); 28 | // }, 1000) 29 | return store; 30 | } 31 | 32 | render() { 33 | return ( 34 |
35 | {this._renderChildren()} 36 |
37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/ResizableGrid/ResizableGrid.less: -------------------------------------------------------------------------------- 1 | .resizable-table-wrapper { 2 | overflow: hidden; /* Clips any scrollbars that appear */ 3 | } 4 | 5 | .resizable-table-wrapper { 6 | table { 7 | width: 100%; 8 | overflow: auto; /* Allow scrolling within the table */ 9 | display: grid; 10 | } 11 | 12 | table thead tr th { 13 | position: relative; 14 | } 15 | 16 | table th span, 17 | table td span { 18 | white-space: nowrap; 19 | text-overflow: ellipsis; 20 | overflow: hidden; 21 | } 22 | 23 | table tr td { 24 | border-top: 1px solid #ccc; 25 | } 26 | 27 | table thead, 28 | table tbody, 29 | table tr { 30 | display: contents; 31 | } 32 | } 33 | 34 | .resize-handle { 35 | display: block; 36 | position: absolute; 37 | cursor: col-resize; 38 | width: 7px; 39 | right: 0; 40 | top: 0; 41 | z-index: 1; 42 | border-right: 4px solid transparent; 43 | } 44 | 45 | .resize-handle:hover { 46 | border-color: #ccc; 47 | } 48 | 49 | .resize-handle.active { 50 | border-color: #517ea5; 51 | } 52 | -------------------------------------------------------------------------------- /src/components/ResizableGrid/index.js: -------------------------------------------------------------------------------- 1 | import ResizableGrid from './ResizableGrid.react'; 2 | import localStore from "./store/localStore"; 3 | 4 | export { ResizableGrid, localStore }; 5 | export default ResizableGrid; 6 | -------------------------------------------------------------------------------- /src/components/ResizableGrid/integrationWithCrudEditorStyles.less: -------------------------------------------------------------------------------- 1 | .resizable-table-wrapper { 2 | table thead tr th { 3 | align-self: center; 4 | border-bottom: 1px solid #ddd; 5 | } 6 | 7 | table thead tr th:not(:first-child) { 8 | padding: 15px 5px 15px 5px; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/SearchBulkOperationsPanel/SearchBulkOperationsPanel.less: -------------------------------------------------------------------------------- 1 | .crud---search-bulk-operations-panel { 2 | display: inline-flex; 3 | padding: 6px 0; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/SearchForm/SearchForm.less: -------------------------------------------------------------------------------- 1 | .crud--search-form { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: flex-start; 5 | height: 100%; 6 | } 7 | 8 | .crud--search-form__title { 9 | margin: 0; 10 | line-height: 1; 11 | } 12 | 13 | .crud--search-form__header { 14 | background-color: #3b4a56; 15 | color: #fff; 16 | padding: 12px 12px 8px; 17 | } 18 | 19 | .crud--search-form__controls { 20 | flex: 1 1; 21 | display: flex; 22 | flex-direction: column; 23 | padding: 5px 12px 0 0; 24 | } 25 | 26 | .crud--search-form__submit-group { 27 | padding: 12px; 28 | justify-content: flex-end; 29 | display: flex; 30 | border-top: 1px solid #e5e5e5; 31 | } 32 | 33 | .crud--search-form__form-group { 34 | margin-left: 0 !important; 35 | margin-right: 0 !important; 36 | } 37 | -------------------------------------------------------------------------------- /src/components/SearchMain/SearchMain.less: -------------------------------------------------------------------------------- 1 | .crud--search-main { 2 | display: block; 3 | } 4 | 5 | .crud--search-main__container { 6 | display: flex; 7 | flex: 1; 8 | min-height: 0; 9 | position: relative; 10 | height: 100%; 11 | } 12 | 13 | .crud--search-main__page-header { 14 | display: flex; 15 | justify-content: space-between; 16 | align-items: center; 17 | } 18 | 19 | .crud--search-main__search-container { 20 | width: 0; 21 | min-width: 0; 22 | opacity: 0; 23 | height: 0; 24 | transition: all .3s ease-in-out; 25 | 26 | .crud--search-form { 27 | display: none; 28 | } 29 | } 30 | 31 | .crud--search-main__search-container.form-open { 32 | margin: 0; 33 | border-right: 1px solid #ddd; 34 | height: 100%; 35 | min-width: 290px; 36 | opacity: 1; 37 | transition: all .3s ease-in-out; 38 | 39 | .crud--search-form { 40 | display: block; 41 | } 42 | } 43 | 44 | .crud--search-main__results-container { 45 | width: 100%; 46 | transition: width .3s ease-in-out; 47 | } 48 | 49 | .crud--search-main__results-container.form-open { 50 | position: relative; 51 | transition: width .3s ease-in-out; 52 | } 53 | -------------------------------------------------------------------------------- /src/components/SearchPaginationPanel/SearchPaginationPanel.less: -------------------------------------------------------------------------------- 1 | .crud--search-pagination-panel { 2 | display: inline-flex; 3 | flex-wrap: wrap; 4 | align-items: center; 5 | flex-direction: row-reverse; 6 | padding-right: 12px; 7 | margin-left: auto; 8 | } 9 | 10 | .crud--search-pagination-panel__per-page-dropdown { 11 | margin: 6px 0 6px 12px; 12 | } 13 | 14 | 15 | .crud--search-pagination-panel__per-page-dropdown .dropdown-menu { 16 | left: 0; 17 | right: 0; 18 | padding: 0; 19 | } 20 | 21 | .crud--search-pagination-panel__per-page-dropdown .dropdown-menu > li > a { 22 | text-align: right; 23 | } 24 | 25 | .crud--search-pagination-panel__paginate { 26 | display: flex; 27 | flex-wrap: wrap; 28 | align-items: center; 29 | } 30 | 31 | .crud--search-pagination-panel__pagination { 32 | margin: 0 0 0 12px !important; 33 | } 34 | -------------------------------------------------------------------------------- /src/components/SearchResult/SearchResult.less: -------------------------------------------------------------------------------- 1 | .crud--search-result { 2 | height: 100%; 3 | display: block; 4 | } 5 | 6 | .crud--search-result__table { 7 | min-height: 0; 8 | position: relative; 9 | display: flex; 10 | flex: 1; 11 | } 12 | 13 | .crud--search-result__footer { 14 | display: flex; 15 | flex-wrap: wrap; 16 | align-items: center; 17 | justify-content: space-between; 18 | margin-top: auto; 19 | padding: 6px 12px; 20 | border-top: 1px solid #e5e5e5; 21 | } 22 | 23 | .crud--search-result__no-items-found { 24 | padding: 12px; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/SearchResult/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ResultListing from '../SearchResultListing'; 4 | import BulkOperationsPanel from '../SearchBulkOperationsPanel'; 5 | import PaginationPanel from '../SearchPaginationPanel'; 6 | import WithSpinner from '../Spinner/SpinnerOverlayHOC'; 7 | import './SearchResult.less'; 8 | 9 | class SearchResult extends PureComponent { 10 | static propTypes = { 11 | model: PropTypes.shape({ 12 | data: PropTypes.shape({ 13 | totalCount: PropTypes.number 14 | }).isRequired 15 | }).isRequired 16 | } 17 | 18 | static contextTypes = { 19 | i18n: PropTypes.object 20 | }; 21 | 22 | render() { 23 | const { model } = this.props; 24 | const { i18n } = this.context; 25 | 26 | return model.data.totalCount > 0 ? ( 27 |
28 |
29 | 30 |
31 |
32 | 33 | 34 |
35 |
36 | ) : ( 37 |
38 | {i18n.getMessage('common.CrudEditor.found.items.message', { count: 0 })} 39 |
40 | ); 41 | } 42 | } 43 | 44 | export default WithSpinner(SearchResult); 45 | -------------------------------------------------------------------------------- /src/components/SearchResultListing/SearchResultButtons.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ButtonGroup from 'react-bootstrap/lib/ButtonGroup'; 4 | import OperationsBar from '../OperationsBar'; 5 | 6 | export default class SearchResultButtons extends PureComponent { 7 | static propTypes = { 8 | parentRef: PropTypes.object, 9 | operations: PropTypes.any 10 | } 11 | 12 | state = { 13 | previousSource: null 14 | } 15 | 16 | // handleToggleDropdown is a workaround for weird CSS overflow behavior 17 | // details: https://stackoverflow.com/a/6433475 18 | handleToggleDropdown = (dropdownOpened, event, { source }) => { 19 | const { parentRef } = this.props; 20 | 21 | const parentWidth = parentRef.current.clientWidth; 22 | const tableWidth = parentRef.current.firstChild.scrollWidth 23 | 24 | // table is wider than visible div -> show scroll 25 | if (parentWidth < tableWidth) { 26 | parentRef.current.style.overflow = 'auto'; 27 | return; 28 | } 29 | 30 | // handle multiple dropdowns closing each other 31 | // don't rewrite styles if one DD is closed by opening another DD 32 | if (this.state.previousSource === 'click' && source === 'rootClose') { 33 | return; 34 | } 35 | 36 | parentRef.current.style.overflow = dropdownOpened ? 'visible' : 'auto'; 37 | this.setState({ previousSource: source }); 38 | } 39 | 40 | render() { 41 | const { operations } = this.props; 42 | 43 | return ( 44 | 45 | { 46 | buttons => buttons.length ? ( 47 | 48 | {buttons} 49 | 50 | ) : 51 | null 52 | } 53 | 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/SearchResultListing/styles.less: -------------------------------------------------------------------------------- 1 | .crud--search-result-listing { 2 | position: relative; 3 | flex: 1; 4 | height: 100%; 5 | max-width: 100%; 6 | } 7 | 8 | .crud--search-result-listing__table-container { 9 | position: relative; 10 | flex: 1; 11 | height: 100%; 12 | overflow: auto; 13 | padding: 0 12px; 14 | } 15 | 16 | .crud--search-result-listing__table-container--with-spinner { 17 | overflow: hidden; 18 | } 19 | 20 | .crud--search-result-listing__table tbody { 21 | vertical-align: top; 22 | } 23 | 24 | .crud--search-result-listing__table td .checkbox { 25 | margin-top: auto; 26 | } 27 | 28 | .crud--search-result-listing__table td, 29 | .crud--search-result-listing__table th { 30 | vertical-align: inherit !important; 31 | } 32 | 33 | .crud--search-result-listing__sort-icon { 34 | font-size: 80%; 35 | margin-left: 1ch; 36 | } 37 | 38 | .crud--search-result-listing__sort-button { 39 | padding-left: 0 !important; 40 | padding-right: 0 !important; 41 | } 42 | 43 | .crud--search-result-listing__action-buttons { 44 | display: flex !important; 45 | justify-content: flex-end; 46 | } 47 | 48 | .dropdown.btn-group { 49 | display: flex !important; 50 | } 51 | 52 | ul.dropdown-menu { 53 | text-align: left; 54 | left: auto; 55 | right: 0; 56 | } 57 | 58 | .crud--search-custom-bulk-operations-dropdown ul.dropdown-menu { 59 | text-align: inherit; 60 | left: inherit; 61 | right: inherit; 62 | } 63 | -------------------------------------------------------------------------------- /src/components/ShowMain/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import Heading from '../EditHeading'; 4 | import Tab from '../EditTab'; 5 | import WithSpinner from '../Spinner/SpinnerOverlayHOC'; 6 | import { VIEW_NAME } from '../../crudeditor-lib/views/show/constants'; 7 | 8 | const ShowMain = (props) => { 9 | const { model } = props; 10 | const ActiveTabComponent = model.data.activeTab && model.data.activeTab.component; 11 | 12 | return (
13 | 14 | {ActiveTabComponent ? 15 | : 16 | 17 | } 18 |
); 19 | }; 20 | 21 | ShowMain.propTypes = { 22 | model: PropTypes.shape({ 23 | data: PropTypes.shape({ 24 | activeTab: PropTypes.array, 25 | viewName: PropTypes.oneOf([VIEW_NAME]).isRequired, 26 | persistentInstance: PropTypes.object 27 | }).isRequired 28 | }) 29 | } 30 | 31 | export default WithSpinner(ShowMain); 32 | -------------------------------------------------------------------------------- /src/components/Spinner/SpinnerOverlay.less: -------------------------------------------------------------------------------- 1 | .crud--spinner-overlay { 2 | position: absolute; 3 | top: 0; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | width: 100%; 8 | height: 100%; 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | background: rgba(255, 255, 255, 0.78); 13 | z-index: 999; 14 | } 15 | 16 | .ready-for-spinner { 17 | position: relative; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/Spinner/SpinnerOverlayHOC.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { SVG as Svg } from '@opuscapita/react-svg'; 4 | import spinnerSVG from './spinner2.svg'; 5 | import './SpinnerOverlay.less'; 6 | 7 | const withSpinner = WrappedComponent => { 8 | return class WithSpinner extends PureComponent { 9 | static propTypes = { 10 | model: PropTypes.shape({ 11 | data: PropTypes.shape({ 12 | spinner: PropTypes.func, 13 | isLoading: PropTypes.bool 14 | }).isRequired 15 | }).isRequired 16 | } 17 | 18 | render() { 19 | const { children, model, ...props } = this.props; 20 | 21 | const CustomSpinner = model.data.spinner; 22 | 23 | const defaultSpinner = (); 24 | 25 | const Spinner = model.data.isLoading ? 26 | ( 27 |
28 | { CustomSpinner ? : defaultSpinner } 29 |
30 | ) : 31 | null; 32 | 33 | return ( 34 |
35 | {Spinner} 36 | 41 | {children} 42 | 43 |
44 | ); 45 | } 46 | } 47 | } 48 | 49 | export default withSpinner; 50 | -------------------------------------------------------------------------------- /src/components/Spinner/spinner2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/crudeditor-lib/check-model/formLayout.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { uiTypes } from './lib'; 3 | 4 | const fieldPropTypes = PropTypes.shape({ 5 | field: PropTypes.string.isRequired, 6 | readOnly: PropTypes.bool.isRequired, 7 | render: PropTypes.shape({ 8 | component: PropTypes.func.isRequired, 9 | props: PropTypes.object, 10 | value: PropTypes.shape({ 11 | converter: PropTypes.shape({ 12 | format: PropTypes.func, 13 | parse: PropTypes.func 14 | }), 15 | propName: PropTypes.string, 16 | type: PropTypes.oneOf(uiTypes) 17 | }) 18 | }) 19 | }) 20 | 21 | const formLayoutPropTypes = { 22 | formLayout: PropTypes.arrayOf(PropTypes.arrayOf( 23 | PropTypes.oneOfType([ 24 | fieldPropTypes, 25 | PropTypes.arrayOf( 26 | PropTypes.oneOfType([ 27 | fieldPropTypes, 28 | PropTypes.array 29 | ]) 30 | ) 31 | ]) 32 | )) 33 | } 34 | 35 | export default /* istanbul ignore next */ formLayout => PropTypes.checkPropTypes( 36 | formLayoutPropTypes, 37 | { formLayout }, 38 | 'property', 39 | 'React-CrudEditor Form Layout' 40 | ) 41 | -------------------------------------------------------------------------------- /src/crudeditor-lib/check-model/index.js: -------------------------------------------------------------------------------- 1 | export checkModelDefinition from './modelDefinition.js'; 2 | export checkSearchUi from './searchUi'; 3 | export checkFormLayout from './formLayout'; 4 | -------------------------------------------------------------------------------- /src/crudeditor-lib/check-model/lib.js: -------------------------------------------------------------------------------- 1 | import * as dataTypes from '../../data-types-lib/constants'; 2 | import { isAllowed } from '../lib'; 3 | 4 | import { 5 | PERMISSION_CREATE, 6 | PERMISSION_DELETE, 7 | PERMISSION_EDIT, 8 | PERMISSION_VIEW 9 | } from '../common/constants'; 10 | 11 | export const allowedSome = /* istanbul ignore next */ ( 12 | actions = [], 13 | { 14 | permissions: { 15 | crudOperations = {} 16 | } = {} 17 | } 18 | ) => [ 19 | PERMISSION_CREATE, 20 | PERMISSION_EDIT, 21 | PERMISSION_DELETE, 22 | PERMISSION_VIEW 23 | ].filter(action => actions.indexOf(action) > -1). 24 | some(action => isAllowed(crudOperations, action)); 25 | 26 | // https://stackoverflow.com/a/31169012 27 | export const allPropTypes = /* istanbul ignore next */ (...types) => (...args) => { 28 | const errors = types.map(type => type(...args)).filter(Boolean); 29 | if (errors.length === 0) { 30 | return 31 | } 32 | // eslint-disable-next-line consistent-return 33 | return new Error(errors.map(e => e.message).join('\n')); 34 | }; 35 | 36 | export const uiTypes = Object.keys(dataTypes). 37 | filter(key => key.indexOf('UI_TYPE') === 0). 38 | reduce((types, key) => types.concat(dataTypes[key]), []); 39 | -------------------------------------------------------------------------------- /src/crudeditor-lib/common/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | INSTANCES_CUSTOM, 3 | INSTANCES_DELETE, 4 | VIEW_HARD_REDIRECT, 5 | VIEW_SOFT_REDIRECT 6 | } from './constants'; 7 | 8 | export const 9 | hardRedirectView = /* istanbul ignore next */ ({ 10 | viewName, 11 | viewState 12 | }) => ({ 13 | type: VIEW_HARD_REDIRECT, 14 | payload: { 15 | viewName, 16 | viewState 17 | }, 18 | meta: { 19 | source: 'owner' 20 | } 21 | }), 22 | 23 | softRedirectView = /* istanbul ignore next */ ({ 24 | name, 25 | state = {}, 26 | ...additionalArgs 27 | }) => ({ 28 | type: VIEW_SOFT_REDIRECT, 29 | payload: { 30 | view: { 31 | name, 32 | state 33 | }, 34 | ...additionalArgs 35 | } 36 | }), 37 | 38 | deleteInstances = /* istanbul ignore next */ instances => ({ 39 | type: INSTANCES_DELETE, 40 | payload: { 41 | instances: Array.isArray(instances) ? instances : [instances] 42 | } 43 | }), 44 | 45 | customBulkOperation = ({ instances, handler }) => ({ 46 | type: INSTANCES_CUSTOM, 47 | payload: { 48 | instances: Array.isArray(instances) ? instances : [instances], 49 | handler, 50 | } 51 | }); 52 | -------------------------------------------------------------------------------- /src/crudeditor-lib/common/reducer.js: -------------------------------------------------------------------------------- 1 | import cloneDeep from 'lodash/cloneDeep'; 2 | import u from 'updeep'; 3 | 4 | import { ACTIVE_VIEW_CHANGE } from './constants'; 5 | 6 | const defaultStoreStateTemplate = { 7 | activeViewName: null, // XXX: must be null until initialization completes. 8 | }; 9 | 10 | /* 11 | * XXX: 12 | * Only objects and arrays are allowed at branch nodes. 13 | * Only primitive data types are allowed at leaf nodes. 14 | */ 15 | export default /* istanbul ignore next */ (modelMetaData, i18n) => ( 16 | storeState = cloneDeep(defaultStoreStateTemplate), 17 | { type, payload, error, meta } 18 | ) => { 19 | const newStoreStateSlice = {}; 20 | 21 | if (type === ACTIVE_VIEW_CHANGE) { 22 | newStoreStateSlice.activeViewName = payload.viewName; 23 | } 24 | 25 | return u(newStoreStateSlice, storeState); // returned object is frozen for NODE_ENV === 'development' 26 | }; 27 | -------------------------------------------------------------------------------- /src/crudeditor-lib/common/workerSagas/save.js: -------------------------------------------------------------------------------- 1 | import { put, call, select } from 'redux-saga/effects'; 2 | 3 | import { 4 | VIEW_NAME as CREATE_VIEW, 5 | INSTANCE_SAVE_REQUEST as CREATE_INSTANCE_SAVE_REQUEST, 6 | INSTANCE_SAVE_FAIL as CREATE_INSTANCE_SAVE_FAIL, 7 | INSTANCE_SAVE_SUCCESS as CREATE_INSTANCE_SAVE_SUCCESS 8 | } from '../../views/create/constants'; 9 | 10 | import { 11 | VIEW_NAME as EDIT_VIEW, 12 | INSTANCE_SAVE_REQUEST as EDIT_INSTANCE_SAVE_REQUEST, 13 | INSTANCE_SAVE_FAIL as EDIT_INSTANCE_SAVE_FAIL, 14 | INSTANCE_SAVE_SUCCESS as EDIT_INSTANCE_SAVE_SUCCESS 15 | } from '../../views/edit/constants'; 16 | 17 | const INSTANCE_SAVE_REQUEST = { 18 | [CREATE_VIEW]: CREATE_INSTANCE_SAVE_REQUEST, 19 | [EDIT_VIEW]: EDIT_INSTANCE_SAVE_REQUEST 20 | } 21 | 22 | const INSTANCE_SAVE_FAIL = { 23 | [CREATE_VIEW]: CREATE_INSTANCE_SAVE_FAIL, 24 | [EDIT_VIEW]: EDIT_INSTANCE_SAVE_FAIL 25 | } 26 | 27 | const INSTANCE_SAVE_SUCCESS = { 28 | [CREATE_VIEW]: CREATE_INSTANCE_SAVE_SUCCESS, 29 | [EDIT_VIEW]: EDIT_INSTANCE_SAVE_SUCCESS 30 | } 31 | 32 | const viewSaveApi = { 33 | [CREATE_VIEW]: 'create', 34 | [EDIT_VIEW]: 'update' 35 | } 36 | 37 | export default function* saveSaga({ modelDefinition, meta }) { 38 | const viewName = meta.spawner; 39 | 40 | yield put({ 41 | type: INSTANCE_SAVE_REQUEST[viewName], 42 | meta 43 | }); 44 | 45 | let instance; 46 | 47 | try { 48 | instance = yield call(modelDefinition.api[viewSaveApi[viewName]], { 49 | instance: yield select(storeState => storeState.views[viewName].formInstance) 50 | }); 51 | } catch (err) { 52 | yield put({ 53 | type: INSTANCE_SAVE_FAIL[viewName], 54 | payload: err, 55 | error: true, 56 | meta 57 | }); 58 | 59 | throw err; 60 | } 61 | 62 | yield put({ 63 | type: INSTANCE_SAVE_SUCCESS[viewName], 64 | payload: { instance }, 65 | meta 66 | }); 67 | 68 | return instance 69 | } 70 | -------------------------------------------------------------------------------- /src/crudeditor-lib/components/Main/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import ViewSwitcher from '../ViewSwitcher'; 5 | import { hardRedirectView } from '../../common/actions'; 6 | 7 | class CrudMain extends PureComponent { 8 | static propTypes = { 9 | viewName: PropTypes.string, 10 | viewState: PropTypes.object, 11 | modelDefinition: PropTypes.object.isRequired, 12 | hardRedirectView: PropTypes.func.isRequired, 13 | externalOperations: PropTypes.func.isRequired, 14 | customBulkOperations: PropTypes.arrayOf(PropTypes.object).isRequired, 15 | uiConfig: PropTypes.object.isRequired 16 | } 17 | 18 | constructor(...args) { 19 | super(...args); 20 | 21 | // Initial initialization (viewState structure is unknown and depends on viewName value): 22 | this.props.hardRedirectView({ 23 | viewName: this.props.viewName, 24 | viewState: this.props.viewState 25 | }); 26 | } 27 | 28 | componentWillReceiveProps({ viewName, viewState }) { 29 | if (viewName !== this.props.viewName || viewState !== this.props.viewState) { 30 | // Re-initialization (viewState structure is unknown and depends on viewName value): 31 | this.props.hardRedirectView({ viewName, viewState }); 32 | } 33 | } 34 | 35 | render = _ => ( 36 | 42 | ) 43 | } 44 | 45 | export default connect( 46 | undefined, 47 | { hardRedirectView }, 48 | undefined, 49 | { areOwnPropsEqual: _ => false } 50 | )(CrudMain); 51 | -------------------------------------------------------------------------------- /src/crudeditor-lib/components/ViewSwitcher/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | import SearchView from '../../views/search/container'; 6 | import CreateView from '../../views/create/container'; 7 | import EditView from '../../views/edit/container'; 8 | import ShowView from '../../views/show/container'; 9 | import ErrorView from '../../views/error/container'; 10 | 11 | import { 12 | VIEW_SEARCH, 13 | VIEW_CREATE, 14 | VIEW_EDIT, 15 | VIEW_SHOW, 16 | VIEW_ERROR 17 | } from '../../common/constants'; 18 | 19 | import WithAlerts from '../WithAlertsHOC'; 20 | 21 | const ViewSwitcher = ({ 22 | activeViewName, modelDefinition, externalOperations, customBulkOperations, uiConfig 23 | }, { i18n }) => { 24 | if (!activeViewName) { 25 | return null; 26 | } 27 | 28 | const ViewContainer = ({ 29 | [VIEW_SEARCH]: SearchView, 30 | [VIEW_CREATE]: CreateView, 31 | [VIEW_EDIT]: EditView, 32 | [VIEW_SHOW]: ShowView, 33 | [VIEW_ERROR]: ErrorView 34 | })[activeViewName]; 35 | 36 | return ( 37 |
38 | { 39 | ViewContainer ? 40 | : 47 |
Unknown view {activeViewName}
48 | } 49 |
50 | ); 51 | } 52 | 53 | ViewSwitcher.propTypes = { 54 | activeViewName: PropTypes.string, 55 | modelDefinition: PropTypes.object.isRequired, 56 | externalOperations: PropTypes.func.isRequired, 57 | customBulkOperations: PropTypes.arrayOf(PropTypes.object).isRequired, 58 | uiConfig: PropTypes.object.isRequired 59 | }; 60 | 61 | ViewSwitcher.contextTypes = { 62 | i18n: PropTypes.object 63 | }; 64 | 65 | export default connect( 66 | storeState => ({ activeViewName: storeState.common.activeViewName }) 67 | )(WithAlerts(ViewSwitcher)); 68 | -------------------------------------------------------------------------------- /src/crudeditor-lib/components/WithAlertsHOC/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { 3 | NotificationContainer, 4 | NotificationManager 5 | } from 'react-notifications'; 6 | import 'react-notifications/lib/notifications.css'; 7 | import './styles.css'; 8 | 9 | import { 10 | NOTIFICATION_SUCCESS, 11 | NOTIFICATION_ERROR, 12 | NOTIFICATION_VALIDATION_ERROR 13 | } from '../../middleware/notifications'; 14 | 15 | const withAlerts = WrappedComponent => { 16 | return class WithAlerts extends PureComponent { 17 | componentWillUnmount() { 18 | [ 19 | NOTIFICATION_SUCCESS, 20 | NOTIFICATION_ERROR, 21 | NOTIFICATION_VALIDATION_ERROR 22 | ].forEach(id => NotificationManager.remove({ id })); 23 | } 24 | 25 | render() { 26 | const { children, ...props } = this.props; 27 | 28 | return ( 29 |
30 | 31 | {children} 32 | 33 | 34 |
35 | ); 36 | } 37 | } 38 | } 39 | 40 | export default withAlerts; 41 | -------------------------------------------------------------------------------- /src/crudeditor-lib/components/WithAlertsHOC/styles.css: -------------------------------------------------------------------------------- 1 | /*notification styles*/ 2 | .notification-error { 3 | color: #333; 4 | background-color: #fdf7f7; 5 | border-left: 3px solid #eed3d7; 6 | box-shadow: none; 7 | } 8 | 9 | .notification-error:before { 10 | content: ""; 11 | } 12 | 13 | .notification-success { 14 | background-color: #f4f8fa; 15 | color: black; 16 | border-left: 3px solid #bce8f1; 17 | box-shadow: none; 18 | } 19 | 20 | .notification-success:before { 21 | content: ""; 22 | } 23 | 24 | .notification { 25 | padding: 15px 0 15px 15px; 26 | } 27 | 28 | .message-error { 29 | color: red; 30 | } -------------------------------------------------------------------------------- /src/crudeditor-lib/i18n/exceptions/da.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "common.CrudEditor.default.doesnt.match.message": "Værdien svarer ikke til det krævede mønster ''{pattern}''", 3 | "common.CrudEditor.default.invalid.email.message": "Ikke et gyldigt e-mailadresseformat", 4 | "common.CrudEditor.default.invalid.max.message": "Værdien overskrider den maksimale værdi ''{max}''", 5 | "common.CrudEditor.default.invalid.min.message": "Værdien er mindre end minimumsværdien ''{min}''", 6 | "common.CrudEditor.default.invalid.max.size.message": "Værdien overskrider den maksimale størrelse på ''{max}''", 7 | "common.CrudEditor.default.invalid.min.size.message": "Værdien er mindre end minimumsstørrelsen på ''{min}''", 8 | "common.CrudEditor.default.invalid.validator.message": 9 | "Værdien kunne ikke gennemføre den brugerdefinerede validering", 10 | "common.CrudEditor.default.blank.message": "Feltet må ikke være tomt", 11 | "common.CrudEditor.default.null.message": "Egenskaben må ikke være nul", 12 | "common.CrudEditor.default.not.unique.message": "Værdien skal være entydig", 13 | "common.CrudEditor.default.invalid.url.message": "Værdien skal være en gyldig URL-adresse", 14 | "common.CrudEditor.default.invalid.date.message": "Værdien skal være en gyldig dato", 15 | "common.CrudEditor.default.invalid.decimal.message": "Værdien skal være et gyldigt tal", 16 | "common.CrudEditor.default.invalid.integer.message": "Værdien skal være et gyldigt tal", 17 | "common.CrudEditor.default.errorOccurred.message": "Der opstod en fejl" 18 | } 19 | -------------------------------------------------------------------------------- /src/crudeditor-lib/i18n/exceptions/de.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "common.CrudEditor.default.doesnt.match.message": "Der Wert entspricht nicht dem vorgegebenen Muster ''{pattern}''", 3 | "common.CrudEditor.default.invalid.email.message": "Dies ist keine gültige E-Mail Adresse", 4 | "common.CrudEditor.default.invalid.max.message": "Der Wert ist größer als der Höchstwert von ''{max}''", 5 | "common.CrudEditor.default.invalid.min.message": "Der Wert ist kleiner als der Mindestwert von ''{min}''", 6 | "common.CrudEditor.default.invalid.max.size.message": "Der Wert übersteigt den Höchstwert von ''{max}''", 7 | "common.CrudEditor.default.invalid.min.size.message": "Der Wert unterschreitet den Mindestwert von ''{min}''", 8 | "common.CrudEditor.default.invalid.validator.message": "Der Wert ist ungültig", 9 | "common.CrudEditor.default.blank.message": "Das Feld darf nicht leer sein", 10 | "common.CrudEditor.default.null.message": "Die Eigenschaft darf nicht null sein", 11 | "common.CrudEditor.default.not.unique.message": "Der Wert darf nur einmal vorkommen", 12 | "common.CrudEditor.default.invalid.url.message": "Die Wert muss eine gültige URL sein", 13 | "common.CrudEditor.default.invalid.date.message": "Die Wert muss ein gültiges Datum sein", 14 | 15 | "common.CrudEditor.default.invalid.integer.message": "Die Wert muss eine gültige Zahl sein", 16 | 17 | "common.CrudEditor.default.errorOccurred.message": "Ein Fehler ist aufgetreten" 18 | } 19 | -------------------------------------------------------------------------------- /src/crudeditor-lib/i18n/exceptions/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "common.CrudEditor.default.doesnt.match.message": "The value does not match the required pattern ''{pattern}''", 3 | "common.CrudEditor.default.invalid.email.message": "Not a valid e-mail address format", 4 | "common.CrudEditor.default.invalid.max.message": "The value exceeds the maximum value ''{max}''", 5 | "common.CrudEditor.default.invalid.min.message": "The value is less than the minimum value ''{min}''", 6 | "common.CrudEditor.default.invalid.max.size.message": "The value exceeds the maximum size of ''{max}''", 7 | "common.CrudEditor.default.invalid.min.size.message": "The value is less than the minimum size of ''{min}''", 8 | "common.CrudEditor.default.invalid.validator.message": "The value does not pass custom validation", 9 | "common.CrudEditor.default.blank.message": "The field cannot be blank", 10 | "common.CrudEditor.default.null.message": "The property cannot be null", 11 | "common.CrudEditor.default.not.unique.message": "The value must be unique", 12 | "common.CrudEditor.default.invalid.url.message": "The value must be a valid URL", 13 | "common.CrudEditor.default.invalid.date.message": "The value must be a valid Date", 14 | 15 | "common.CrudEditor.default.invalid.decimal.message": "The value must be a valid number", 16 | "common.CrudEditor.default.invalid.integer.message": "The value must be a valid number", 17 | 18 | "common.CrudEditor.default.errorOccurred.message": "Error occurred" 19 | } 20 | -------------------------------------------------------------------------------- /src/crudeditor-lib/i18n/exceptions/fi.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "common.CrudEditor.default.doesnt.match.message": "Arvo ei vastaa vaadittua kuviota {pattern}", 3 | "common.CrudEditor.default.invalid.email.message": "Sähköpostiosoite on väärän muotoinen", 4 | "common.CrudEditor.default.invalid.max.message": "Arvo ylittää enimmäisarvon {max}", 5 | "common.CrudEditor.default.invalid.min.message": "Arvo alittaa vähimmäisarvon {min}", 6 | "common.CrudEditor.default.invalid.max.size.message": "Arvo ylittää enimmäiskoon {max}", 7 | "common.CrudEditor.default.invalid.min.size.message": "Arvo alittaa vähimmäiskoon {min}", 8 | "common.CrudEditor.default.invalid.validator.message": "Arvo ei läpäise mukautettua tarkistusta", 9 | "common.CrudEditor.default.blank.message": "Kenttä ei voi olla tyhjä", 10 | "common.CrudEditor.default.null.message": "Ominaisuus ei voi olla tyhjä", 11 | "common.CrudEditor.default.not.unique.message": "Arvon on oltava yksilöivä", 12 | "common.CrudEditor.default.invalid.url.message": "URL on väärän muotoinen", 13 | "common.CrudEditor.default.invalid.date.message": "Päivämäärä on väärän muotoinen", 14 | 15 | "common.CrudEditor.default.invalid.decimal.message": "Numero on väärän muotoinen", 16 | "common.CrudEditor.default.invalid.integer.message": "Numero on väärän muotoinen", 17 | 18 | "common.CrudEditor.default.errorOccurred.message": "Virhetila" 19 | } 20 | -------------------------------------------------------------------------------- /src/crudeditor-lib/i18n/exceptions/no.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "common.CrudEditor.default.doesnt.match.message": "Verdien stemmer ikke med det påkrevde mønsteret ''{pattern}''", 3 | "common.CrudEditor.default.invalid.email.message": "Formatet til e-postadressen er ugyldig", 4 | "common.CrudEditor.default.invalid.max.message": "Verdien overskrider maksimumsverdien ''{max}''", 5 | "common.CrudEditor.default.invalid.min.message": "Verdien er under minimumsverdien ''{min}''", 6 | "common.CrudEditor.default.invalid.max.size.message": "Verdien overskrider maksimumsstørrelsen ''{max}''", 7 | "common.CrudEditor.default.invalid.min.size.message": "Verdien er under minimumsstørrelsen ''{min}''", 8 | "common.CrudEditor.default.invalid.validator.message": "Verdien godkjennes ikke av tilpasset validering", 9 | "common.CrudEditor.default.blank.message": "Feltet kan ikke være tomt", 10 | "common.CrudEditor.default.null.message": "Egenskap kan ikke være null", 11 | "common.CrudEditor.default.not.unique.message": "Verdien må være unik", 12 | "common.CrudEditor.default.invalid.url.message": "Verdien må være en gyldig URL", 13 | "common.CrudEditor.default.invalid.date.message": "Verdien må være en gyldig dato", 14 | 15 | "common.CrudEditor.default.invalid.integer.message": "Verdien må være et gyldig tall", 16 | 17 | "common.CrudEditor.default.errorOccurred.message": "Det oppsto en feil" 18 | } 19 | -------------------------------------------------------------------------------- /src/crudeditor-lib/i18n/exceptions/ru.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "common.CrudEditor.default.blank.message": "Данное поле не может быть пустым", 3 | "common.CrudEditor.default.doesnt.match.message": "Значение не соответствует требуемому шаблону ''{pattern}''.", 4 | "common.CrudEditor.default.invalid.email.message": "Недействительный формат email.", 5 | "common.CrudEditor.default.invalid.max.message": "Значение превышает максимальное (''{max}'').", 6 | "common.CrudEditor.default.invalid.max.size.message": "Значение превышает максимальный размер (''{max}'').", 7 | "common.CrudEditor.default.invalid.min.message": "Значение меньше минимально допустимого (''{min}'').", 8 | "common.CrudEditor.default.invalid.min.size.message": "Значение меньше минимального допустимого размера (''{min}'').", 9 | "common.CrudEditor.default.invalid.validator.message": "Значение не проходит выборочную валидацию.", 10 | "common.CrudEditor.default.not.unique.message": "Значение должно быть уникальным.", 11 | "common.CrudEditor.default.null.message": "Свойство не может быть нулевым", 12 | 13 | "common.CrudEditor.default.invalid.integer.message": "Значение должно быть действительным числом.", 14 | "common.CrudEditor.default.invalid.url.message": "Значение должно быть действительным URL.", 15 | "common.CrudEditor.default.invalid.date.message": "Значение должно быть действительной датой.", 16 | 17 | "common.CrudEditor.default.errorOccurred.message": "Произошла ошибка" 18 | } 19 | -------------------------------------------------------------------------------- /src/crudeditor-lib/i18n/exceptions/sv.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "common.CrudEditor.default.doesnt.match.message": "Värdet matchar inte det obligatoriska mönstret \"{pattern}\"", 3 | "common.CrudEditor.default.invalid.email.message": "Inte ett giltigt e-postadressformat", 4 | "common.CrudEditor.default.invalid.max.message": "Värdet överskrider högsta värdet \"{max}\"", 5 | "common.CrudEditor.default.invalid.min.message": "Värdet underskrider lägsta värdet \"{min}\"", 6 | "common.CrudEditor.default.invalid.max.size.message": "Värdet överskrider den största storleken på \"{max}\"", 7 | "common.CrudEditor.default.invalid.min.size.message": "Värdet underskrider den minsta storleken på \"{min}\"", 8 | "common.CrudEditor.default.invalid.validator.message": "Värdet godkänns inte vid anpassad validering", 9 | "common.CrudEditor.default.blank.message": "Fältet får inte vara tomt", 10 | "common.CrudEditor.default.null.message": "Egenskapen kan inte vara null", 11 | "common.CrudEditor.default.not.unique.message": "Värdet måste vara unikt", 12 | "common.CrudEditor.default.invalid.url.message": "Värdet måste vara en giltig URL", 13 | "common.CrudEditor.default.invalid.date.message": "Värdet måste vara ett giltigt datum", 14 | 15 | "common.CrudEditor.default.invalid.integer.message": "Värdet måste vara ett giltigt tal", 16 | 17 | "common.CrudEditor.default.errorOccurred.message": "Fel har uppst\u00e5tt" 18 | } 19 | -------------------------------------------------------------------------------- /src/crudeditor-lib/i18n/index.js: -------------------------------------------------------------------------------- 1 | import en from './en'; 2 | import de from './de'; 3 | import fi from './fi'; 4 | import no from './no'; 5 | import ru from './ru'; 6 | import sv from './sv'; 7 | import da from './da'; 8 | 9 | /* eslint-disable max-len */ 10 | export default { 11 | en, 12 | de, 13 | fi, 14 | no, 15 | ru, 16 | sv, 17 | da 18 | } 19 | /* eslint-enable max-len */ 20 | -------------------------------------------------------------------------------- /src/crudeditor-lib/lib.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import sinon from 'sinon'; 3 | import { isAllowed } from './lib'; 4 | 5 | describe('crudeditor-lib / lib.js', () => { 6 | describe('isAllowed', () => { 7 | const f = sinon.spy(); 8 | 9 | const permissions = { 10 | create: true, 11 | edit: ({ instance } = {}) => { 12 | if (instance) { 13 | f(instance); 14 | return instance.editable 15 | } 16 | return true 17 | }, 18 | delete: false 19 | } 20 | 21 | it('returns boolean if boolean is defined', () => { 22 | expect(isAllowed(permissions, 'create')).to.equal(true); 23 | expect(isAllowed(permissions, 'delete')).to.equal(false); 24 | }); 25 | 26 | it('executes a function for global permissions', () => { 27 | expect(isAllowed(permissions, 'edit')).to.equal(true); 28 | expect(f.called).to.equal(false); 29 | }); 30 | 31 | it('executes a function for instance permissions', () => { 32 | const instance = { 33 | editable: false 34 | } 35 | expect(isAllowed(permissions, 'edit', { instance })).to.equal(instance.editable); 36 | expect(f.calledOnce).to.be.true; // eslint-disable-line no-unused-expressions 37 | expect(f.calledWith(instance)).to.be.true; // eslint-disable-line no-unused-expressions 38 | }); 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /src/crudeditor-lib/middleware/appStateChangeDetect.js: -------------------------------------------------------------------------------- 1 | import isEqual from 'lodash/isEqual'; 2 | import { STATUS_READY } from '../common/constants'; 3 | import { storeState2appState } from '../lib'; 4 | 5 | // appStateChangeDetect is a function which returns Redux middleware 6 | export default /* istanbul ignore next */ ({ 7 | lastState, 8 | onTransition, 9 | modelDefinition 10 | }) => ({ getState }) => next => action => { 11 | const rez = next(action); 12 | const storeState = getState(); 13 | const activeViewName = storeState.common.activeViewName; 14 | 15 | if (!activeViewName || storeState.views[activeViewName].status !== STATUS_READY) { 16 | return rez; 17 | } 18 | 19 | if (action.meta && action.meta.source === 'owner' || !onTransition) { 20 | lastState = { // eslint-disable-line no-param-reassign 21 | store: storeState 22 | }; 23 | 24 | return rez; 25 | } 26 | 27 | // XXX: updeep must be used in reducers for below store states comparison to work as expected. 28 | if (storeState === lastState.store) { 29 | return rez; 30 | } 31 | 32 | if (lastState.store && !lastState.app) { 33 | lastState.app = storeState2appState(lastState.store, modelDefinition); // eslint-disable-line no-param-reassign 34 | } 35 | 36 | const appState = storeState2appState(storeState, modelDefinition); 37 | 38 | if (!isEqual(appState, lastState.app)) { 39 | onTransition(appState); 40 | } 41 | 42 | lastState = { // eslint-disable-line no-param-reassign 43 | store: storeState, 44 | app: appState 45 | }; 46 | 47 | return rez; 48 | } 49 | -------------------------------------------------------------------------------- /src/crudeditor-lib/middleware/notifications/ExpandableNotice.less: -------------------------------------------------------------------------------- 1 | .react-crudeditor--clipboard-icon { 2 | margin-left: 0.6em; 3 | opacity: 0.5; 4 | &:hover { 5 | opacity: 1; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/crudeditor-lib/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import common from './common/reducer'; 3 | import search from './views/search/reducer'; 4 | import create from './views/create/reducer'; 5 | import edit from './views/edit/reducer'; 6 | import show from './views/show/reducer'; 7 | import error from './views/error/reducer'; 8 | import { isAllowed } from './lib'; 9 | import { 10 | VIEW_SEARCH, 11 | VIEW_CREATE, 12 | VIEW_EDIT, 13 | VIEW_SHOW, 14 | VIEW_ERROR, 15 | PERMISSION_CREATE, 16 | PERMISSION_EDIT, 17 | PERMISSION_VIEW 18 | } from './common/constants'; 19 | 20 | export default /* istanbul ignore next */ (modelDefinition, i18n) => { 21 | const { crudOperations } = modelDefinition.permissions; 22 | 23 | const viewReducers = { 24 | ...(isAllowed(crudOperations, PERMISSION_VIEW) ? { [VIEW_SEARCH]: search(modelDefinition, i18n) } : null), 25 | ...(isAllowed(crudOperations, PERMISSION_CREATE) ? { [VIEW_CREATE]: create(modelDefinition, i18n) } : null), 26 | ...(isAllowed(crudOperations, PERMISSION_EDIT) ? { [VIEW_EDIT]: edit(modelDefinition, i18n) } : null), 27 | ...(isAllowed(crudOperations, PERMISSION_VIEW) ? { [VIEW_SHOW]: show(modelDefinition, i18n) } : null), 28 | [VIEW_ERROR]: error(modelDefinition, i18n) 29 | } 30 | 31 | return combineReducers({ 32 | common: common(modelDefinition, i18n), 33 | views: combineReducers(viewReducers) 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/crudeditor-lib/selectorWrapper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A selector decorator allowing the wrapped selector to access particular subtree of Redux store 3 | * => selectors are namespaced just like reducers. 4 | */ 5 | const buildSelectorWrapper = (...path) => selector => (storeState, ...args) => selector( 6 | path.reduce( 7 | (subtree, node) => subtree[node], 8 | storeState 9 | ), 10 | ...args 11 | ); 12 | 13 | export const 14 | 15 | buildViewSelectorWrapper = viewName => buildSelectorWrapper('views', viewName), 16 | 17 | buildCommonSelectorWrapper = _ => buildSelectorWrapper('common'); 18 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/create/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | INSTANCE_SAVE, 3 | TAB_SELECT, 4 | INSTANCE_FIELD_VALIDATE, 5 | INSTANCE_FIELD_CHANGE, 6 | AFTER_ACTION_NEW 7 | } from './constants'; 8 | 9 | export const 10 | 11 | // █████████████████████████████████████████████████████████████████████████████████████████████████████████ 12 | 13 | saveInstance = /* istanbul ignore next */ _ => ({ 14 | type: INSTANCE_SAVE 15 | }), 16 | 17 | // █████████████████████████████████████████████████████████████████████████████████████████████████████████ 18 | 19 | selectTab = /* istanbul ignore next */ tabName => ({ 20 | type: TAB_SELECT, 21 | payload: { tabName } 22 | }), 23 | 24 | validateInstanceField = /* istanbul ignore next */ fieldName => ({ 25 | type: INSTANCE_FIELD_VALIDATE, 26 | payload: { 27 | name: fieldName 28 | } 29 | }), 30 | 31 | saveAndNewInstance = /* istanbul ignore next */ _ => ({ 32 | type: INSTANCE_SAVE, 33 | payload: { 34 | afterAction: AFTER_ACTION_NEW 35 | } 36 | }), 37 | 38 | changeInstanceField = /* istanbul ignore next */ ({ 39 | name: fieldName, 40 | value: fieldNewValue 41 | }) => ({ 42 | type: INSTANCE_FIELD_CHANGE, 43 | payload: { 44 | name: fieldName, 45 | value: fieldNewValue 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/create/constants.js: -------------------------------------------------------------------------------- 1 | import { VIEW_CREATE } from '../../common/constants'; 2 | 3 | const namespace = VIEW_CREATE; 4 | 5 | export const 6 | VIEW_NAME = VIEW_CREATE, 7 | AFTER_ACTION_NEW = 'new', 8 | 9 | /* ████████████████████████████████████████████ 10 | * ███ ACTION TYPES (in alphabetical order) ███ 11 | * ████████████████████████████████████████████ 12 | */ 13 | 14 | ALL_INSTANCE_FIELDS_VALIDATE_REQUEST = namespace + '/ALL_INSTANCE_FIELDS_VALIDATE_REQUEST', 15 | ALL_INSTANCE_FIELDS_VALIDATE_FAIL = namespace + '/ALL_INSTANCE_FIELDS_VALIDATE_FAIL', 16 | 17 | INSTANCE_FIELD_CHANGE = namespace + '/INSTANCE_FIELD_CHANGE', 18 | INSTANCE_FIELD_VALIDATE = namespace + '/INSTANCE_FIELD_VALIDATE', 19 | 20 | INSTANCE_VALIDATE_REQUEST = namespace + '/INSTANCE_VALIDATE_REQUEST', 21 | INSTANCE_VALIDATE_FAIL = namespace + '/INSTANCE_VALIDATE_FAIL', 22 | INSTANCE_VALIDATE_SUCCESS = namespace + '/INSTANCE_VALIDATE_SUCCESS', 23 | 24 | INSTANCE_SAVE = namespace + '/INSTANCE_SAVE', 25 | INSTANCE_SAVE_FAIL = namespace + '/INSTANCE_SAVE_FAIL', 26 | INSTANCE_SAVE_REQUEST = namespace + '/INSTANCE_SAVE_REQUEST', 27 | INSTANCE_SAVE_SUCCESS = namespace + '/INSTANCE_SAVE_SUCCESS', 28 | 29 | TAB_SELECT = namespace + '/TAB_SELECT', 30 | 31 | VIEW_INITIALIZE = namespace + '/VIEW_INITIALIZE', 32 | 33 | VIEW_REDIRECT_REQUEST = namespace + '/VIEW_REDIRECT_REQUEST', 34 | VIEW_REDIRECT_FAIL = namespace + '/VIEW_REDIRECT_FAIL', 35 | VIEW_REDIRECT_SUCCESS = namespace + '/VIEW_REDIRECT_SUCCESS'; 36 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/create/index.js: -------------------------------------------------------------------------------- 1 | import { VIEW_NAME } from './constants'; 2 | import { buildFormLayout } from '../lib'; 3 | 4 | export { getViewState } from './selectors'; 5 | 6 | export const getUi = ({ modelDefinition }) => { 7 | const createMeta = modelDefinition.ui.create || {}; 8 | 9 | if (!createMeta.defaultNewInstance) { 10 | createMeta.defaultNewInstance = _ => ({}); 11 | } 12 | 13 | createMeta.formLayout = buildFormLayout({ 14 | customBuilder: createMeta.formLayout, 15 | viewName: VIEW_NAME, 16 | fieldsMeta: modelDefinition.model.fields 17 | }); 18 | 19 | return createMeta; 20 | } 21 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/create/index.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { getUi } from './'; 3 | import { DEFAULT_FIELD_TYPE } from '../../common/constants'; 4 | 5 | describe('create view / index / getUi', () => { 6 | const fieldName = 'id'; 7 | 8 | const modelDefinition = { 9 | model: { 10 | fields: { 11 | [fieldName]: { 12 | type: DEFAULT_FIELD_TYPE 13 | } 14 | } 15 | }, 16 | ui: {} 17 | } 18 | 19 | it('should generate proper ui', () => { 20 | const result = getUi({ modelDefinition }) 21 | 22 | expect(result).to.have.ownProperty('defaultNewInstance'); 23 | expect(result).to.have.ownProperty('formLayout'); 24 | 25 | expect(result.defaultNewInstance).to.be.instanceof(Function); 26 | expect(result.formLayout).to.be.instanceof(Function); 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/create/scenario.js: -------------------------------------------------------------------------------- 1 | import { put, spawn } from 'redux-saga/effects'; 2 | 3 | import saveSaga from './workerSagas/save'; 4 | import redirectSaga from '../../common/workerSagas/redirect'; 5 | import { VIEW_SOFT_REDIRECT } from '../../common/constants'; 6 | import scenarioSaga from '../../common/scenario'; 7 | 8 | import { 9 | INSTANCE_SAVE, 10 | VIEW_INITIALIZE, 11 | VIEW_NAME 12 | } from './constants'; 13 | 14 | const transitions = { 15 | blocking: { 16 | [INSTANCE_SAVE]: saveSaga 17 | }, 18 | nonBlocking: { 19 | [VIEW_SOFT_REDIRECT]: redirectSaga 20 | } 21 | } 22 | 23 | // See Search View scenario for detailed description of the saga. 24 | export default function*({ 25 | modelDefinition, 26 | softRedirectSaga, 27 | viewState: { 28 | predefinedFields = {} 29 | }, 30 | source 31 | }) { 32 | yield put({ 33 | type: VIEW_INITIALIZE, 34 | payload: { predefinedFields }, 35 | meta: { source } 36 | }); 37 | 38 | return (yield spawn(scenarioSaga, { 39 | modelDefinition, 40 | softRedirectSaga, 41 | transitions, 42 | viewName: VIEW_NAME 43 | })); 44 | } 45 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/create/scenario.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { put, spawn } from 'redux-saga/effects'; 3 | import scenario from './scenario'; 4 | import commonScenario from '../../common/scenario'; 5 | import saveSaga from './workerSagas/save'; 6 | import redirectSaga from '../../common/workerSagas/redirect'; 7 | import { VIEW_SOFT_REDIRECT } from '../../common/constants'; 8 | 9 | import { 10 | INSTANCE_SAVE, 11 | VIEW_INITIALIZE, 12 | VIEW_NAME 13 | } from './constants'; 14 | 15 | const transitions = { 16 | blocking: { 17 | [INSTANCE_SAVE]: saveSaga 18 | }, 19 | nonBlocking: { 20 | [VIEW_SOFT_REDIRECT]: redirectSaga 21 | } 22 | } 23 | 24 | const arg = { 25 | modelDefinition: {}, 26 | softRedirectSaga: _ => null, 27 | viewState: {} 28 | } 29 | 30 | describe('create view / scenario', () => { 31 | const gen = scenario(arg); 32 | 33 | it('should put VIEW_INITIALIZE', () => { 34 | const { value, done } = gen.next(); 35 | expect(value).to.deep.equal(put({ 36 | type: VIEW_INITIALIZE, 37 | payload: { predefinedFields: arg.viewState.predefinedFields || {} }, 38 | meta: { source: arg.source } 39 | })); 40 | expect(done).to.be.false; // eslint-disable-line no-unused-expressions 41 | }) 42 | 43 | it('should fork scenario saga', () => { 44 | const { value, done } = gen.next(); 45 | expect(value).to.deep.equal(spawn(commonScenario, { 46 | viewName: VIEW_NAME, 47 | modelDefinition: arg.modelDefinition, 48 | softRedirectSaga: arg.softRedirectSaga, 49 | transitions 50 | })) 51 | expect(done).to.be.false; // eslint-disable-line no-unused-expressions 52 | }) 53 | 54 | it('should end iterator', () => { 55 | const { done } = gen.next(); 56 | expect(done).to.be.true; // eslint-disable-line no-unused-expressions 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/create/selectors.js: -------------------------------------------------------------------------------- 1 | import { buildViewSelectorWrapper } from '../../selectorWrapper'; 2 | 3 | import { 4 | VIEW_NAME 5 | } from './constants'; 6 | 7 | import { 8 | STATUS_REDIRECTING, 9 | STATUS_CREATING 10 | } from '../../common/constants'; 11 | 12 | const wrapper = buildViewSelectorWrapper(VIEW_NAME); 13 | 14 | export const 15 | 16 | // █████████████████████████████████████████████████████████████████████████████████████████████████████████ 17 | 18 | getViewState = wrapper(/* istanbul ignore next */ ({ 19 | predefinedFields 20 | }) => ({ 21 | predefinedFields 22 | })), 23 | 24 | // █████████████████████████████████████████████████████████████████████████████████████████████████████████ 25 | 26 | getViewModelData = wrapper(/* istanbul ignore next */ (storeState, { 27 | model: modelMeta, 28 | ui: { spinner } 29 | }) => ({ 30 | spinner, 31 | activeEntries: storeState.activeTab || storeState.formLayout, 32 | activeTab: storeState.activeTab, 33 | entityName: modelMeta.name, 34 | fieldErrors: storeState.errors.fields, 35 | fieldsMeta: modelMeta.fields, 36 | formattedInstance: storeState.formattedInstance, 37 | instanceLabel: storeState.instanceLabel, 38 | isLoading: ([STATUS_REDIRECTING, STATUS_CREATING].indexOf(storeState.status) > -1), 39 | tabs: storeState.formLayout.filter(({ tab }) => tab), 40 | status: storeState.status, 41 | unsavedChanges: 42 | storeState.formInstance && 43 | Object.keys(storeState.formInstance).some( 44 | key => storeState.formInstance[key] !== null && 45 | storeState.formInstance[key] !== storeState.predefinedFields[key] 46 | ), 47 | viewName: VIEW_NAME 48 | })); 49 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/edit/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | AFTER_ACTION_NEW, 3 | AFTER_ACTION_NEXT, 4 | INSTANCE_FIELD_VALIDATE, 5 | INSTANCE_FIELD_CHANGE, 6 | INSTANCE_SAVE, 7 | TAB_SELECT, 8 | ADJACENT_INSTANCE_EDIT 9 | } from './constants'; 10 | 11 | export const 12 | 13 | changeInstanceField = /* istanbul ignore next */ ({ 14 | name: fieldName, 15 | value: fieldNewValue 16 | }) => ({ 17 | type: INSTANCE_FIELD_CHANGE, 18 | payload: { 19 | name: fieldName, 20 | value: fieldNewValue 21 | } 22 | }), 23 | 24 | validateInstanceField = /* istanbul ignore next */ fieldName => ({ 25 | type: INSTANCE_FIELD_VALIDATE, 26 | payload: { 27 | name: fieldName 28 | } 29 | }), 30 | 31 | saveInstance = /* istanbul ignore next */ _ => ({ 32 | type: INSTANCE_SAVE 33 | }), 34 | 35 | saveAndNewInstance = /* istanbul ignore next */ _ => ({ 36 | type: INSTANCE_SAVE, 37 | payload: { 38 | afterAction: AFTER_ACTION_NEW 39 | } 40 | }), 41 | 42 | saveAndNextInstance = /* istanbul ignore next */ _ => ({ 43 | type: INSTANCE_SAVE, 44 | payload: { 45 | afterAction: AFTER_ACTION_NEXT 46 | } 47 | }), 48 | 49 | selectTab = /* istanbul ignore next */ tabName => ({ 50 | type: TAB_SELECT, 51 | payload: { tabName } 52 | }), 53 | 54 | editPreviousInstance = /* istanbul ignore next */ _ => ({ 55 | type: ADJACENT_INSTANCE_EDIT, 56 | payload: { 57 | step: -1 58 | } 59 | }), 60 | 61 | editNextInstance = /* istanbul ignore next */ _ => ({ 62 | type: ADJACENT_INSTANCE_EDIT, 63 | payload: { 64 | step: 1 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/edit/constants.js: -------------------------------------------------------------------------------- 1 | import { VIEW_EDIT } from '../../common/constants'; 2 | 3 | const namespace = VIEW_EDIT; 4 | 5 | export const 6 | VIEW_NAME = VIEW_EDIT, 7 | 8 | AFTER_ACTION_NEW = 'new', 9 | AFTER_ACTION_NEXT = 'next', 10 | 11 | /* ████████████████████████████████████████████ 12 | * ███ ACTION TYPES (in alphabetical order) ███ 13 | * ████████████████████████████████████████████ 14 | */ 15 | 16 | ADJACENT_INSTANCE_EDIT = namespace + '/ADJACENT_INSTANCE_EDIT', 17 | ADJACENT_INSTANCE_EDIT_FAIL = namespace + '/ADJACENT_INSTANCE_EDIT_FAIL', 18 | 19 | ALL_INSTANCE_FIELDS_VALIDATE_REQUEST = namespace + '/ALL_INSTANCE_FIELDS_VALIDATE_REQUEST', 20 | ALL_INSTANCE_FIELDS_VALIDATE_FAIL = namespace + '/ALL_INSTANCE_FIELDS_VALIDATE_FAIL', 21 | 22 | INSTANCE_EDIT_FAIL = namespace + '/INSTANCE_EDIT_FAIL', 23 | INSTANCE_EDIT_REQUEST = namespace + '/INSTANCE_EDIT_REQUEST', 24 | INSTANCE_EDIT_SUCCESS = namespace + '/INSTANCE_EDIT_SUCCESS', 25 | 26 | INSTANCE_FIELD_CHANGE = namespace + '/INSTANCE_FIELD_CHANGE', 27 | INSTANCE_FIELD_VALIDATE = namespace + '/INSTANCE_FIELD_VALIDATE', 28 | 29 | INSTANCE_VALIDATE_REQUEST = namespace + '/INSTANCE_VALIDATE_REQUEST', 30 | INSTANCE_VALIDATE_FAIL = namespace + '/INSTANCE_VALIDATE_FAIL', 31 | INSTANCE_VALIDATE_SUCCESS = namespace + '/INSTANCE_VALIDATE_SUCCESS', 32 | 33 | INSTANCE_SAVE = namespace + '/INSTANCE_SAVE', 34 | INSTANCE_SAVE_FAIL = namespace + '/INSTANCE_SAVE_FAIL', 35 | INSTANCE_SAVE_REQUEST = namespace + '/INSTANCE_SAVE_REQUEST', 36 | INSTANCE_SAVE_SUCCESS = namespace + '/INSTANCE_SAVE_SUCCESS', 37 | 38 | TAB_SELECT = namespace + '/TAB_SELECT', 39 | 40 | VIEW_INITIALIZE_REQUEST = namespace + '/VIEW_INITIALIZE_REQUEST', 41 | VIEW_INITIALIZE_FAIL = namespace + '/VIEW_INITIALIZE_FAIL', 42 | VIEW_INITIALIZE_SUCCESS = namespace + '/VIEW_INITIALIZE_SUCCESS', 43 | 44 | VIEW_REDIRECT_REQUEST = namespace + '/VIEW_REDIRECT_REQUEST', 45 | VIEW_REDIRECT_FAIL = namespace + '/VIEW_REDIRECT_FAIL', 46 | VIEW_REDIRECT_SUCCESS = namespace + '/VIEW_REDIRECT_SUCCESS'; 47 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/edit/index.js: -------------------------------------------------------------------------------- 1 | import { VIEW_NAME } from './constants'; 2 | import { buildFormLayout } from '../lib'; 3 | 4 | export { getViewState } from './selectors'; 5 | 6 | export const getUi = ({ modelDefinition }) => { 7 | const editMeta = modelDefinition.ui.edit || {}; 8 | 9 | editMeta.formLayout = buildFormLayout({ 10 | customBuilder: editMeta.formLayout, 11 | viewName: VIEW_NAME, 12 | fieldsMeta: modelDefinition.model.fields 13 | }); 14 | 15 | return editMeta; 16 | } 17 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/edit/index.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { getUi } from './'; 3 | import { DEFAULT_FIELD_TYPE } from '../../common/constants'; 4 | 5 | describe('edit view / index / getUi', () => { 6 | const fieldName = 'id'; 7 | 8 | const modelDefinition = { 9 | model: { 10 | fields: { 11 | [fieldName]: { 12 | type: DEFAULT_FIELD_TYPE 13 | } 14 | } 15 | }, 16 | ui: {} 17 | } 18 | 19 | it('should generate proper ui', () => { 20 | const result = getUi({ modelDefinition }) 21 | expect(result).to.have.ownProperty('formLayout'); 22 | expect(result.formLayout).to.be.instanceof(Function); 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/edit/workerSagas/delete.js: -------------------------------------------------------------------------------- 1 | import { call, put } from 'redux-saga/effects'; 2 | 3 | import deleteSaga from '../../../common/workerSagas/delete'; 4 | import { VIEW_REDIRECT_REQUEST } from '../constants'; 5 | 6 | import { 7 | VIEW_ERROR, 8 | VIEW_SEARCH 9 | } from '../../../common/constants'; 10 | 11 | /* 12 | * XXX: in case of failure, a worker saga must dispatch an appropriate action and exit by throwing error(s). 13 | */ 14 | export default function*({ 15 | modelDefinition, 16 | softRedirectSaga, 17 | action: { 18 | payload: { instances }, 19 | meta 20 | } 21 | }) { 22 | yield call(deleteSaga, { // Forwarding thrown error(s) to the parent saga. 23 | modelDefinition, 24 | action: { 25 | payload: { instances }, 26 | meta 27 | } 28 | }); 29 | 30 | yield put({ 31 | type: VIEW_REDIRECT_REQUEST, 32 | meta 33 | }); 34 | 35 | try { 36 | yield call(softRedirectSaga, { 37 | viewName: VIEW_SEARCH 38 | }); 39 | } catch (err) { 40 | yield call(softRedirectSaga, { 41 | viewName: VIEW_ERROR, 42 | viewState: err 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/edit/workerSagas/edit.js: -------------------------------------------------------------------------------- 1 | import { call, put } from 'redux-saga/effects'; 2 | 3 | import { getLogicalKeyBuilder } from '../../lib'; 4 | 5 | import { 6 | INSTANCE_EDIT_FAIL, 7 | INSTANCE_EDIT_REQUEST, 8 | INSTANCE_EDIT_SUCCESS 9 | } from '../constants'; 10 | 11 | /* 12 | * XXX: in case of failure, a worker saga must dispatch an appropriate action and exit by throwing error(s). 13 | */ 14 | export default function*({ 15 | modelDefinition, 16 | action: { 17 | payload: { 18 | instance, 19 | offset 20 | }, 21 | meta 22 | } 23 | }) { 24 | yield put({ 25 | type: INSTANCE_EDIT_REQUEST, 26 | meta 27 | }); 28 | 29 | let persistentInstance; 30 | 31 | try { 32 | persistentInstance = yield call(modelDefinition.api.get, { 33 | instance: getLogicalKeyBuilder(modelDefinition.model.fields)(instance) 34 | }); 35 | } catch (err) { 36 | yield put({ 37 | type: INSTANCE_EDIT_FAIL, 38 | payload: err, 39 | error: true, 40 | meta 41 | }); 42 | 43 | throw err; 44 | } 45 | 46 | yield put({ 47 | type: INSTANCE_EDIT_SUCCESS, 48 | payload: { 49 | instance: persistentInstance, 50 | offset 51 | }, 52 | meta 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/edit/workerSagas/save.js: -------------------------------------------------------------------------------- 1 | import { call, select } from 'redux-saga/effects'; 2 | 3 | import adjacentSaga from '../../../common/workerSagas/adjacent'; 4 | import redirectSaga from '../../../common/workerSagas/redirect'; 5 | import validateSaga from '../../../common/workerSagas/validate'; 6 | import updateSaga from '../../../common/workerSagas/save'; 7 | import { VIEW_CREATE } from '../../../common/constants'; 8 | import { getDefaultNewInstance } from '../../search/selectors'; 9 | 10 | import { 11 | AFTER_ACTION_NEXT, 12 | AFTER_ACTION_NEW 13 | } from '../constants'; 14 | 15 | /* 16 | * XXX: in case of failure, a worker saga must dispatch an appropriate action and exit by throwing error(s). 17 | */ 18 | export default function*({ 19 | modelDefinition, 20 | softRedirectSaga, 21 | action: { 22 | payload: { afterAction } = {}, 23 | meta 24 | } 25 | }) { 26 | // XXX: error(s) thrown in called below sagas are forwarded to the parent saga. Use try..catch to alter this default. 27 | 28 | yield call(validateSaga, { modelDefinition, meta }); 29 | yield call(updateSaga, { modelDefinition, meta }); 30 | 31 | if (afterAction === AFTER_ACTION_NEW) { 32 | yield call(redirectSaga, { 33 | modelDefinition, 34 | softRedirectSaga, 35 | action: { 36 | payload: { 37 | view: { 38 | name: VIEW_CREATE, 39 | state: { 40 | predefinedFields: yield select(storeState => getDefaultNewInstance(storeState, modelDefinition)) 41 | } 42 | } 43 | }, 44 | meta 45 | } 46 | }); 47 | } else if (afterAction === AFTER_ACTION_NEXT) { 48 | yield call(adjacentSaga, { 49 | modelDefinition, 50 | softRedirectSaga, 51 | action: { 52 | payload: { 53 | step: 1 54 | }, 55 | meta 56 | } 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/error/constants.js: -------------------------------------------------------------------------------- 1 | import { VIEW_ERROR } from '../../common/constants'; 2 | 3 | const namespace = VIEW_ERROR; 4 | 5 | export const 6 | VIEW_NAME = VIEW_ERROR, 7 | 8 | /* ████████████████████████████████████████████ 9 | * ███ ACTION TYPES (in alphabetical order) ███ 10 | * ████████████████████████████████████████████ 11 | */ 12 | 13 | VIEW_INITIALIZE = namespace + '/VIEW_INITIALIZE', 14 | 15 | VIEW_REDIRECT_REQUEST = namespace + '/VIEW_REDIRECT_REQUEST', 16 | VIEW_REDIRECT_FAIL = namespace + '/VIEW_REDIRECT_FAIL', 17 | VIEW_REDIRECT_SUCCESS = namespace + '/VIEW_REDIRECT_SUCCESS'; 18 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/error/container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Main from '../../../components/ErrorMain'; 4 | import { getViewModelData } from './selectors'; 5 | import { softRedirectView } from '../../common/actions'; 6 | import { VIEW_SEARCH, PERMISSION_VIEW } from '../../common/constants'; 7 | import { isAllowed } from '../../lib'; 8 | 9 | const mergeProps = /* istanbul ignore next */ ( 10 | { 11 | viewModelData, 12 | permissions: { 13 | crudOperations 14 | }, 15 | uiConfig 16 | }, 17 | { 18 | goHome, 19 | ...dispatchProps 20 | } 21 | ) => ({ 22 | viewModel: { 23 | uiConfig, 24 | data: viewModelData, 25 | actions: { 26 | ...(isAllowed(crudOperations, PERMISSION_VIEW) && { goHome }), 27 | ...dispatchProps 28 | } 29 | } 30 | }); 31 | 32 | export default connect( 33 | /* istanbul ignore next */ 34 | (storeState, { modelDefinition, uiConfig }) => ({ 35 | viewModelData: getViewModelData(storeState, modelDefinition), 36 | permissions: modelDefinition.permissions, 37 | uiConfig 38 | }), 39 | { 40 | goHome: /* istanbul ignore next */ _ => softRedirectView({ 41 | name: VIEW_SEARCH 42 | }) 43 | }, 44 | mergeProps 45 | )( 46 | /* istanbul ignore next */ 47 | ({ viewModel }) =>
48 | ); 49 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/error/index.js: -------------------------------------------------------------------------------- 1 | export { getViewState } from './selectors'; 2 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/error/scenario.js: -------------------------------------------------------------------------------- 1 | import { put, spawn } from 'redux-saga/effects'; 2 | 3 | import { VIEW_NAME } from './constants'; 4 | import { VIEW_SOFT_REDIRECT } from '../../common/constants'; 5 | import redirectSaga from '../../common/workerSagas/redirect'; 6 | import scenarioSaga from '../../common/scenario'; 7 | 8 | import { VIEW_INITIALIZE } from './constants'; 9 | 10 | const transitions = { 11 | blocking: {}, 12 | nonBlocking: { 13 | [VIEW_SOFT_REDIRECT]: redirectSaga 14 | } 15 | }; 16 | 17 | // See Search View scenario for detailed description of the saga. 18 | export default function*({ 19 | modelDefinition, 20 | softRedirectSaga, 21 | viewState: err, 22 | source 23 | }) { 24 | yield put({ 25 | type: VIEW_INITIALIZE, 26 | payload: err, 27 | meta: { source } 28 | }); 29 | 30 | return (yield spawn(scenarioSaga, { 31 | modelDefinition, 32 | softRedirectSaga, 33 | transitions, 34 | viewName: VIEW_NAME 35 | })); 36 | } 37 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/error/scenario.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { runSaga } from 'redux-saga'; 3 | import scenarioSaga from './scenario'; 4 | 5 | import { 6 | VIEW_INITIALIZE 7 | } from './constants'; 8 | 9 | const arg = { 10 | modelDefinition: { 11 | api: { 12 | get: _ => ({}) 13 | }, 14 | model: { 15 | fields: {} 16 | } 17 | }, 18 | softRedirectSaga: _ => null, 19 | viewState: { 20 | code: 303, 21 | message: 'Some error' 22 | } 23 | } 24 | 25 | describe('error view / scenario', () => { 26 | it('should initialize view with error', () => { 27 | const dispatched = []; 28 | 29 | runSaga({ 30 | dispatch: (action) => dispatched.push(action) 31 | }, scenarioSaga, arg); 32 | 33 | expect(dispatched[0]).to.deep.equal({ 34 | type: VIEW_INITIALIZE, 35 | payload: arg.viewState, 36 | meta: { 37 | source: arg.source 38 | } 39 | }) 40 | }); 41 | }) 42 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/error/selectors.js: -------------------------------------------------------------------------------- 1 | import { buildViewSelectorWrapper } from '../../selectorWrapper'; 2 | import { VIEW_NAME } from './constants'; 3 | import { STATUS_REDIRECTING } from '../../common/constants'; 4 | 5 | const wrapper = buildViewSelectorWrapper(VIEW_NAME); 6 | 7 | export const 8 | 9 | // █████████████████████████████████████████████████████████████████████████████████████████████████████████ 10 | 11 | getViewState = wrapper(/* istanbul ignore next */ ({ errors }) => errors), 12 | 13 | // █████████████████████████████████████████████████████████████████████████████████████████████████████████ 14 | 15 | getViewModelData = wrapper(/* istanbul ignore next */ storeState => ({ 16 | errors: storeState.errors, 17 | isLoading: storeState.status === STATUS_REDIRECTING 18 | })); 19 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/search/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | FORM_FILTER_RESET, 3 | FORM_FILTER_UPDATE, 4 | GOTO_PAGE_UPDATE, 5 | INSTANCES_SEARCH, 6 | INSTANCE_SELECT, 7 | INSTANCE_DESELECT, 8 | ALL_INSTANCES_SELECT, 9 | ALL_INSTANCES_DESELECT, 10 | SEARCH_FORM_TOGGLE 11 | } from './constants'; 12 | 13 | export const 14 | searchInstances = /* istanbul ignore next */ ({ 15 | filter, 16 | sort, 17 | order, 18 | max, 19 | offset 20 | } = {}) => ({ 21 | type: INSTANCES_SEARCH, 22 | payload: { 23 | filter, 24 | sort, 25 | order, 26 | max, 27 | offset 28 | } 29 | }), 30 | 31 | updateFormFilter = /* istanbul ignore next */ ({ 32 | name, 33 | value 34 | }) => ({ 35 | type: FORM_FILTER_UPDATE, 36 | payload: { 37 | name, 38 | value 39 | } 40 | }), 41 | 42 | updateGotoPage = /* istanbul ignore next */ page => ({ 43 | type: GOTO_PAGE_UPDATE, 44 | payload: { page } 45 | }), 46 | 47 | resetFormFilter = _ => ({ 48 | type: FORM_FILTER_RESET 49 | }), 50 | 51 | toggleSelected = /* istanbul ignore next */ ({ selected, instance }) => ({ 52 | type: selected ? INSTANCE_SELECT : INSTANCE_DESELECT, 53 | payload: { instance } 54 | }), 55 | 56 | toggleSelectedAll = /* istanbul ignore next */ selected => ({ 57 | type: selected ? ALL_INSTANCES_SELECT : ALL_INSTANCES_DESELECT 58 | }), 59 | 60 | toggleSearchForm = _ => ({ 61 | type: SEARCH_FORM_TOGGLE 62 | }); 63 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/search/constants.js: -------------------------------------------------------------------------------- 1 | import { VIEW_SEARCH } from '../../common/constants'; 2 | 3 | const namespace = VIEW_SEARCH; 4 | 5 | export const 6 | VIEW_NAME = VIEW_SEARCH, 7 | 8 | DEFAULT_OFFSET = 0, 9 | DEFAULT_ORDER = 'asc', 10 | 11 | /* ████████████████████████████████████████████ 12 | * ███ ACTION TYPES (in alphabetical order) ███ 13 | * ████████████████████████████████████████████ 14 | */ 15 | 16 | ALL_INSTANCES_SELECT = namespace + '/ALL_INSTANCES_SELECT', 17 | ALL_INSTANCES_DESELECT = namespace + '/ALL_INSTANCES_DESELECT', 18 | 19 | FORM_FILTER_RESET = namespace + '/FORM_FILTER_RESET', 20 | FORM_FILTER_UPDATE = namespace + '/FORM_FILTER_UPDATE', 21 | GOTO_PAGE_UPDATE = namespace + '/GOTO_PAGE_UPDATE', 22 | 23 | INSTANCES_CUSTOM_ACTION_INITIALIZATION = namespace + '/INSTANCES_CUSTOM_ACTION_INITIALIZATION', 24 | INSTANCES_CUSTOM_ACTION_FINALIZATION = namespace + '/INSTANCES_CUSTOM_ACTION_FINALIZATION', 25 | 26 | INSTANCES_SEARCH = namespace + '/INSTANCES_SEARCH', 27 | INSTANCES_SEARCH_FAIL = namespace + '/INSTANCES_SEARCH_FAIL', 28 | INSTANCES_SEARCH_REQUEST = namespace + '/INSTANCES_SEARCH_REQUEST', 29 | INSTANCES_SEARCH_SUCCESS = namespace + '/INSTANCES_SEARCH_SUCCESS', 30 | 31 | INSTANCE_SELECT = namespace + '/INSTANCE_SELECT', 32 | INSTANCE_DESELECT = namespace + '/INSTANCE_DESELECT', 33 | 34 | VIEW_INITIALIZE_REQUEST = namespace + '/VIEW_INITIALIZE_REQUEST', 35 | VIEW_INITIALIZE_FAIL = namespace + '/VIEW_INITIALIZE_FAIL', 36 | VIEW_INITIALIZE_SUCCESS = namespace + '/VIEW_INITIALIZE_SUCCESS', 37 | 38 | VIEW_REDIRECT_REQUEST = namespace + '/VIEW_REDIRECT_REQUEST', 39 | VIEW_REDIRECT_FAIL = namespace + '/VIEW_REDIRECT_FAIL', 40 | VIEW_REDIRECT_SUCCESS = namespace + '/VIEW_REDIRECT_SUCCESS', 41 | 42 | SEARCH_FORM_TOGGLE = namespace + '/SEARCH_FORM_TOGGLE'; 43 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/search/index.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { I18nManager } from '@opuscapita/i18n'; 3 | import { getUi } from './'; 4 | import { DEFAULT_FIELD_TYPE } from '../../common/constants'; 5 | 6 | describe('search view / index / getUi', () => { 7 | const fieldName = 'id'; 8 | 9 | const modelDefinition = { 10 | model: { 11 | fields: { 12 | [fieldName]: { 13 | type: DEFAULT_FIELD_TYPE 14 | } 15 | } 16 | }, 17 | ui: {} 18 | } 19 | 20 | const i18n = new I18nManager(); 21 | 22 | it('should generate proper ui', () => { 23 | const result = getUi({ modelDefinition, i18n }) 24 | expect(result).to.have.ownProperty('resultFields'); 25 | expect(result).to.have.ownProperty('searchableFields'); 26 | 27 | expect(result.resultFields[0]).to.have.ownProperty('format'); 28 | expect(result.searchableFields[0]).to.have.ownProperty('render'); 29 | 30 | expect(result.resultFields[0].name).to.equal(fieldName); 31 | expect(result.searchableFields[0].name).to.equal(fieldName); 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/search/lib.js: -------------------------------------------------------------------------------- 1 | import { EMPTY_FIELD_VALUE } from '../../common/constants'; 2 | 3 | // The function returns new filter value with EMPTY_FIELD_VALUE leaf nodes removed, 4 | // or undefined when all filter values get cleansed. 5 | export const cleanFilter = filter => Object.keys(filter).reduce( 6 | (rez, name) => filter[name] === EMPTY_FIELD_VALUE ? 7 | rez : { 8 | ...(rez || {}), 9 | [name]: filter[name] 10 | }, 11 | undefined 12 | ); 13 | 14 | export const getDefaultSortField = searchMeta => { 15 | let fieldName = searchMeta.resultFields[0].name; 16 | 17 | if (searchMeta.resultFields.every(({ name, sortByDefault }) => { 18 | if (sortByDefault) { 19 | fieldName = name; 20 | return false; 21 | } 22 | 23 | return true; 24 | })) { 25 | searchMeta.resultFields.some(({ name, sortable }) => { 26 | if (sortable) { 27 | fieldName = name; 28 | return true; 29 | } 30 | 31 | return false; 32 | }) 33 | } 34 | 35 | return fieldName; 36 | }; 37 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/search/lib.spec.js: -------------------------------------------------------------------------------- 1 | import { assert, expect } from 'chai'; 2 | import { cleanFilter, getDefaultSortField } from './lib'; 3 | import { EMPTY_FIELD_VALUE } from '../../common/constants'; 4 | 5 | describe('search view lib', () => { 6 | describe('cleanFilter', () => { 7 | const filter = { 8 | 'firstField': null, 9 | 'secondField': 'second value', 10 | 'thirdField': 10 11 | } 12 | 13 | it('should return filter with not-null values only', () => { 14 | const result = cleanFilter(filter); 15 | assert.deepEqual( 16 | result, Object.keys(filter).filter(key => filter[key] !== EMPTY_FIELD_VALUE).reduce( 17 | (acc, cur) => ({ ...acc, [cur]: filter[cur] }), undefined 18 | ) 19 | ) 20 | }); 21 | 22 | it('should return undefined for filter with all null values', () => { 23 | const result = cleanFilter(Object.keys(filter).reduce((acc, cur) => ({ ...acc, [cur]: null }), undefined)); 24 | expect(result).to.not.exist; // eslint-disable-line no-unused-expressions 25 | }); 26 | }); 27 | 28 | describe('getDefaultSortField', () => { 29 | const searchMeta = { 30 | resultFields: [ 31 | { name: 'first' }, 32 | { name: 'second' }, 33 | { name: 'third' } 34 | ] 35 | } 36 | 37 | it(`should return first field name if no 'sortByDefault' found`, () => { 38 | const result = getDefaultSortField(searchMeta); 39 | expect(result).to.equal(searchMeta.resultFields[0].name) 40 | }); 41 | 42 | it(`should return field name with 'sortByDefault' prop`, () => { 43 | searchMeta.resultFields[2].sortByDefault = true; 44 | const result = getDefaultSortField(searchMeta); 45 | expect(result).to.equal(searchMeta.resultFields[2].name) 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/search/workerSagas/customBulkOperation.js: -------------------------------------------------------------------------------- 1 | import { call, put } from 'redux-saga/effects'; 2 | 3 | import searchSaga from './search'; 4 | import { 5 | INSTANCES_CUSTOM_ACTION_INITIALIZATION, 6 | INSTANCES_CUSTOM_ACTION_FINALIZATION 7 | } from "../constants"; 8 | 9 | /* 10 | * XXX: in case of failure, a worker saga must dispatch an appropriate action and exit by throwing error(s). 11 | */ 12 | export default function*({ 13 | modelDefinition, 14 | softRedirectSaga, 15 | action: { 16 | payload: { instances, handler }, 17 | meta 18 | } 19 | }) { 20 | // XXX: error(s) thrown in called below sagas are forwarded to the parent saga. Use try..catch to alter this default. 21 | 22 | yield put({ 23 | type: INSTANCES_CUSTOM_ACTION_INITIALIZATION, 24 | meta 25 | }); 26 | 27 | try { 28 | yield call(handler, { instances }); 29 | } finally { 30 | yield put({ 31 | type: INSTANCES_CUSTOM_ACTION_FINALIZATION, 32 | meta 33 | }); 34 | } 35 | 36 | // Refresh search results. 37 | yield call(searchSaga, { 38 | modelDefinition, 39 | softRedirectSaga, 40 | action: { 41 | payload: {}, 42 | meta 43 | } 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/search/workerSagas/delete.js: -------------------------------------------------------------------------------- 1 | import { call } from 'redux-saga/effects'; 2 | 3 | import deleteSaga from '../../../common/workerSagas/delete'; 4 | import searchSaga from './search'; 5 | 6 | /* 7 | * XXX: in case of failure, a worker saga must dispatch an appropriate action and exit by throwing error(s). 8 | */ 9 | export default function*({ 10 | modelDefinition, 11 | softRedirectSaga, 12 | action: { 13 | payload: { instances }, 14 | meta 15 | } 16 | }) { 17 | // XXX: error(s) thrown in called below sagas are forwarded to the parent saga. Use try..catch to alter this default. 18 | 19 | yield call(deleteSaga, { 20 | modelDefinition, 21 | action: { 22 | payload: { instances }, 23 | meta 24 | } 25 | }); 26 | 27 | // Refresh search results. 28 | yield call(searchSaga, { 29 | modelDefinition, 30 | softRedirectSaga, 31 | action: { 32 | payload: {}, 33 | meta 34 | } 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/search/workerSagas/delete.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import omit from 'lodash/omit'; 3 | 4 | import deleteSaga from './delete'; 5 | import searchSaga from './search'; 6 | import commonDeleteSaga from '../../../common/workerSagas/delete'; 7 | 8 | describe('search view / worker sagas / delete', () => { 9 | const instances = [{ a: 1 }, { b: 2 }]; 10 | 11 | const arg = { 12 | modelDefinition: {}, 13 | softRedirectSaga: _ => null, 14 | action: { 15 | payload: { instances }, 16 | meta: {} 17 | } 18 | } 19 | 20 | const gen = deleteSaga(arg); 21 | 22 | it('should call delete saga', () => { 23 | const { value, done } = gen.next(); 24 | expect(value).to.have.ownProperty('CALL'); 25 | expect(value.CALL.fn).to.equal(commonDeleteSaga); 26 | expect(value.CALL.args[0]).to.deep.equal(omit(arg, ['softRedirectSaga'])); 27 | expect(done).to.be.false; // eslint-disable-line no-unused-expressions 28 | }) 29 | 30 | it('should call search saga', () => { 31 | const { value, done } = gen.next(); 32 | expect(value).to.have.ownProperty('CALL'); 33 | expect(value.CALL.fn).to.equal(searchSaga); 34 | expect(done).to.be.false; // eslint-disable-line no-unused-expressions 35 | }) 36 | 37 | it('should end iterator', () => { 38 | const { done } = gen.next(); 39 | expect(done).to.be.true; // eslint-disable-line no-unused-expressions 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/show/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | TAB_SELECT, 3 | ADJACENT_INSTANCE_SHOW 4 | } from './constants' 5 | 6 | export const 7 | 8 | selectTab = tabName => ({ 9 | type: TAB_SELECT, 10 | payload: { tabName } 11 | }), 12 | 13 | showPreviousInstance = _ => ({ 14 | type: ADJACENT_INSTANCE_SHOW, 15 | payload: { 16 | step: -1 17 | } 18 | }), 19 | 20 | showNextInstance = _ => ({ 21 | type: ADJACENT_INSTANCE_SHOW, 22 | payload: { 23 | step: 1 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/show/constants.js: -------------------------------------------------------------------------------- 1 | import { VIEW_SHOW } from '../../common/constants' 2 | 3 | const namespace = VIEW_SHOW; 4 | 5 | export const 6 | VIEW_NAME = VIEW_SHOW, 7 | 8 | /* ████████████████████████████████████████████ 9 | * ███ ACTION TYPES (in alphabetical order) ███ 10 | * ████████████████████████████████████████████ 11 | */ 12 | 13 | ADJACENT_INSTANCE_SHOW = namespace + '/ADJACENT_INSTANCE_SHOW', 14 | ADJACENT_INSTANCE_SHOW_FAIL = namespace + '/ADJACENT_INSTANCE_SHOW_FAIL', 15 | 16 | INSTANCE_SHOW_REQUEST = namespace + '/INSTANCE_SHOW_REQUEST', 17 | INSTANCE_SHOW_FAIL = namespace + '/INSTANCE_SHOW_FAIL', 18 | INSTANCE_SHOW_SUCCESS = namespace + '/INSTANCE_SHOW_SUCCESS', 19 | 20 | TAB_SELECT = namespace + '/TAB_SELECT', 21 | 22 | VIEW_REDIRECT_REQUEST = namespace + '/VIEW_REDIRECT_REQUEST', 23 | VIEW_REDIRECT_FAIL = namespace + '/VIEW_REDIRECT_FAIL', 24 | VIEW_REDIRECT_SUCCESS = namespace + '/VIEW_REDIRECT_SUCCESS', 25 | 26 | VIEW_INITIALIZE_REQUEST = namespace + '/VIEW_INITIALIZE_REQUEST', 27 | VIEW_INITIALIZE_FAIL = namespace + '/VIEW_INITIALIZE_FAIL', 28 | VIEW_INITIALIZE_SUCCESS = namespace + '/VIEW_INITIALIZE_SUCCESS'; 29 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/show/index.js: -------------------------------------------------------------------------------- 1 | import { VIEW_NAME } from './constants'; 2 | import { buildFormLayout } from '../lib'; 3 | 4 | export { getViewState } from './selectors'; 5 | 6 | export const getUi = ({ modelDefinition }) => { 7 | const showMeta = modelDefinition.ui.show || {}; 8 | 9 | showMeta.formLayout = buildFormLayout({ 10 | customBuilder: showMeta.formLayout, 11 | viewName: VIEW_NAME, 12 | fieldsMeta: modelDefinition.model.fields 13 | }); 14 | 15 | return showMeta; 16 | } 17 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/show/index.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { getUi } from './'; 3 | import { DEFAULT_FIELD_TYPE } from '../../common/constants'; 4 | 5 | describe('show view / index / getUi', () => { 6 | const fieldName = 'id'; 7 | 8 | const modelDefinition = { 9 | model: { 10 | fields: { 11 | [fieldName]: { 12 | type: DEFAULT_FIELD_TYPE 13 | } 14 | } 15 | }, 16 | ui: {} 17 | } 18 | 19 | it('should generate proper ui', () => { 20 | const result = getUi({ modelDefinition }) 21 | expect(result).to.have.ownProperty('formLayout'); 22 | expect(result.formLayout).to.be.instanceof(Function); 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/show/scenario.js: -------------------------------------------------------------------------------- 1 | import { call, put, spawn } from 'redux-saga/effects'; 2 | 3 | import showSaga from './workerSagas/show'; 4 | import adjacentSaga from '../../common/workerSagas/adjacent'; 5 | import redirectSaga from '../../common/workerSagas/redirect'; 6 | import scenarioSaga from '../../common/scenario'; 7 | import { VIEW_SOFT_REDIRECT } from '../../common/constants'; 8 | 9 | import { 10 | TAB_SELECT, 11 | VIEW_INITIALIZE_REQUEST, 12 | VIEW_INITIALIZE_FAIL, 13 | VIEW_INITIALIZE_SUCCESS, 14 | ADJACENT_INSTANCE_SHOW, 15 | VIEW_NAME 16 | } from './constants'; 17 | 18 | const transitions = { 19 | blocking: {}, 20 | nonBlocking: { 21 | [ADJACENT_INSTANCE_SHOW]: adjacentSaga, 22 | [VIEW_SOFT_REDIRECT]: redirectSaga 23 | } 24 | }; 25 | 26 | // See Search View scenario for detailed description of the saga. 27 | export default function*({ 28 | modelDefinition, 29 | softRedirectSaga, 30 | viewState: { 31 | instance, 32 | tab: tabName 33 | }, 34 | offset, 35 | source 36 | }) { 37 | yield put({ 38 | type: VIEW_INITIALIZE_REQUEST, 39 | meta: { source } 40 | }); 41 | 42 | try { 43 | yield call(showSaga, { 44 | modelDefinition, 45 | action: { 46 | payload: { 47 | instance, 48 | offset 49 | }, 50 | meta: { source } 51 | } 52 | }); 53 | } catch (err) { 54 | yield put({ 55 | type: VIEW_INITIALIZE_FAIL, 56 | payload: err, 57 | error: true, 58 | meta: { source } 59 | }); 60 | 61 | throw err; // Initialization error(s) are forwarded to the parent saga. 62 | } 63 | 64 | yield put({ 65 | type: TAB_SELECT, 66 | payload: { tabName }, 67 | meta: { source } 68 | }); 69 | 70 | yield put({ 71 | type: VIEW_INITIALIZE_SUCCESS, 72 | meta: { source } 73 | }); 74 | 75 | return (yield spawn(scenarioSaga, { 76 | modelDefinition, 77 | softRedirectSaga, 78 | transitions, 79 | viewName: VIEW_NAME 80 | })); 81 | } 82 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/show/scenario.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { runSaga } from 'redux-saga'; 3 | import { call } from 'redux-saga/effects'; 4 | import scenarioSaga from './scenario'; 5 | import { 6 | VIEW_INITIALIZE_REQUEST, 7 | VIEW_INITIALIZE_SUCCESS, 8 | VIEW_INITIALIZE_FAIL, 9 | INSTANCE_SHOW_REQUEST, 10 | INSTANCE_SHOW_SUCCESS, 11 | INSTANCE_SHOW_FAIL, 12 | TAB_SELECT 13 | } from './constants'; 14 | 15 | const arg = { 16 | modelDefinition: { 17 | api: { 18 | get: _ => ({}) 19 | }, 20 | model: { 21 | fields: {} 22 | } 23 | }, 24 | softRedirectSaga: _ => null, 25 | viewState: {} 26 | } 27 | 28 | describe('show view / scenario', () => { 29 | it('should dispatch required actions in order', () => { 30 | const dispatched = []; 31 | 32 | runSaga({ 33 | dispatch: (action) => dispatched.push(action) 34 | }, scenarioSaga, arg); 35 | 36 | expect(dispatched.map(({ type }) => type)).to.deep.equal([ 37 | VIEW_INITIALIZE_REQUEST, 38 | INSTANCE_SHOW_REQUEST, 39 | INSTANCE_SHOW_SUCCESS, 40 | TAB_SELECT, 41 | VIEW_INITIALIZE_SUCCESS 42 | ]) 43 | }); 44 | 45 | it('should fail if get api throws an error', () => { 46 | const dispatched = []; 47 | 48 | const errMessage = 'Pretend that server-side failed'; 49 | 50 | const wrapper = function*(...args) { 51 | try { 52 | yield call(scenarioSaga, ...args) 53 | } catch (e) { 54 | expect(e.message).equal(errMessage) 55 | } 56 | } 57 | 58 | runSaga({ 59 | dispatch: (action) => dispatched.push(action) 60 | }, wrapper, { 61 | ...arg, 62 | modelDefinition: { 63 | ...arg.modelDefinition, 64 | api: { 65 | get: _ => { throw new Error(errMessage) } 66 | } 67 | } 68 | }); 69 | 70 | expect(dispatched.map(({ type }) => type)).to.deep.equal([ 71 | VIEW_INITIALIZE_REQUEST, 72 | INSTANCE_SHOW_REQUEST, 73 | INSTANCE_SHOW_FAIL, 74 | VIEW_INITIALIZE_FAIL 75 | ]) 76 | }); 77 | }) 78 | -------------------------------------------------------------------------------- /src/crudeditor-lib/views/show/workerSagas/show.js: -------------------------------------------------------------------------------- 1 | import { call, put } from 'redux-saga/effects'; 2 | 3 | import { getLogicalKeyBuilder } from '../../lib'; 4 | 5 | import { 6 | INSTANCE_SHOW_FAIL, 7 | INSTANCE_SHOW_REQUEST, 8 | INSTANCE_SHOW_SUCCESS 9 | } from '../constants'; 10 | 11 | /* // 12 | * XXX: in case of failure, a worker saga must dispatch an appropriate action and exit by throwing error(s). 13 | */ 14 | export default function*({ 15 | modelDefinition, 16 | action: { 17 | payload: { 18 | instance, 19 | offset 20 | }, 21 | meta 22 | } 23 | }) { 24 | yield put({ 25 | type: INSTANCE_SHOW_REQUEST, 26 | meta 27 | }); 28 | 29 | let persistentInstance; 30 | 31 | try { 32 | persistentInstance = yield call(modelDefinition.api.get, { 33 | instance: getLogicalKeyBuilder(modelDefinition.model.fields)(instance) 34 | }); 35 | } catch (err) { 36 | yield put({ 37 | type: INSTANCE_SHOW_FAIL, 38 | payload: err, 39 | error: true, 40 | meta 41 | }); 42 | 43 | throw err; 44 | } 45 | 46 | yield put({ 47 | type: INSTANCE_SHOW_SUCCESS, 48 | payload: { 49 | instance: persistentInstance, 50 | offset 51 | }, 52 | meta 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/boolean/booleanUiType.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | /* 4 | * ██████████████████████████████████████████████████ 5 | * ████ FIELD_TYPE_BOOLEAN ► UI_TYPE_BOOLEAN ████ 6 | * ██████████████████████████████████████████████████ 7 | */ 8 | format: value => value, 9 | 10 | /* 11 | * ██████████████████████████████████████████████████ 12 | * ████ FIELD_TYPE_BOOLEAN ◄ UI_TYPE_BOOLEAN ████ 13 | * ██████████████████████████████████████████████████ 14 | */ 15 | parse: value => value 16 | }; 17 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/boolean/booleanUiType.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import converter from './booleanUiType'; 3 | 4 | describe('fieldTypes :: boolean <-> boolean', () => { 5 | it('should return itself for format', () => { 6 | expect(converter.format(true)).to.equal(true); 7 | expect(converter.format(false)).to.equal(false); 8 | }); 9 | 10 | it('should return itself for parse', () => { 11 | expect(converter.parse(true)).to.equal(true); 12 | expect(converter.parse(false)).to.equal(false); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/boolean/decimalUiType.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | /* 4 | * ██████████████████████████████████████████████████ 5 | * ████ FIELD_TYPE_BOOLEAN ► UI_TYPE_DECIMAL ████ 6 | * ██████████████████████████████████████████████████ 7 | */ 8 | format: value => value ? 1 : 0, 9 | 10 | /* 11 | * ██████████████████████████████████████████████████ 12 | * ████ FIELD_TYPE_BOOLEAN ◄ UI_TYPE_DECIMAL ████ 13 | * ██████████████████████████████████████████████████ 14 | */ 15 | parse: value => !!value 16 | }; 17 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/boolean/decimalUiType.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import converter from './decimalUiType'; 3 | 4 | describe('fieldTypes :: boolean <-> decimal', () => { 5 | describe('format', () => { 6 | it('should return 1 for true', () => { 7 | expect(converter.format(true)).to.equal(1); 8 | }); 9 | it('should return 0 for false', () => { 10 | expect(converter.format(false)).to.equal(0); 11 | }); 12 | }) 13 | 14 | describe('parse', () => { 15 | it('should return false for 0', () => { 16 | expect(converter.parse(0)).to.equal(false); 17 | expect(converter.parse(-0)).to.equal(false); 18 | }); 19 | it('should return true for any integer !== 0', () => { 20 | expect(converter.parse(12.23)).to.equal(true); 21 | expect(converter.parse(-124.234)).to.equal(true); 22 | }); 23 | }) 24 | }); 25 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/boolean/index.js: -------------------------------------------------------------------------------- 1 | import booleanUiType from './booleanUiType'; 2 | import integerUiType from './integerUiType'; 3 | import decimalUiType from './decimalUiType'; 4 | import stringUiType from './stringUiType'; 5 | 6 | import { 7 | EMPTY_FIELD_VALUE, 8 | 9 | UI_TYPE_BOOLEAN, 10 | UI_TYPE_INTEGER, 11 | UI_TYPE_DECIMAL, 12 | UI_TYPE_STRING 13 | } from '../../constants'; 14 | 15 | export default { 16 | 17 | isValid: value => value === EMPTY_FIELD_VALUE || typeof value === 'boolean', 18 | 19 | converter: { 20 | [UI_TYPE_BOOLEAN]: booleanUiType, 21 | [UI_TYPE_INTEGER]: integerUiType, 22 | [UI_TYPE_DECIMAL]: decimalUiType, 23 | [UI_TYPE_STRING]: stringUiType 24 | }, 25 | 26 | buildValidator: value => ({}) 27 | }; 28 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/boolean/index.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | import index from './index'; 4 | 5 | describe('fieldTypes :: boolean', () => { 6 | describe('isValid', () => { 7 | it('should return true for null', () => { 8 | assert.equal(index.isValid(null), true) 9 | }); 10 | 11 | it('should return true for boolean true', () => { 12 | assert.equal(index.isValid(true), true) 13 | }); 14 | 15 | it('should return true for boolean false', () => { 16 | assert.equal(index.isValid(false), true) 17 | }); 18 | 19 | it('should return false for undefined, number or string', () => { 20 | assert.equal(index.isValid(undefined), false) 21 | assert.equal(index.isValid(0), false) 22 | assert.equal(index.isValid(12), false) 23 | assert.equal(index.isValid(''), false) 24 | assert.equal(index.isValid('qweqwewq'), false) 25 | }); 26 | }); 27 | 28 | describe('buildValidator', () => { 29 | it('should return an empty object', () => { 30 | assert.deepEqual(index.buildValidator(true), {}); 31 | assert.deepEqual(index.buildValidator(false), {}) 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/boolean/integerUiType.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | /* 4 | * ██████████████████████████████████████████████████ 5 | * ████ FIELD_TYPE_BOOLEAN ► UI_TYPE_INTEGER ████ 6 | * ██████████████████████████████████████████████████ 7 | */ 8 | format: value => value ? 1 : 0, 9 | 10 | /* 11 | * ██████████████████████████████████████████████████ 12 | * ████ FIELD_TYPE_BOOLEAN ◄ UI_TYPE_INTEGER ████ 13 | * ██████████████████████████████████████████████████ 14 | */ 15 | parse: value => !!value 16 | }; 17 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/boolean/integerUiType.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import converter from './integerUiType'; 3 | 4 | describe('fieldTypes :: boolean <-> integer', () => { 5 | describe('format', () => { 6 | it('should return 1 for true', () => { 7 | expect(converter.format(true)).to.equal(1); 8 | }); 9 | it('should return 0 for false', () => { 10 | expect(converter.format(false)).to.equal(0); 11 | }); 12 | }) 13 | 14 | describe('parse', () => { 15 | it('should return false for 0', () => { 16 | expect(converter.parse(0)).to.equal(false); 17 | expect(converter.parse(-0)).to.equal(false); 18 | }); 19 | it('should return true for any integer !== 0', () => { 20 | expect(converter.parse(12)).to.equal(true); 21 | expect(converter.parse(-124)).to.equal(true); 22 | }); 23 | }) 24 | }); 25 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/boolean/stringUiType.js: -------------------------------------------------------------------------------- 1 | import { 2 | ERROR_CODE_PARSING, 3 | EMPTY_FIELD_VALUE, 4 | ERROR_INVALID_BOOLEAN 5 | } from '../../constants'; 6 | 7 | export default { 8 | 9 | /* 10 | * █████████████████████████████████████████████████ 11 | * ████ FIELD_TYPE_BOOLEAN ► UI_TYPE_STRING ████ 12 | * █████████████████████████████████████████████████ 13 | */ 14 | format: value => value ? '+' : '-', 15 | 16 | /* 17 | * █████████████████████████████████████████████████ 18 | * ████ FIELD_TYPE_BOOLEAN ◄ UI_TYPE_STRING ████ 19 | * █████████████████████████████████████████████████ 20 | */ 21 | parse: value => { 22 | const optimized = value.trim().toLowerCase(); 23 | 24 | if (!optimized) { 25 | return EMPTY_FIELD_VALUE; // Considering whitespaces-only strings to be empty value. 26 | } 27 | 28 | if (['-', 'no', 'false', 'off'].indexOf(optimized) !== -1) { 29 | return false; 30 | } 31 | 32 | if (['+', 'yes', 'true', 'on', 'ok'].indexOf(optimized) !== -1) { 33 | return true; 34 | } 35 | 36 | const error = { 37 | code: ERROR_CODE_PARSING, 38 | id: ERROR_INVALID_BOOLEAN, 39 | message: 'Unable to parse string as boolean value' 40 | }; 41 | 42 | throw error; 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/boolean/stringUiType.spec.js: -------------------------------------------------------------------------------- 1 | import { expect, assert } from 'chai'; 2 | import converter from './stringUiType'; 3 | import { 4 | ERROR_CODE_PARSING, 5 | ERROR_INVALID_BOOLEAN 6 | } from '../../constants'; 7 | 8 | describe('fieldTypes :: boolean <-> string', () => { 9 | describe('format', () => { 10 | it('should return + for true', () => { 11 | expect(converter.format(true)).to.equal('+'); 12 | }); 13 | it('should return - for false', () => { 14 | expect(converter.format(false)).to.equal('-'); 15 | }); 16 | }) 17 | 18 | describe('parse', () => { 19 | it('should return null for empty string', () => { 20 | expect(converter.parse('')).to.equal(null); 21 | }); 22 | 23 | it(`should return true for ['+', 'yes', 'true', 'on', 'ok']`, () => { 24 | expect(converter.parse('+')).to.equal(true); 25 | expect(converter.parse('yes')).to.equal(true); 26 | expect(converter.parse('true')).to.equal(true); 27 | expect(converter.parse('on')).to.equal(true); 28 | expect(converter.parse('ok')).to.equal(true); 29 | }); 30 | 31 | it(`should return false for ['-', 'no', 'false', 'off']`, () => { 32 | expect(converter.parse('-')).to.equal(false); 33 | expect(converter.parse('no')).to.equal(false); 34 | expect(converter.parse('false')).to.equal(false); 35 | expect(converter.parse('off')).to.equal(false); 36 | }); 37 | 38 | it(`should throw for unknown string`, () => { 39 | try { 40 | converter.parse('weewew'); 41 | assert(false); 42 | } catch (e) { 43 | assert.deepEqual( 44 | e, { 45 | code: ERROR_CODE_PARSING, 46 | id: ERROR_INVALID_BOOLEAN, 47 | message: 'Unable to parse string as boolean value' 48 | } 49 | ) 50 | } 51 | }); 52 | }) 53 | }); 54 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/decimal/decimalUiType.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | /* 4 | * ██████████████████████████████████████████████████ 5 | * ████ FIELD_TYPE_DECIMAL ► UI_TYPE_DECMIAL ████ 6 | * ██████████████████████████████████████████████████ 7 | */ 8 | format: value => value, 9 | 10 | /* 11 | * ██████████████████████████████████████████████████ 12 | * ████ FIELD_TYPE_DECIMAL ◄ UI_TYPE_DECMIAL ████ 13 | * ██████████████████████████████████████████████████ 14 | */ 15 | parse: value => value 16 | }; 17 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/decimal/decimalUiType.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import converter from './decimalUiType'; 3 | 4 | describe('fieldTypes :: decimal <-> decimal', () => { 5 | it('should return itself for format', () => { 6 | const value = 10.12; 7 | const result = converter.format(value); 8 | 9 | expect(result).to.equal(value); 10 | }); 11 | 12 | it('should return itself for parse', () => { 13 | const value = 10.12; 14 | const result = converter.parse(value); 15 | 16 | expect(result).to.equal(value); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/decimal/index.js: -------------------------------------------------------------------------------- 1 | import decimalUiType from './decimalUiType'; 2 | import stringUiType from './stringUiType'; 3 | import { throwError } from '../lib'; 4 | 5 | import { 6 | CONSTRAINT_MIN, 7 | CONSTRAINT_MAX, 8 | 9 | EMPTY_FIELD_VALUE, 10 | 11 | ERROR_CODE_VALIDATION, 12 | 13 | ERROR_MIN_DECEEDED, 14 | ERROR_MAX_EXCEEDED, 15 | 16 | UI_TYPE_DECIMAL, 17 | UI_TYPE_STRING 18 | } from '../../constants'; 19 | 20 | export default { 21 | 22 | isValid: value => value === EMPTY_FIELD_VALUE || typeof value === 'number' && !isNaN(value), 23 | 24 | converter: { 25 | [UI_TYPE_DECIMAL]: decimalUiType, 26 | [UI_TYPE_STRING]: stringUiType 27 | }, 28 | 29 | buildValidator: value => ({ 30 | 31 | /* 32 | * Specifies the minimum number allowed. 33 | * param is a number. 34 | */ 35 | [CONSTRAINT_MIN]: param => value >= param || throwError({ 36 | code: ERROR_CODE_VALIDATION, 37 | id: ERROR_MIN_DECEEDED, 38 | args: { 39 | min: param 40 | } 41 | }), 42 | 43 | /* 44 | * Specifies the maximum number allowed. 45 | * param is a number. 46 | */ 47 | [CONSTRAINT_MAX]: param => value <= param || throwError({ 48 | code: ERROR_CODE_VALIDATION, 49 | id: ERROR_MAX_EXCEEDED, 50 | args: { 51 | max: param 52 | } 53 | }) 54 | }) 55 | }; 56 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/decimal/stringUiType.js: -------------------------------------------------------------------------------- 1 | import { 2 | ERROR_CODE_PARSING, 3 | ERROR_INVALID_DECIMAL 4 | } from '../../constants'; 5 | 6 | export default { 7 | 8 | /* 9 | * █████████████████████████████████████████████████ 10 | * ████ FIELD_TYPE_DECIMAL ► UI_TYPE_STRING ████ 11 | * █████████████████████████████████████████████████ 12 | */ 13 | format: (value, i18n) => i18n.formatDecimalNumber(value), 14 | 15 | /* 16 | * █████████████████████████████████████████████████ 17 | * ████ FIELD_TYPE_DECIMAL ◄ UI_TYPE_STRING ████ 18 | * █████████████████████████████████████████████████ 19 | */ 20 | parse: (value, i18n) => { 21 | let n; 22 | 23 | try { 24 | n = i18n.parseDecimalNumber(value || null) 25 | } catch (err) { 26 | if (err.name === 'ParseError') { 27 | err = { // eslint-disable-line no-ex-assign 28 | code: ERROR_CODE_PARSING, 29 | id: ERROR_INVALID_DECIMAL, 30 | message: 'Invalid decimal number' 31 | } 32 | } 33 | 34 | throw err; 35 | } 36 | 37 | if (isNaN(n)) { 38 | const error = { 39 | code: ERROR_CODE_PARSING, 40 | id: ERROR_INVALID_DECIMAL, 41 | message: 'Invalid decimal number' 42 | }; 43 | 44 | throw error; 45 | } 46 | 47 | return n; 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/decimal/stringUiType.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import { I18nManager } from '@opuscapita/i18n'; 3 | 4 | import { 5 | ERROR_CODE_PARSING, 6 | ERROR_INVALID_DECIMAL 7 | } from '../../constants'; 8 | 9 | import converter from './stringUiType'; 10 | 11 | describe('fieldTypes :: decimal <-> string', () => { 12 | const i18n = new I18nManager(); 13 | 14 | it('should convert decimal to string', () => { 15 | const value = 10.12; 16 | const result = converter.format(value, i18n); 17 | 18 | assert.strictEqual( 19 | result, 20 | i18n.formatDecimalNumber(value) 21 | ) 22 | }); 23 | 24 | it('should convert stringified decimal to decimal', () => { 25 | const value = '132.125'; 26 | const result = converter.parse(value, i18n); 27 | 28 | assert.strictEqual( 29 | result, 30 | i18n.parseDecimalNumber(value) 31 | ) 32 | }); 33 | 34 | it('should throw for unparsable value', () => { 35 | const value = 'sdfsdfdsf'; 36 | 37 | try { 38 | converter.parse(value, i18n) 39 | assert(false) 40 | } catch (e) { 41 | assert.deepEqual( 42 | e, { 43 | code: ERROR_CODE_PARSING, 44 | id: ERROR_INVALID_DECIMAL, 45 | message: 'Invalid decimal number' 46 | } 47 | ) 48 | } 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/decimalRange/index.js: -------------------------------------------------------------------------------- 1 | import baseFieldType from '../decimal'; 2 | import { buildRangeFieldType } from '../lib'; 3 | 4 | import { 5 | UI_TYPE_DECIMAL_RANGE_OBJECT, 6 | UI_TYPE_STRING_RANGE_OBJECT, 7 | UI_TYPE_DECIMAL, 8 | UI_TYPE_STRING 9 | } from '../../constants'; 10 | 11 | export default buildRangeFieldType(baseFieldType, { 12 | [UI_TYPE_DECIMAL_RANGE_OBJECT]: UI_TYPE_DECIMAL, 13 | [UI_TYPE_STRING_RANGE_OBJECT]: UI_TYPE_STRING 14 | }); 15 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/integer/index.js: -------------------------------------------------------------------------------- 1 | import integerUiType from './integerUiType'; 2 | import stringUiType from './stringUiType'; 3 | import { throwError } from '../lib'; 4 | import { 5 | CONSTRAINT_MIN, 6 | CONSTRAINT_MAX, 7 | 8 | EMPTY_FIELD_VALUE, 9 | 10 | ERROR_CODE_VALIDATION, 11 | 12 | ERROR_MIN_DECEEDED, 13 | ERROR_MAX_EXCEEDED, 14 | 15 | UI_TYPE_INTEGER, 16 | UI_TYPE_STRING 17 | } from '../../constants'; 18 | 19 | export default { 20 | 21 | isValid: value => 22 | value === EMPTY_FIELD_VALUE || 23 | typeof value === 'number' && !isNaN(value) && value === Math.floor(value), 24 | 25 | converter: { 26 | [UI_TYPE_INTEGER]: integerUiType, 27 | [UI_TYPE_STRING]: stringUiType 28 | }, 29 | 30 | buildValidator: value => ({ 31 | 32 | /* 33 | * Specifies the minimum number allowed. 34 | * param is a number. 35 | */ 36 | [CONSTRAINT_MIN]: param => value >= param || throwError({ 37 | code: ERROR_CODE_VALIDATION, 38 | id: ERROR_MIN_DECEEDED, 39 | args: { 40 | min: param 41 | } 42 | }), 43 | 44 | /* 45 | * Specifies the maximum number allowed. 46 | * param is a number. 47 | */ 48 | [CONSTRAINT_MAX]: param => value <= param || throwError({ 49 | code: ERROR_CODE_VALIDATION, 50 | id: ERROR_MAX_EXCEEDED, 51 | args: { 52 | max: param 53 | } 54 | }) 55 | }) 56 | }; 57 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/integer/integerUiType.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | /* 4 | * ██████████████████████████████████████████████████ 5 | * ████ FIELD_TYPE_INTEGER ► UI_TYPE_INTEGER ████ 6 | * ██████████████████████████████████████████████████ 7 | */ 8 | format: value => value, 9 | 10 | /* 11 | * ██████████████████████████████████████████████████ 12 | * ████ FIELD_TYPE_INTEGER ◄ UI_TYPE_INTEGER ████ 13 | * ██████████████████████████████████████████████████ 14 | */ 15 | parse: value => value 16 | }; 17 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/integer/integerUiType.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import converter from './integerUiType'; 3 | 4 | describe('fieldTypes :: integer <-> integer', () => { 5 | it('should return itself for format', () => { 6 | const value = 1012; 7 | const result = converter.format(value); 8 | 9 | expect(result).to.equal(value); 10 | }); 11 | 12 | it('should return itself for parse', () => { 13 | const value = 1012; 14 | const result = converter.parse(value); 15 | 16 | expect(result).to.equal(value); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/integer/stringUiType.js: -------------------------------------------------------------------------------- 1 | import { 2 | ERROR_CODE_PARSING, 3 | ERROR_INVALID_INTEGER 4 | } from '../../constants'; 5 | 6 | export default { 7 | 8 | /* 9 | * █████████████████████████████████████████████████ 10 | * ████ FIELD_TYPE_INTEGER ► UI_TYPE_STRING ████ 11 | * █████████████████████████████████████████████████ 12 | */ 13 | format: (value, i18n) => i18n.formatNumber(value), 14 | 15 | /* 16 | * █████████████████████████████████████████████████ 17 | * ████ FIELD_TYPE_INTEGER ◄ UI_TYPE_STRING ████ 18 | * █████████████████████████████████████████████████ 19 | */ 20 | parse: (value, i18n) => { 21 | let n; 22 | 23 | try { 24 | n = i18n.parseNumber(value || null) 25 | } catch (err) { 26 | if (err.name === 'ParseError') { 27 | err = { // eslint-disable-line no-ex-assign 28 | code: ERROR_CODE_PARSING, 29 | id: ERROR_INVALID_INTEGER, 30 | message: 'Invalid integer' 31 | } 32 | } 33 | 34 | throw err; 35 | } 36 | 37 | if (isNaN(n)) { 38 | const error = { 39 | code: ERROR_CODE_PARSING, 40 | id: ERROR_INVALID_INTEGER, 41 | message: 'Invalid integer' 42 | }; 43 | 44 | throw error; 45 | } 46 | 47 | return n; 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/integer/stringUiType.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import { I18nManager } from '@opuscapita/i18n'; 3 | 4 | import { 5 | ERROR_CODE_PARSING, 6 | ERROR_INVALID_INTEGER 7 | } from '../../constants'; 8 | 9 | import converter from './stringUiType'; 10 | 11 | describe('fieldTypes :: decimal <-> string', () => { 12 | const i18n = new I18nManager(); 13 | 14 | it('should convert integer to string', () => { 15 | const value = 1012567; 16 | const result = converter.format(value, i18n); 17 | 18 | assert.strictEqual( 19 | result, 20 | i18n.formatNumber(value) 21 | ) 22 | }); 23 | 24 | it('should convert stringified integer to decimal', () => { 25 | const value = '132345'; 26 | const result = converter.parse(value, i18n); 27 | 28 | assert.strictEqual( 29 | result, 30 | i18n.parseNumber(value) 31 | ) 32 | }); 33 | 34 | it('should throw for unparsable value', () => { 35 | const value = 'sdfsdfdsf'; 36 | 37 | try { 38 | converter.parse(value, i18n) 39 | assert(false) 40 | } catch (e) { 41 | assert.deepEqual( 42 | e, { 43 | code: ERROR_CODE_PARSING, 44 | id: ERROR_INVALID_INTEGER, 45 | message: 'Invalid integer' 46 | } 47 | ) 48 | } 49 | }); 50 | 51 | it('should throw for decimal string value', () => { 52 | const value = '12.21'; 53 | 54 | try { 55 | converter.parse(value, i18n) 56 | assert(false) 57 | } catch (e) { 58 | assert.deepEqual( 59 | e, { 60 | code: ERROR_CODE_PARSING, 61 | id: ERROR_INVALID_INTEGER, 62 | message: 'Invalid integer' 63 | } 64 | ) 65 | } 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/integerRange/index.js: -------------------------------------------------------------------------------- 1 | import baseFieldType from '../integer'; 2 | import { buildRangeFieldType } from '../lib'; 3 | 4 | import { 5 | UI_TYPE_INTEGER_RANGE_OBJECT, 6 | UI_TYPE_STRING_RANGE_OBJECT, 7 | UI_TYPE_INTEGER, 8 | UI_TYPE_STRING 9 | } from '../../constants'; 10 | 11 | export default buildRangeFieldType(baseFieldType, { 12 | [UI_TYPE_INTEGER_RANGE_OBJECT]: UI_TYPE_INTEGER, 13 | [UI_TYPE_STRING_RANGE_OBJECT]: UI_TYPE_STRING 14 | }); 15 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/lib.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { throwError } from './lib'; 3 | 4 | describe('fieldTypes lib', () => { 5 | describe('throwError', () => { 6 | it('should throw given object', () => { 7 | const err = { a: 'b' }; 8 | try { 9 | throwError(err); 10 | assert(false) 11 | } catch (e) { 12 | assert.deepEqual(e, err) 13 | } 14 | }) 15 | }) 16 | }); 17 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/string/decimalUiType.js: -------------------------------------------------------------------------------- 1 | import { 2 | ERROR_CODE_FORMATING, 3 | ERROR_INVALID_DECIMAL 4 | } from '../../constants'; 5 | 6 | export default { 7 | 8 | /* 9 | * █████████████████████████████████████████████████ 10 | * ████ FIELD_TYPE_STRING ► UI_TYPE_DECIMAL ████ 11 | * █████████████████████████████████████████████████ 12 | */ 13 | format: value => { 14 | const n = Number(value); 15 | 16 | if (n !== parseFloat(value)) { 17 | const error = { 18 | code: ERROR_CODE_FORMATING, 19 | id: ERROR_INVALID_DECIMAL, 20 | message: 'Invalid decimal number' 21 | }; 22 | 23 | throw error; 24 | } 25 | 26 | return n; 27 | }, 28 | 29 | /* 30 | * █████████████████████████████████████████████████ 31 | * ████ FIELD_TYPE_STRING ◄ UI_TYPE_DECIMAL ████ 32 | * █████████████████████████████████████████████████ 33 | */ 34 | parse: value => value.toString() 35 | }; 36 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/string/decimalUiType.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import assert from 'assert'; 3 | import { 4 | ERROR_CODE_FORMATING, 5 | ERROR_INVALID_DECIMAL 6 | } from '../../constants'; 7 | 8 | import converter from './decimalUiType'; 9 | 10 | describe('fieldTypes :: string <-> decimal', () => { 11 | it('should convert stringified decimal to decimal', () => { 12 | const value = '213.21'; 13 | const result = converter.format(value); 14 | 15 | expect(result).to.equal(213.21) 16 | }); 17 | 18 | it('should throw for not a number', () => { 19 | const value = '23Hello'; 20 | 21 | try { 22 | converter.format(value); 23 | assert(false) 24 | } catch (e) { 25 | assert.deepEqual( 26 | e, { 27 | code: ERROR_CODE_FORMATING, 28 | id: ERROR_INVALID_DECIMAL, 29 | message: 'Invalid decimal number' 30 | } 31 | ) 32 | } 33 | }); 34 | 35 | it('should stringify decimal', () => { 36 | const value = 23432.323; 37 | 38 | expect(converter.parse(value)).to.equal(String(value)); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/string/integerUiType.js: -------------------------------------------------------------------------------- 1 | import { 2 | ERROR_CODE_FORMATING, 3 | ERROR_INVALID_INTEGER 4 | } from '../../constants'; 5 | 6 | export default { 7 | 8 | /* 9 | * █████████████████████████████████████████████████ 10 | * ████ FIELD_TYPE_STRING ► UI_TYPE_INTEGER ████ 11 | * █████████████████████████████████████████████████ 12 | */ 13 | format: value => { 14 | const n = Number(value); 15 | 16 | if (n !== parseInt(value, 10)) { 17 | const error = { 18 | code: ERROR_CODE_FORMATING, 19 | id: ERROR_INVALID_INTEGER, 20 | message: 'Invalid integer' 21 | }; 22 | 23 | throw error; 24 | } 25 | 26 | return n; 27 | }, 28 | 29 | /* 30 | * █████████████████████████████████████████████████ 31 | * ████ FIELD_TYPE_STRING ◄ UI_TYPE_INTEGER ████ 32 | * █████████████████████████████████████████████████ 33 | */ 34 | parse: value => value.toString() 35 | }; 36 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/string/integerUiType.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import assert from 'assert'; 3 | import { 4 | ERROR_CODE_FORMATING, 5 | ERROR_INVALID_INTEGER 6 | } from '../../constants'; 7 | 8 | import converter from './integerUiType'; 9 | 10 | describe('fieldTypes :: string <-> integer', () => { 11 | it('should convert stringified integer to integer', () => { 12 | const value = '21321'; 13 | const result = converter.format(value); 14 | 15 | expect(result).to.equal(21321) 16 | }); 17 | 18 | it('should throw for decimal number', () => { 19 | const value = '132.125'; 20 | 21 | try { 22 | converter.format(value); 23 | assert(false) 24 | } catch (e) { 25 | assert.deepEqual( 26 | e, { 27 | code: ERROR_CODE_FORMATING, 28 | id: ERROR_INVALID_INTEGER, 29 | message: 'Invalid integer' 30 | } 31 | ) 32 | } 33 | }); 34 | 35 | it('should throw for not a number', () => { 36 | const value = '23Hello'; 37 | 38 | try { 39 | converter.format(value); 40 | assert(false) 41 | } catch (e) { 42 | assert.deepEqual( 43 | e, { 44 | code: ERROR_CODE_FORMATING, 45 | id: ERROR_INVALID_INTEGER, 46 | message: 'Invalid integer' 47 | } 48 | ) 49 | } 50 | }); 51 | 52 | it('should stringify integer', () => { 53 | const value = 2342423423; 54 | 55 | expect(converter.parse(value)).to.equal(String(value)); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/string/stringUiType.js: -------------------------------------------------------------------------------- 1 | import { EMPTY_FIELD_VALUE } from '../../constants'; 2 | 3 | export default { 4 | 5 | /* 6 | * ████████████████████████████████████████████████ 7 | * ████ FIELD_TYPE_STRING ► UI_TYPE_STRING ████ 8 | * ████████████████████████████████████████████████ 9 | */ 10 | format: value => value, 11 | 12 | /* 13 | * ████████████████████████████████████████████████ 14 | * ████ FIELD_TYPE_STRING ◄ UI_TYPE_STRING ████ 15 | * ████████████████████████████████████████████████ 16 | */ 17 | parse: value => value || EMPTY_FIELD_VALUE 18 | }; 19 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/string/stringUiType.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import converter from './stringUiType'; 3 | 4 | describe('fieldTypes :: string <-> string', () => { 5 | it('should return itself for format', () => { 6 | const value = 'ewrewrewrw2342rwe'; 7 | const result = converter.format(value); 8 | 9 | expect(result).to.equal(value); 10 | }); 11 | 12 | it('should return itself for parse if not empty string', () => { 13 | const value = 'ewrewrewrw2342rwe'; 14 | const result = converter.parse(value); 15 | 16 | expect(result).to.equal(value); 17 | }); 18 | 19 | it('should return null for parse if empty string', () => { 20 | const value = ''; 21 | const result = converter.parse(value); 22 | 23 | expect(result).to.equal(null); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/stringDate/dateUiType.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | /* 4 | * ███████████████████████████████████████████████████ 5 | * ████ FIELD_TYPE_STRING_DATE ► UI_TYPE_DATE ████ 6 | * ███████████████████████████████████████████████████ 7 | * 8 | * UI_TYPE_DATE has empty value => value !== EMPTY_FIELD_VALUE 9 | */ 10 | format: value => new Date(value), 11 | 12 | /* 13 | * ███████████████████████████████████████████████████ 14 | * ████ FIELD_TYPE_STRING_DATE ◄ UI_TYPE_DATE ████ 15 | * ███████████████████████████████████████████████████ 16 | */ 17 | parse: value => value.toISOString() 18 | }; 19 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/stringDate/dateUiType.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import converter from './dateUiType'; 4 | 5 | describe('fieldTypes :: stringDate <-> date', () => { 6 | it('should convert stringDate to date', () => { 7 | const value = '2017-12-2'; 8 | const result = converter.format(value); 9 | 10 | expect(result.valueOf()).to.equal(new Date(value).valueOf()) 11 | }); 12 | 13 | it('should convert date to stringDate (ISO String)', () => { 14 | const value = new Date(); 15 | const result = converter.parse(value); 16 | 17 | expect(result).to.equal(value.toISOString()) 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/stringDate/index.js: -------------------------------------------------------------------------------- 1 | import dateUiType from './dateUiType'; 2 | import stringUiType from './stringUiType'; 3 | import { throwError } from '../lib'; 4 | import { 5 | CONSTRAINT_MIN, 6 | CONSTRAINT_MAX, 7 | 8 | EMPTY_FIELD_VALUE, 9 | 10 | ERROR_CODE_VALIDATION, 11 | 12 | ERROR_MIN_DECEEDED, 13 | ERROR_MAX_EXCEEDED, 14 | 15 | UI_TYPE_DATE, 16 | UI_TYPE_STRING 17 | } from '../../constants'; 18 | 19 | export default { 20 | 21 | isValid(value) { 22 | if (value === EMPTY_FIELD_VALUE) { 23 | return true; 24 | } 25 | 26 | if (typeof value !== 'string') { 27 | return false; 28 | } 29 | 30 | return (new Date(value) !== "Invalid Date") && !isNaN(new Date(value)); 31 | }, 32 | 33 | converter: { 34 | [UI_TYPE_DATE]: dateUiType, 35 | [UI_TYPE_STRING]: stringUiType 36 | }, 37 | 38 | 39 | buildValidator(origValue) { 40 | const value = new Date(origValue); 41 | 42 | return { 43 | 44 | /* 45 | * Specifies the minimum value allowed. 46 | * param is string. 47 | */ 48 | [CONSTRAINT_MIN]: param => value >= new Date(param) || throwError({ 49 | code: ERROR_CODE_VALIDATION, 50 | id: ERROR_MIN_DECEEDED, 51 | args: { 52 | min: param 53 | } 54 | }), 55 | 56 | /* 57 | * Specifies the maximum value allowed. 58 | * param is string. 59 | */ 60 | [CONSTRAINT_MAX]: param => value <= new Date(param) || throwError({ 61 | code: ERROR_CODE_VALIDATION, 62 | id: ERROR_MAX_EXCEEDED, 63 | args: { 64 | max: param 65 | } 66 | }) 67 | }; 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/stringDate/stringUiType.js: -------------------------------------------------------------------------------- 1 | import { 2 | ERROR_CODE_PARSING, 3 | EMPTY_FIELD_VALUE, 4 | ERROR_INVALID_DATE 5 | } from '../../constants'; 6 | 7 | export default { 8 | 9 | /* 10 | * █████████████████████████████████████████████████████ 11 | * ████ FIELD_TYPE_STRING_DATE ► UI_TYPE_STRING ████ 12 | * █████████████████████████████████████████████████████ 13 | */ 14 | format: value => new Date(value).toString(), 15 | 16 | /* 17 | * █████████████████████████████████████████████████████ 18 | * ████ FIELD_TYPE_STRING_DATE ◄ UI_TYPE_STRING ████ 19 | * █████████████████████████████████████████████████████ 20 | */ 21 | parse: value => { 22 | if (!value.trim()) { 23 | return EMPTY_FIELD_VALUE; // Considering whitespaces-only strings to be empty value. 24 | } 25 | 26 | if ((new Date(value) === "Invalid Date") || isNaN(new Date(value))) { 27 | const error = { 28 | code: ERROR_CODE_PARSING, 29 | id: ERROR_INVALID_DATE, 30 | message: 'Invalid date' 31 | }; 32 | 33 | throw error; 34 | } 35 | 36 | return new Date(value).toISOString(); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/stringDate/stringUiType.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import assert from 'assert'; 3 | import { 4 | ERROR_CODE_PARSING, 5 | EMPTY_FIELD_VALUE, 6 | ERROR_INVALID_DATE 7 | } from '../../constants'; 8 | import converter from './stringUiType'; 9 | 10 | describe('fieldTypes :: stringDate <-> string', () => { 11 | describe('format', () => { 12 | it('should convert stringified date to string', () => { 13 | const value = new Date().toISOString(); 14 | const result = converter.format(value); 15 | 16 | expect(result).to.equal(new Date(value).toString()) 17 | }); 18 | }); 19 | 20 | describe('parse', () => { 21 | it('should convert empty string into null', () => { 22 | const value = ''; 23 | const result = converter.parse(value); 24 | 25 | expect(result).to.equal(EMPTY_FIELD_VALUE) 26 | }); 27 | 28 | it('should convert stringified date into stringDate', () => { 29 | const value = new Date().toString(); 30 | const result = converter.parse(value); 31 | 32 | expect(result).to.equal(new Date(value).toISOString()) 33 | }); 34 | 35 | it('should throw for not date-like string', () => { 36 | const value = 'ewqrwerew'; 37 | 38 | try { 39 | converter.parse(value); 40 | assert(false) 41 | } catch (e) { 42 | assert.deepEqual( 43 | e, { 44 | code: ERROR_CODE_PARSING, 45 | id: ERROR_INVALID_DATE, 46 | message: 'Invalid date' 47 | } 48 | ) 49 | } 50 | }) 51 | }) 52 | }); 53 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/stringDateOnly/dateUiType.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | /* 4 | * ███████████████████████████████████████████████████ 5 | * ████ FIELD_TYPE_STRING_DATE ► UI_TYPE_DATE ████ 6 | * ███████████████████████████████████████████████████ 7 | * 8 | * UI_TYPE_DATE has empty value => value !== EMPTY_FIELD_VALUE 9 | */ 10 | format: value => new Date(value), 11 | 12 | /* 13 | * ███████████████████████████████████████████████████ 14 | * ████ FIELD_TYPE_STRING_DATE ◄ UI_TYPE_DATE ████ 15 | * ███████████████████████████████████████████████████ 16 | */ 17 | parse: value => value.toISOString().slice(0, 10) 18 | }; 19 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/stringDateOnly/dateUiType.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import converter from './dateUiType'; 4 | 5 | describe('fieldTypes :: stringDateOnly <-> date', () => { 6 | it('should convert stringDateOnly to date', () => { 7 | const value = '2017-12-20'; 8 | const result = converter.format(value); 9 | expect(result.valueOf()).to.equal(new Date(value).valueOf()) 10 | }); 11 | 12 | it('should convert date to stringDateOnly ("YYYY-MM-DD" String)', () => { 13 | const date = '2017-12-20'; 14 | const value = new Date(date); 15 | const result = converter.parse(value); 16 | expect(result).to.equal(date) 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/stringDateOnly/index.js: -------------------------------------------------------------------------------- 1 | import dateUiType from './dateUiType'; 2 | import stringUiType from './stringUiType'; 3 | import { throwError } from '../lib'; 4 | import { 5 | CONSTRAINT_MIN, 6 | CONSTRAINT_MAX, 7 | 8 | EMPTY_FIELD_VALUE, 9 | 10 | ERROR_CODE_VALIDATION, 11 | 12 | ERROR_MIN_DECEEDED, 13 | ERROR_MAX_EXCEEDED, 14 | 15 | UI_TYPE_DATE, 16 | UI_TYPE_STRING 17 | } from '../../constants'; 18 | 19 | export default { 20 | 21 | isValid(value) { 22 | if (value === EMPTY_FIELD_VALUE) { 23 | return true; 24 | } 25 | 26 | if (typeof value !== 'string') { 27 | return false; 28 | } 29 | 30 | return (new Date(value) !== "Invalid Date") && !isNaN(new Date(value)); 31 | }, 32 | 33 | converter: { 34 | [UI_TYPE_DATE]: dateUiType, 35 | [UI_TYPE_STRING]: stringUiType 36 | }, 37 | 38 | 39 | buildValidator(origValue) { 40 | const value = new Date(origValue); 41 | 42 | return { 43 | 44 | /* 45 | * Specifies the minimum value allowed. 46 | * param is string. 47 | */ 48 | [CONSTRAINT_MIN]: param => value >= new Date(param) || throwError({ 49 | code: ERROR_CODE_VALIDATION, 50 | id: ERROR_MIN_DECEEDED, 51 | args: { 52 | min: param 53 | } 54 | }), 55 | 56 | /* 57 | * Specifies the maximum value allowed. 58 | * param is string. 59 | */ 60 | [CONSTRAINT_MAX]: param => value <= new Date(param) || throwError({ 61 | code: ERROR_CODE_VALIDATION, 62 | id: ERROR_MAX_EXCEEDED, 63 | args: { 64 | max: param 65 | } 66 | }) 67 | }; 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/stringDateOnly/stringUiType.js: -------------------------------------------------------------------------------- 1 | import { 2 | ERROR_CODE_PARSING, 3 | EMPTY_FIELD_VALUE, 4 | ERROR_INVALID_DATE 5 | } from '../../constants'; 6 | 7 | export default { 8 | 9 | /* 10 | * █████████████████████████████████████████████████████ 11 | * ████ FIELD_TYPE_STRING_DATE ► UI_TYPE_STRING ████ 12 | * █████████████████████████████████████████████████████ 13 | */ 14 | format: value => new Date(value).toString(), 15 | 16 | /* 17 | * █████████████████████████████████████████████████████ 18 | * ████ FIELD_TYPE_STRING_DATE ◄ UI_TYPE_STRING ████ 19 | * █████████████████████████████████████████████████████ 20 | */ 21 | parse: value => { 22 | if (!value.trim()) { 23 | return EMPTY_FIELD_VALUE; // Considering whitespaces-only strings to be empty value. 24 | } 25 | 26 | if ((new Date(value) === "Invalid Date") || isNaN(new Date(value))) { 27 | const error = { 28 | code: ERROR_CODE_PARSING, 29 | id: ERROR_INVALID_DATE, 30 | message: 'Invalid date' 31 | }; 32 | 33 | throw error; 34 | } 35 | 36 | return new Date(value).toISOString().slice(0, 10); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/stringDateOnly/stringUiType.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import assert from 'assert'; 3 | import { 4 | ERROR_CODE_PARSING, 5 | EMPTY_FIELD_VALUE, 6 | ERROR_INVALID_DATE 7 | } from '../../constants'; 8 | import converter from './stringUiType'; 9 | 10 | describe('fieldTypes :: stringDateOnly <-> string', () => { 11 | describe('format', () => { 12 | it('should convert stringified date to string', () => { 13 | const value = new Date().toISOString().slice(0, 10); 14 | const result = converter.format(value); 15 | expect(result).to.equal(new Date(value).toString()) 16 | }); 17 | }); 18 | 19 | describe('parse', () => { 20 | it('should convert empty string into null', () => { 21 | const value = ''; 22 | const result = converter.parse(value); 23 | 24 | expect(result).to.equal(EMPTY_FIELD_VALUE) 25 | }); 26 | 27 | it('should convert stringified date into stringDateOnly', () => { 28 | const date = new Date('1995-02-17T03:24:00') 29 | const value = date.toString(); 30 | const result = converter.parse(value); 31 | expect(result).to.equal('1995-02-17') 32 | }); 33 | 34 | it('should throw for not date-like string', () => { 35 | const value = 'ewqrwerew'; 36 | 37 | try { 38 | converter.parse(value); 39 | assert(false) 40 | } catch (e) { 41 | assert.deepEqual( 42 | e, { 43 | code: ERROR_CODE_PARSING, 44 | id: ERROR_INVALID_DATE, 45 | message: 'Invalid date' 46 | } 47 | ) 48 | } 49 | }) 50 | }) 51 | }); 52 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/stringDateRange/index.js: -------------------------------------------------------------------------------- 1 | import baseFieldType from '../stringDate'; 2 | import { buildRangeFieldType } from '../lib'; 3 | 4 | import { 5 | UI_TYPE_DATE_RANGE_OBJECT, 6 | UI_TYPE_STRING_RANGE_OBJECT, 7 | UI_TYPE_DATE, 8 | UI_TYPE_STRING 9 | } from '../../constants'; 10 | 11 | export default buildRangeFieldType(baseFieldType, { 12 | [UI_TYPE_DATE_RANGE_OBJECT]: UI_TYPE_DATE, 13 | [UI_TYPE_STRING_RANGE_OBJECT]: UI_TYPE_STRING 14 | }); 15 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/stringDecimal/decimalUiType.js: -------------------------------------------------------------------------------- 1 | import Big from 'big.js'; 2 | 3 | import { 4 | ERROR_CODE_FORMATING, 5 | ERROR_FORMAT, 6 | UI_TYPE_DECIMAL, 7 | } from '../../constants'; 8 | 9 | export default { 10 | 11 | /* 12 | * █████████████████████████████████████████████████████████ 13 | * ████ FIELD_TYPE_STRING_DECIMAL ► UI_TYPE_DECIMAL ████ 14 | * █████████████████████████████████████████████████████████ 15 | */ 16 | format: origValue => { 17 | const value = new Big(origValue); 18 | const n = Number(value); 19 | 20 | if (!value.eq(n)) { 21 | // ex. value is larger than Number.MAX_SAFE_INTEGER 22 | const error = { 23 | code: ERROR_CODE_FORMATING, 24 | id: ERROR_FORMAT, 25 | message: `Unable to convert to "${UI_TYPE_DECIMAL}" UI Type`, 26 | }; 27 | 28 | throw error; 29 | } 30 | 31 | return n; 32 | }, 33 | 34 | /* 35 | * █████████████████████████████████████████████████████████ 36 | * ████ FIELD_TYPE_STRING_DECIMAL ◄ UI_TYPE_DECIMAL ████ 37 | * █████████████████████████████████████████████████████████ 38 | */ 39 | parse: value => new Big(value).toString() 40 | }; 41 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/stringDecimal/index.js: -------------------------------------------------------------------------------- 1 | import Big from 'big.js'; 2 | 3 | import decimalUiType from './decimalUiType'; 4 | import stringUiType from './stringUiType'; 5 | import { throwError } from '../lib'; 6 | import { 7 | CONSTRAINT_MIN, 8 | CONSTRAINT_MAX, 9 | 10 | EMPTY_FIELD_VALUE, 11 | 12 | ERROR_CODE_VALIDATION, 13 | 14 | ERROR_MIN_DECEEDED, 15 | ERROR_MAX_EXCEEDED, 16 | 17 | UI_TYPE_DECIMAL, 18 | UI_TYPE_STRING 19 | } from '../../constants'; 20 | 21 | export default { 22 | 23 | isValid(value) { 24 | if (value === EMPTY_FIELD_VALUE) { 25 | return true; 26 | } 27 | 28 | if (typeof value !== 'string') { 29 | return false; 30 | } 31 | 32 | try { 33 | new Big(value); // eslint-disable-line no-new 34 | return true; 35 | } catch (_) { 36 | return false; 37 | } 38 | }, 39 | 40 | converter: { 41 | [UI_TYPE_DECIMAL]: decimalUiType, 42 | [UI_TYPE_STRING]: stringUiType 43 | }, 44 | 45 | 46 | buildValidator(origValue) { 47 | const value = new Big(origValue); 48 | 49 | return { 50 | 51 | /* 52 | * Specifies the minimum value allowed. 53 | * param is number|string|Big. 54 | */ 55 | [CONSTRAINT_MIN]: param => value.gte(param) || throwError({ 56 | code: ERROR_CODE_VALIDATION, 57 | id: ERROR_MIN_DECEEDED, 58 | args: { 59 | min: param 60 | } 61 | }), 62 | 63 | /* 64 | * Specifies the maximum value allowed. 65 | * param is number|string|Big. 66 | */ 67 | [CONSTRAINT_MAX]: param => value.lte(param) || throwError({ 68 | code: ERROR_CODE_VALIDATION, 69 | id: ERROR_MAX_EXCEEDED, 70 | args: { 71 | max: param 72 | } 73 | }) 74 | }; 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/stringDecimal/stringUiType.js: -------------------------------------------------------------------------------- 1 | import Big from 'big.js'; 2 | 3 | import { 4 | ERROR_CODE_PARSING, 5 | EMPTY_FIELD_VALUE, 6 | ERROR_INVALID_DECIMAL 7 | } from '../../constants'; 8 | 9 | export default { 10 | 11 | /* 12 | * ████████████████████████████████████████████████████████ 13 | * ████ FIELD_TYPE_STRING_DECIMAL ► UI_TYPE_STRING ████ 14 | * ████████████████████████████████████████████████████████ 15 | */ 16 | format: value => new Big(value).toString(), 17 | 18 | /* 19 | * ████████████████████████████████████████████████████████ 20 | * ████ FIELD_TYPE_STRING_DECIMAL ◄ UI_TYPE_STRING ████ 21 | * ████████████████████████████████████████████████████████ 22 | */ 23 | parse: value => { 24 | const optimized = value.trim(); 25 | 26 | if (!optimized) { 27 | return EMPTY_FIELD_VALUE; // Considering whitespaces-only strings to be empty value. 28 | } 29 | 30 | try { 31 | return new Big(optimized).toString(); 32 | } catch (_) { 33 | const error = { 34 | code: ERROR_CODE_PARSING, 35 | id: ERROR_INVALID_DECIMAL, 36 | message: 'Invalid decimal number' 37 | }; 38 | 39 | throw error; 40 | } 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/stringDecimalRange/index.js: -------------------------------------------------------------------------------- 1 | import baseFieldType from '../stringDecimal'; 2 | import { buildRangeFieldType } from '../lib'; 3 | 4 | import { 5 | UI_TYPE_DECIMAL_RANGE_OBJECT, 6 | UI_TYPE_STRING_RANGE_OBJECT, 7 | UI_TYPE_DECIMAL, 8 | UI_TYPE_STRING 9 | } from '../../constants'; 10 | 11 | export default buildRangeFieldType(baseFieldType, { 12 | [UI_TYPE_DECIMAL_RANGE_OBJECT]: UI_TYPE_DECIMAL, 13 | [UI_TYPE_STRING_RANGE_OBJECT]: UI_TYPE_STRING 14 | }); 15 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/stringInteger/index.js: -------------------------------------------------------------------------------- 1 | import Big from 'big.js'; 2 | 3 | import integerUiType from './integerUiType'; 4 | import stringUiType from './stringUiType'; 5 | import { throwError } from '../lib'; 6 | import { 7 | CONSTRAINT_MIN, 8 | CONSTRAINT_MAX, 9 | 10 | EMPTY_FIELD_VALUE, 11 | 12 | ERROR_CODE_VALIDATION, 13 | 14 | ERROR_MIN_DECEEDED, 15 | ERROR_MAX_EXCEEDED, 16 | 17 | UI_TYPE_INTEGER, 18 | UI_TYPE_STRING 19 | } from '../../constants'; 20 | 21 | export default { 22 | 23 | isValid(value) { 24 | if (value === EMPTY_FIELD_VALUE) { 25 | return true; 26 | } 27 | 28 | if (typeof value !== 'string') { 29 | return false; 30 | } 31 | 32 | let big; 33 | 34 | try { 35 | big = new Big(value); // eslint-disable-line no-new 36 | } catch (_) { 37 | return false; 38 | } 39 | 40 | return big.eq(big.round()); 41 | }, 42 | 43 | converter: { 44 | [UI_TYPE_INTEGER]: integerUiType, 45 | [UI_TYPE_STRING]: stringUiType 46 | }, 47 | 48 | 49 | buildValidator(origValue) { 50 | const value = new Big(origValue); 51 | 52 | return { 53 | 54 | /* 55 | * Specifies the minimum value allowed. 56 | * param is number|string|Big. 57 | */ 58 | [CONSTRAINT_MIN]: param => value.gte(param) || throwError({ 59 | code: ERROR_CODE_VALIDATION, 60 | id: ERROR_MIN_DECEEDED, 61 | args: { 62 | min: param 63 | } 64 | }), 65 | 66 | /* 67 | * Specifies the maximum value allowed. 68 | * param is number|string|Big. 69 | */ 70 | [CONSTRAINT_MAX]: param => value.lte(param) || throwError({ 71 | code: ERROR_CODE_VALIDATION, 72 | id: ERROR_MAX_EXCEEDED, 73 | args: { 74 | max: param 75 | } 76 | }) 77 | }; 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/stringInteger/integerUiType.js: -------------------------------------------------------------------------------- 1 | import Big from 'big.js'; 2 | 3 | import { 4 | ERROR_CODE_FORMATING, 5 | ERROR_FORMAT, 6 | UI_TYPE_INTEGER, 7 | } from '../../constants'; 8 | 9 | export default { 10 | 11 | /* 12 | * █████████████████████████████████████████████████████████ 13 | * ████ FIELD_TYPE_STRING_INTEGER ► UI_TYPE_INTEGER ████ 14 | * █████████████████████████████████████████████████████████ 15 | */ 16 | format: origValue => { 17 | const value = new Big(origValue); 18 | const n = Number(value); 19 | 20 | if (!value.eq(n)) { 21 | // ex. value is larger than Number.MAX_SAFE_INTEGER 22 | const error = { 23 | code: ERROR_CODE_FORMATING, 24 | id: ERROR_FORMAT, 25 | message: `Unable to convert to "${UI_TYPE_INTEGER}" UI Type`, 26 | }; 27 | 28 | throw error; 29 | } 30 | 31 | return n; 32 | }, 33 | 34 | /* 35 | * █████████████████████████████████████████████████████████ 36 | * ████ FIELD_TYPE_STRING_INTEGER ◄ UI_TYPE_INTEGER ████ 37 | * █████████████████████████████████████████████████████████ 38 | */ 39 | parse: value => new Big(value).toString() 40 | }; 41 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/stringInteger/stringUiType.js: -------------------------------------------------------------------------------- 1 | import Big from 'big.js'; 2 | 3 | import { 4 | ERROR_CODE_PARSING, 5 | EMPTY_FIELD_VALUE, 6 | ERROR_INVALID_INTEGER 7 | } from '../../constants'; 8 | 9 | export default { 10 | 11 | /* 12 | * ████████████████████████████████████████████████████████ 13 | * ████ FIELD_TYPE_STRING_INTEGER ► UI_TYPE_STRING ████ 14 | * ████████████████████████████████████████████████████████ 15 | */ 16 | format: value => new Big(value).toString(), 17 | 18 | /* 19 | * ████████████████████████████████████████████████████████ 20 | * ████ FIELD_TYPE_STRING_INTEGER ◄ UI_TYPE_STRING ████ 21 | * ████████████████████████████████████████████████████████ 22 | */ 23 | parse: value => { 24 | const optimized = value.trim(); 25 | 26 | if (!optimized) { 27 | return EMPTY_FIELD_VALUE; // Considering whitespaces-only strings to be empty value. 28 | } 29 | 30 | let big; 31 | 32 | try { 33 | big = new Big(optimized); 34 | } catch (_) { 35 | const error = { 36 | code: ERROR_CODE_PARSING, 37 | id: ERROR_INVALID_INTEGER, 38 | message: 'Invalid integer number' 39 | }; 40 | 41 | throw error; 42 | } 43 | 44 | if (!big.eq(big.round())) { 45 | const error = { 46 | code: ERROR_CODE_PARSING, 47 | id: ERROR_INVALID_INTEGER, 48 | message: 'Invalid integer number' 49 | }; 50 | 51 | throw error; 52 | } 53 | 54 | return big.toString(); 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/data-types-lib/fieldTypes/stringIntegerRange/index.js: -------------------------------------------------------------------------------- 1 | import baseFieldType from '../stringInteger'; 2 | import { buildRangeFieldType } from '../lib'; 3 | 4 | import { 5 | UI_TYPE_INTEGER_RANGE_OBJECT, 6 | UI_TYPE_STRING_RANGE_OBJECT, 7 | UI_TYPE_INTEGER, 8 | UI_TYPE_STRING 9 | } from '../../constants'; 10 | 11 | export default buildRangeFieldType(baseFieldType, { 12 | [UI_TYPE_INTEGER_RANGE_OBJECT]: UI_TYPE_INTEGER, 13 | [UI_TYPE_STRING_RANGE_OBJECT]: UI_TYPE_STRING 14 | }); 15 | -------------------------------------------------------------------------------- /src/data-types-lib/uiTypes/boolean.js: -------------------------------------------------------------------------------- 1 | const EMPTY_VALUE = null; 2 | 3 | export default { 4 | get EMPTY_VALUE() { 5 | return EMPTY_VALUE; 6 | }, 7 | 8 | isValid(value) { 9 | return value === EMPTY_VALUE || typeof value === 'boolean'; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/data-types-lib/uiTypes/date.js: -------------------------------------------------------------------------------- 1 | const EMPTY_VALUE = null; 2 | 3 | export default { 4 | get EMPTY_VALUE() { 5 | return EMPTY_VALUE; 6 | }, 7 | 8 | isValid(value) { 9 | return value === EMPTY_VALUE || value instanceof Date && !isNaN(value.getTime()); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/data-types-lib/uiTypes/dateRangeObject.js: -------------------------------------------------------------------------------- 1 | import baseUiType from './date'; 2 | import { buildRangeUiType } from './lib'; 3 | 4 | export default buildRangeUiType(baseUiType); 5 | -------------------------------------------------------------------------------- /src/data-types-lib/uiTypes/decimal.js: -------------------------------------------------------------------------------- 1 | const EMPTY_VALUE = null; 2 | 3 | export default { 4 | get EMPTY_VALUE() { 5 | return EMPTY_VALUE; 6 | }, 7 | 8 | isValid(value) { 9 | return value === EMPTY_VALUE || typeof value === 'number' && !isNaN(value); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/data-types-lib/uiTypes/decimalRangeObject.js: -------------------------------------------------------------------------------- 1 | import baseUiType from './decimal'; 2 | import { buildRangeUiType } from './lib'; 3 | 4 | export default buildRangeUiType(baseUiType); 5 | -------------------------------------------------------------------------------- /src/data-types-lib/uiTypes/index.js: -------------------------------------------------------------------------------- 1 | import booleanType from './boolean'; 2 | import dateType from './date'; 3 | import decimalType from './decimal'; 4 | import integerType from './integer'; 5 | import stringType from './string'; 6 | 7 | import dateRangeObjectType from './dateRangeObject'; 8 | import integerRangeObjectType from './integerRangeObject'; 9 | import decimalRangeObjecType from './decimalRangeObject'; 10 | import stringRangeObjectType from './stringRangeObject'; 11 | 12 | import { 13 | UI_TYPE_BOOLEAN, 14 | UI_TYPE_DATE, 15 | UI_TYPE_DECIMAL, 16 | UI_TYPE_INTEGER, 17 | UI_TYPE_STRING, 18 | 19 | UI_TYPE_DATE_RANGE_OBJECT, 20 | UI_TYPE_INTEGER_RANGE_OBJECT, 21 | UI_TYPE_DECIMAL_RANGE_OBJECT, 22 | UI_TYPE_STRING_RANGE_OBJECT 23 | } from '../constants'; 24 | 25 | /* 26 | * Values are objects with the following methods: 27 | * 28 | * isValid(value) 29 | * Return boolean whether input value is indeed of specified UI Type. 30 | * 31 | * EMPTY_VALUE constant getter. 32 | */ 33 | export default { 34 | [UI_TYPE_BOOLEAN]: booleanType, 35 | [UI_TYPE_DATE]: dateType, 36 | [UI_TYPE_DECIMAL]: decimalType, 37 | [UI_TYPE_INTEGER]: integerType, 38 | [UI_TYPE_STRING]: stringType, 39 | 40 | [UI_TYPE_DATE_RANGE_OBJECT]: dateRangeObjectType, 41 | [UI_TYPE_INTEGER_RANGE_OBJECT]: integerRangeObjectType, 42 | [UI_TYPE_DECIMAL_RANGE_OBJECT]: decimalRangeObjecType, 43 | [UI_TYPE_STRING_RANGE_OBJECT]: stringRangeObjectType 44 | }; 45 | -------------------------------------------------------------------------------- /src/data-types-lib/uiTypes/integer.js: -------------------------------------------------------------------------------- 1 | const EMPTY_VALUE = null; 2 | 3 | export default { 4 | get EMPTY_VALUE() { 5 | return EMPTY_VALUE; 6 | }, 7 | 8 | isValid(value) { 9 | return value === EMPTY_VALUE || typeof value === 'number' && !isNaN(value) && value === Math.floor(value); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/data-types-lib/uiTypes/integerRangeObject.js: -------------------------------------------------------------------------------- 1 | import baseUiType from './integer'; 2 | import { buildRangeUiType } from './lib'; 3 | 4 | export default buildRangeUiType(baseUiType); 5 | -------------------------------------------------------------------------------- /src/data-types-lib/uiTypes/lib.js: -------------------------------------------------------------------------------- 1 | import isEqual from 'lodash/isEqual'; 2 | 3 | export const buildRangeUiType = /* istanbul ignore next */ baseUiType => ({ 4 | get EMPTY_VALUE() { 5 | // return baseUiType.EMPTY_VALUE; 6 | return { 7 | from: baseUiType.EMPTY_VALUE, 8 | to: baseUiType.EMPTY_VALUE 9 | } 10 | }, 11 | 12 | isValid(value) { 13 | if (isEqual(value, baseUiType.EMPTY_VALUE)) { 14 | return true; 15 | } 16 | 17 | if (typeof value !== 'object' || !value) { 18 | return false; 19 | } 20 | 21 | if (Object.keys(value).length === 0) { 22 | return true; 23 | } 24 | 25 | if (Object.keys(value).length === 1) { 26 | if (!value.hasOwnProperty('from') && !value.hasOwnProperty('to')) { 27 | return false; 28 | } 29 | 30 | return baseUiType.isValid(value.hasOwnProperty('from') && value.from || value.hasOwnProperty('to') && value.to); 31 | } 32 | 33 | if (Object.keys(value).length === 2) { 34 | if (!value.hasOwnProperty('from') || !value.hasOwnProperty('to')) { 35 | return false; 36 | } 37 | 38 | return baseUiType.isValid(value.from) && baseUiType.isValid(value.to); 39 | } 40 | 41 | return false; 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /src/data-types-lib/uiTypes/string.js: -------------------------------------------------------------------------------- 1 | const EMPTY_VALUE = ''; 2 | 3 | export default { 4 | get EMPTY_VALUE() { 5 | return EMPTY_VALUE; 6 | }, 7 | 8 | isValid(value) { 9 | return typeof value === 'string'; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/data-types-lib/uiTypes/stringRangeObject.js: -------------------------------------------------------------------------------- 1 | import baseUiType from './string'; 2 | import { buildRangeUiType } from './lib'; 3 | 4 | export default buildRangeUiType(baseUiType); 5 | -------------------------------------------------------------------------------- /src/demo/client/components/Home/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | export default () => 5 | (
6 |

Home-Sweet-Home

7 |
8 | 9 | Contracts 10 | 11 |
); 12 | -------------------------------------------------------------------------------- /src/demo/client/components/Revisions/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | const Revisions = ({ 6 | match: { 7 | params: { 8 | entityId 9 | } 10 | } 11 | }) => 12 | (
13 |

Revisions for {entityId}

14 |
15 | 16 | Back to Contracts 17 | 18 |
); 19 | 20 | Revisions.propTypes = { 21 | match: PropTypes.shape({ 22 | params: PropTypes.shape({ 23 | entityId: PropTypes.string 24 | }) 25 | }) 26 | } 27 | 28 | export default Revisions; 29 | -------------------------------------------------------------------------------- /src/demo/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Simple CRUD Editor 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 |
13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /src/demo/client/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Home from './components/Home'; 3 | import CrudWrapper from './components/CrudWrapper'; 4 | import Revisions from './components/Revisions'; 5 | 6 | import { 7 | BrowserRouter as Router, 8 | Route, 9 | Switch, 10 | Redirect 11 | } from 'react-router-dom'; 12 | 13 | const baseUrl = '/'; 14 | 15 | export default () => ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | -------------------------------------------------------------------------------- /src/demo/global-styles.less: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: transparent; 3 | } 4 | 5 | .oc-menu { 6 | margin-bottom: 0; 7 | } 8 | 9 | // fixing Date input styles issue 10 | .opuscapita_date-range-input input[type="text"], .opuscapita_date-range-input input[type="text"]:focus { 11 | box-shadow: none !important; 12 | padding: initial; 13 | } 14 | 15 | .opuscapita_date-input input[type="text"], .opuscapita_date-input input[type="text"]:focus { 16 | box-shadow: none !important; 17 | padding: initial; 18 | } -------------------------------------------------------------------------------- /src/demo/models/contracts/components/ContractReferenceSearch/ReferenceSearchService.js: -------------------------------------------------------------------------------- 1 | import { getContracts } from '../../api/api'; 2 | 3 | export default class ReferenceSearchService { 4 | getData({ contractId, max = 10, offset = 0 }) { 5 | const contractIds = getContracts().map(({ contractId }) => contractId).sort(); 6 | 7 | let result = contractId ? 8 | contractIds.filter(cid => cid.toLowerCase().includes(contractId.toLowerCase())) : 9 | contractIds; 10 | 11 | const items = result. 12 | slice(offset, offset + max). 13 | map(contractId => ({ contractId })) 14 | 15 | return new Promise(resolve => setTimeout(_ => resolve({ 16 | body: items, 17 | headers: { 18 | "content-range": `items ${offset}-${offset + max - 1}/${result.length}` 19 | } 20 | }), 300)) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/demo/models/contracts/components/ContractReferenceSearch/i18n/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'ContractReferenceSearch.dialogTitle': 'Search for parent id', 3 | } 4 | -------------------------------------------------------------------------------- /src/demo/models/contracts/components/ContractReferenceSearch/i18n/index.js: -------------------------------------------------------------------------------- 1 | import en from './en'; 2 | 3 | export default { 4 | en 5 | } 6 | -------------------------------------------------------------------------------- /src/demo/models/contracts/components/CustomSpinner.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default _ => () 4 | -------------------------------------------------------------------------------- /src/demo/models/contracts/components/CustomTabComponent/index.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import createCrud from '../../../../../crudeditor-lib'; 4 | import secondModel from '../../../second-model'; 5 | 6 | export default class CustomTabComponent extends PureComponent { 7 | static propTypes = { 8 | viewName: PropTypes.string.isRequired, 9 | instance: PropTypes.object.isRequired 10 | } 11 | 12 | constructor(...args) { 13 | super(...args); 14 | 15 | this._secondCrud = createCrud(secondModel) 16 | } 17 | 18 | handleTransition = state => { 19 | this._lastState = state 20 | }; 21 | 22 | render() { 23 | const SecondCrud = this._secondCrud; 24 | 25 | return ( 26 | 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/demo/models/contracts/components/DateRangeCellRender/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const date2str = (date, i18n) => i18n.formatDate(typeof date === 'string' ? 5 | new Date(date) : 6 | date 7 | ); 8 | 9 | const DateRangeCellRender = ({ name, instance }, { i18n }) => { 10 | const value = instance[name]; 11 | 12 | return value ? 13 | ( 14 | 15 | { 16 | `${value.from ? date2str(value.from, i18n) : '...'} - ${value.to ? date2str(value.to, i18n) : '...'}` 17 | } 18 | 19 | ) : 20 | null 21 | }; 22 | 23 | DateRangeCellRender.propTypes = { 24 | name: PropTypes.string.isRequired, 25 | instance: PropTypes.object.isRequired 26 | } 27 | 28 | DateRangeCellRender.contextTypes = { 29 | i18n: PropTypes.object.isRequired 30 | } 31 | 32 | export default DateRangeCellRender; 33 | -------------------------------------------------------------------------------- /src/demo/models/contracts/i18n/de.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "model.name": "Kontrakte", 3 | 4 | "model.tab.additional.label": "Zusätzlich", 5 | 6 | "model.section.test.label": "Meine Teststrecke" 7 | } 8 | -------------------------------------------------------------------------------- /src/demo/models/contracts/i18n/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "model.name": "Contracts", 3 | "model.tab.general.label": "General", 4 | "model.tab.additional.label": "Additional", 5 | 6 | "model.section.test.label": "My test section", 7 | 8 | "model.field.contractId.tooltip": "Unique contract identifier.", 9 | 10 | "model.field.testNumberTypeField.label": "Test Number Type Field", 11 | "model.field.contractBoilerplates.label": "Contract Boilerplates", 12 | "model.field.hierarchyCode.label": "Hierarchy Code", 13 | "model.field.termsOfPaymentId.label": "Terms Of Payment Id", 14 | 15 | "model.field.description.label": "Description", 16 | "model.field.description.hint": "Provide a detailed description for this contract.", 17 | "model.field.description.tooltip": "Arbitrary description of your choice.", 18 | "model.field.description.error.forbiddenWord": "Description may not contain `{forbiddenWord}`", 19 | 20 | "model.field.statusId.hint": "Refer to a secret book of status codes for details.", 21 | 22 | "model.field.termsOfDeliveryId.label": "Terms Of Delivery Id", 23 | "model.field.freeShippingBoundary.label": "Free Shipping Boundary", 24 | "model.field.createdOn.label": "Created On", 25 | "model.field.changedOn.label": "Changed On", 26 | "model.field.contractedCatalogs.label": "Contracted Catalogs", 27 | "model.field.minOrderValueRequired.label": "Min Order Value Required", 28 | "model.field.contractedClassificationGroups.label": "contractedClassificationGroups", 29 | "model.field.extContractId.label": "Ext Contract Id", 30 | "model.field.children.label": "children", 31 | "model.field.changedBy.label": "Changed By", 32 | "model.field.usages.label": "usages", 33 | "model.field.currencyId.label": "currencyId", 34 | 35 | "model.label.createChild": "Create child", 36 | 37 | "model.error.requiredFieldMissing": "Instance validation error for [ {contractId} ] in your language!", 38 | "model.error.seminalDeletionAttempt": "Seminal-contracts must not be deleted" 39 | } 40 | -------------------------------------------------------------------------------- /src/demo/models/contracts/i18n/index.js: -------------------------------------------------------------------------------- 1 | import en from './en'; 2 | import de from './de'; 3 | import ru from './ru'; 4 | 5 | // this data is dummy, 6 | // suited ONLY FOR TESTING PURPOSES 7 | // and should be replaced 8 | 9 | export default { 10 | en, 11 | de, 12 | ru 13 | } 14 | -------------------------------------------------------------------------------- /src/demo/models/contracts/i18n/ru.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "model.name": "Контракты", 3 | "model.tab.general.label": "Главное", 4 | "model.tab.additional.label": "Дополнительно", 5 | 6 | "model.section.order.label": "Параметры заказа", 7 | "model.section.test.label": "Тестовое поле", 8 | "model.section.auditable.label": "Проверяемые поля", 9 | 10 | "model.field.contractId.label": "Номер контракта", 11 | "model.field.description.label": "Описание", 12 | "model.field.validRange.label": "Период действия", 13 | "model.field.testNumberTypeField.label": "Тестовое числовое поле", 14 | "model.field.createdOn.label": "Время создания", 15 | "model.field.changedOn.label": "Время изменения", 16 | "model.field.changedBy.label": "Кем изменен", 17 | "model.field.createdBy.label": "Кто создал", 18 | "model.error.seminalDeletionAttempt": "Запрещено удалять seminal-конткракты" 19 | } 20 | -------------------------------------------------------------------------------- /src/demo/models/contracts/index.spec.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { expect } from 'chai'; 3 | import contracts from "./"; 4 | 5 | describe("Models / Contracts", _ => { 6 | describe("ui.create.defaultNewInstance", _ => { 7 | it("should return instance with predefined fields from the passed filter", () => { 8 | const filter = { 9 | contractId: "YYYYYYYYYYY" 10 | }; 11 | const result = contracts.ui.create.defaultNewInstance({ filter }); 12 | 13 | assert.deepEqual( 14 | result, 15 | filter 16 | ) 17 | }); 18 | }); 19 | 20 | describe("ui.instanceLabel", _ => { 21 | it("should return empty string if instanceLabel is not specified for the instance", () => { 22 | const instance = {}; 23 | const result = contracts.ui.instanceLabel(instance) 24 | assert.strictEqual(result, '') 25 | }); 26 | }); 27 | 28 | describe("model.validate", _ => { 29 | it("should check for a required minOrderValue", () => { 30 | const instance = { 31 | minOrderValueRequired: true, 32 | minOrderValue: 100 33 | }; 34 | const result = contracts.model.validate({ formInstance: instance }); 35 | expect(result).to.be.true; // eslint-disable-line no-unused-expressions 36 | }); 37 | 38 | it("should throw if a required minOrderValue is absent", () => { 39 | const instance = { 40 | contractId: 'myCoolId', 41 | minOrderValueRequired: true, 42 | minOrderValue: null 43 | }; 44 | try { 45 | const result = contracts.model.validate({ formInstance: instance }); 46 | assert.fail(result) 47 | } catch (e) { 48 | assert.deepEqual( 49 | e, 50 | [{ 51 | code: 400, 52 | id: 'requiredFieldMissing', 53 | message: 'minOrderValue must be set when minOrderValueRequired is true', 54 | args: { 55 | contractId: instance.contractId 56 | } 57 | }] 58 | ) 59 | } 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/demo/models/index.js: -------------------------------------------------------------------------------- 1 | import contracts from './contracts'; 2 | 3 | export default name => ({ 4 | contracts 5 | })[name]; 6 | -------------------------------------------------------------------------------- /src/demo/models/second-model/components/CustomTabComponent/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const CustomTabComponent = /* istanbul ignore next */ ({ viewName, instance }) => ( 5 |
6 |

Custom Tab Component Example

7 |

Click me for Documentation reference

8 |

props.viewName: {viewName}

9 |

props.instance:

10 |
    11 | { 12 | Object.keys(instance).map(key =>
  • {`${key}: ${JSON.stringify(instance[key])}`}
  • ) 13 | } 14 |
15 |
16 | ) 17 | 18 | CustomTabComponent.propTypes = { 19 | viewName: PropTypes.string.isRequired, 20 | instance: PropTypes.object.isRequired 21 | } 22 | 23 | export default CustomTabComponent; 24 | -------------------------------------------------------------------------------- /src/demo/models/second-model/i18n/de.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "model.name": "Kontrakte", 3 | 4 | "model.tab.additional.label": "Zusätzlich", 5 | 6 | "model.section.test.label": "Meine Teststrecke" 7 | } 8 | -------------------------------------------------------------------------------- /src/demo/models/second-model/i18n/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "model.name": "Contracts", 3 | "model.tab.general.label": "General", 4 | "model.tab.additional.label": "Additional", 5 | 6 | "model.section.test.label": "My test section", 7 | 8 | "model.field.testNumberTypeField.label": "Test Number Type Field", 9 | "model.field.contractBoilerplates.label": "Contract Boilerplates", 10 | "model.field.hierarchyCode.label": "Hierarchy Code", 11 | "model.field.termsOfPaymentId.label": "Terms Of Payment Id", 12 | "model.field.description.label": "Description", 13 | 14 | "model.field.description.error.forbiddenWord": "Description may not contain `{forbiddenWord}`", 15 | 16 | "model.field.termsOfDeliveryId.label": "Terms Of Delivery Id", 17 | "model.field.freeShippingBoundary.label": "Free Shipping Boundary", 18 | "model.field.createdOn.label": "Created On", 19 | "model.field.changedOn.label": "Changed On", 20 | "model.field.contractedCatalogs.label": "Contracted Catalogs", 21 | "model.field.minOrderValueRequired.label": "Min Order Value Required", 22 | "model.field.contractedClassificationGroups.label": "contractedClassificationGroups", 23 | "model.field.extContractId.label": "Ext Contract Id", 24 | "model.field.children.label": "children", 25 | "model.field.changedBy.label": "Changed By", 26 | "model.field.usages.label": "usages", 27 | "model.field.currencyId.label": "currencyId", 28 | 29 | "model.label.createChild": "Create child" 30 | } 31 | -------------------------------------------------------------------------------- /src/demo/models/second-model/i18n/index.js: -------------------------------------------------------------------------------- 1 | import en from './en'; 2 | import de from './de'; 3 | import ru from './ru'; 4 | 5 | // this data is dummy, 6 | // suited ONLY FOR TESTING PURPOSES 7 | // and should be replaced 8 | 9 | export default { 10 | en, 11 | de, 12 | ru 13 | } 14 | -------------------------------------------------------------------------------- /src/demo/models/second-model/i18n/ru.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "model.name": "Контракты", 3 | "model.tab.general.label": "Главное", 4 | "model.tab.additional.label": "Дополнительно", 5 | 6 | "model.section.order.label": "Параметры заказа", 7 | "model.section.test.label": "Тестовое поле", 8 | "model.section.auditable.label": "Проверяемые поля", 9 | 10 | "model.field.contractId.label": "Номер контракта", 11 | "model.field.description.label": "Описание", 12 | "model.field.validRange.label": "Период действия", 13 | "model.field.testNumberTypeField.label": "Тестовое числовое поле", 14 | "model.field.createdOn.label": "Время создания", 15 | "model.field.changedOn.label": "Время изменения", 16 | "model.field.changedBy.label": "Кем изменен", 17 | "model.field.createdBy.label": "Кто создал" 18 | } 19 | -------------------------------------------------------------------------------- /src/demo/showroom/ContractEditor.SCOPE.react.js: -------------------------------------------------------------------------------- 1 | import 'core-js/es6/promise'; 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { showroomScopeDecorator } from '@opuscapita/react-showroom-client'; 5 | import { I18nManager } from '@opuscapita/i18n' 6 | // import './ContractEditorScope.less' 7 | 8 | function getParameterByName(name, url) { 9 | if (!url) {url = window.location.href;} // eslint-disable-line no-param-reassign 10 | name = name.replace(/[\[\]]/g, "\\$&"); // eslint-disable-line no-param-reassign 11 | let regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), 12 | results = regex.exec(url); 13 | if (!results) {return null;} 14 | if (!results[2]) {return '';} 15 | return decodeURIComponent(results[2].replace(/\+/g, " ")); 16 | } 17 | 18 | // This @showroomScopeDecorator modify React.Component prototype by adding _renderChildren() method. 19 | export default 20 | @showroomScopeDecorator 21 | class ContractEditorScope extends React.Component { 22 | static childContextTypes = { 23 | i18n: PropTypes.object 24 | }; 25 | 26 | constructor(...args) { 27 | super(...args); 28 | 29 | // check for URL query parameter 'lang', otherwise use default language 30 | this.i18n = new I18nManager({ locale: getParameterByName('lang') || 'en' }); 31 | } 32 | 33 | getChildContext() { 34 | return { i18n: this.i18n } 35 | } 36 | 37 | render() { 38 | return ( 39 |
40 | {this._renderChildren()} 41 |
42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/demo/showroom/ContractEditor.react.js: -------------------------------------------------------------------------------- 1 | import '../global-styles.less'; 2 | import buildModel from '../models'; 3 | import createCrud from '../../crudeditor-lib'; 4 | 5 | const model = buildModel('contracts'); 6 | 7 | const CrudEditor = createCrud(model); 8 | 9 | export default CrudEditor; 10 | -------------------------------------------------------------------------------- /src/demo/showroom/ContractEditorScope.less: -------------------------------------------------------------------------------- 1 | // ie11 patch 2 | .contract-editor-scope > div > div > div > div { 3 | height: 70vh; 4 | } -------------------------------------------------------------------------------- /www/index-page.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Showroom from '@opuscapita/react-showroom-client'; 4 | 5 | let element = document.getElementById('main'); 6 | let showroom = React.createElement(Showroom, { 7 | loaderOptions: { 8 | componentsInfo: require('.opuscapita-showroom/componentsInfo'), 9 | packagesInfo: require('.opuscapita-showroom/packageInfo') 10 | } 11 | }); 12 | 13 | ReactDOM.render(showroom, element); 14 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React CrudEditor (Showroom) 7 | 8 | 9 | 10 | 11 |
12 | 13 | --------------------------------------------------------------------------------