├── .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 |
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 |
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 | Column 1 |
27 | Column 2 |
28 | Column 3 |
29 | Column 4 |
30 |
31 |
32 |
33 |
34 | Value 1 1 |
35 | Value 1 2 |
36 | Value 1 3 |
37 | Value 1 4 |
38 |
39 |
40 | Value 2 1 |
41 | Value 2 2 |
42 | Value 2 3 |
43 | Value 2 4 |
44 |
45 |
46 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------