├── .editorconfig ├── .env.dist ├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ ├── coding_standards.yml │ └── frontend.yml ├── .gitignore ├── .php-cs-fixer.php ├── LICENSE ├── README.md ├── bundle ├── DependencyInjection │ └── NetgenContentBrowserUIExtension.php ├── NetgenContentBrowserUIBundle.php └── Resources │ ├── config │ └── framework │ │ └── assets.yaml │ └── public │ ├── css │ └── main.css │ ├── js │ └── main.js │ └── media │ ├── Roboto-300.eot │ ├── Roboto-300.svg │ ├── Roboto-300.ttf │ ├── Roboto-300.woff │ ├── Roboto-300.woff2 │ ├── Roboto-500.eot │ ├── Roboto-500.svg │ ├── Roboto-500.ttf │ ├── Roboto-500.woff │ ├── Roboto-500.woff2 │ ├── Roboto-700.eot │ ├── Roboto-700.svg │ ├── Roboto-700.ttf │ ├── Roboto-700.woff │ ├── Roboto-700.woff2 │ ├── Roboto-regular.eot │ ├── Roboto-regular.svg │ ├── Roboto-regular.ttf │ ├── Roboto-regular.woff │ └── Roboto-regular.woff2 ├── composer.json ├── config ├── env.js ├── paths.js ├── webpack.config.js └── webpackDevServer.config.js ├── cypress.json ├── cypress ├── integration │ └── browser_test.js ├── plugins │ └── index.js ├── support │ ├── commands.js │ └── index.js └── utils.js ├── express ├── data │ ├── config.json │ └── items.json ├── helpers.js ├── public │ └── index.html └── server.js ├── package.json ├── public └── index.html ├── scripts ├── build.js └── start.js └── src ├── App.js ├── App.module.css ├── assets └── fonts │ └── Roboto │ ├── Roboto-300.eot │ ├── Roboto-300.svg │ ├── Roboto-300.ttf │ ├── Roboto-300.woff │ ├── Roboto-300.woff2 │ ├── Roboto-500.eot │ ├── Roboto-500.svg │ ├── Roboto-500.ttf │ ├── Roboto-500.woff │ ├── Roboto-500.woff2 │ ├── Roboto-700.eot │ ├── Roboto-700.svg │ ├── Roboto-700.ttf │ ├── Roboto-700.woff │ ├── Roboto-700.woff2 │ ├── Roboto-regular.eot │ ├── Roboto-regular.svg │ ├── Roboto-regular.ttf │ ├── Roboto-regular.woff │ └── Roboto-regular.woff2 ├── components ├── Breadcrumbs.js ├── Breadcrumbs.module.css ├── Browser.js ├── Browser.module.css ├── Footer.js ├── Footer.module.css ├── FooterItem.js ├── Item.js ├── Items.js ├── Items.module.css ├── ItemsTable.js ├── ItemsTable.module.css ├── Preview.js ├── Preview.module.css ├── Search.js ├── Search.module.css ├── Tab.js ├── TableSettings.js ├── Tabs.js ├── Tabs.module.css ├── TogglePreview.js ├── Tree.js ├── Tree.module.css ├── TreeItem.js ├── TreeItems.js ├── Variables.module.css └── utils │ ├── Button.js │ ├── Button.module.css │ ├── Checkbox.js │ ├── Checkbox.module.css │ ├── Dropdown.js │ ├── Dropdown.module.css │ ├── Input.js │ ├── Input.module.css │ ├── Loader.js │ ├── Loader.module.css │ ├── Pager.js │ ├── Pager.module.css │ ├── Select.js │ ├── Select.module.css │ ├── Toggle.js │ └── Toggle.module.css ├── containers ├── Browser.js ├── Items.js ├── Search.js ├── SearchItems.js └── Tree.js ├── fonts.css ├── helpers └── index.js ├── index.css ├── index.js ├── plugins ├── Browser.js ├── InputBrowse.js └── MultipleBrowse.js ├── setupProxy.js └── store ├── actionTypes.js ├── actions ├── app.js ├── items.js └── search.js ├── reducers ├── app.js ├── index.js ├── items.js └── search.js └── store.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.env.dist: -------------------------------------------------------------------------------- 1 | SITE_URL=http://localhost:8282 2 | PORT=8181 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "react-app" 4 | } 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /bundle/Resources/public/js/*.js binary 2 | /bundle/Resources/public/css/*.css binary 3 | -------------------------------------------------------------------------------- /.github/workflows/coding_standards.yml: -------------------------------------------------------------------------------- 1 | name: Coding standards 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | - '[0-9].[0-9]+' 8 | pull_request: ~ 9 | 10 | jobs: 11 | php-cs-fixer: 12 | name: PHP CS Fixer 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/checkout@v3 18 | with: 19 | repository: netgen-layouts/layouts-coding-standard 20 | path: vendor/netgen/layouts-coding-standard 21 | - uses: docker://oskarstark/php-cs-fixer-ga 22 | with: 23 | args: --diff --dry-run 24 | -------------------------------------------------------------------------------- /.github/workflows/frontend.yml: -------------------------------------------------------------------------------- 1 | name: Frontend 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | - '[0-9].[0-9]+' 8 | pull_request: ~ 9 | 10 | jobs: 11 | frontend: 12 | name: ${{ matrix.script }} 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | script: ['build', 'ci'] 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - uses: actions/setup-node@v3 23 | with: 24 | node-version: '14' 25 | - uses: shivammathur/setup-php@v2 26 | with: 27 | php-version: '8.1' 28 | coverage: none 29 | 30 | # Install Flex as a global dependency to enable usage of extra.symfony.require 31 | # while keeping Flex recipes from applying 32 | - run: composer global config --no-plugins allow-plugins.symfony/flex true 33 | - run: composer global require --no-scripts symfony/flex 34 | 35 | - run: composer config extra.symfony.require ~6.4.0 36 | 37 | - run: composer update --prefer-dist 38 | 39 | - run: yarn install 40 | - run: cp .env.dist .env 41 | 42 | - run: yarn run ${{ matrix.script }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .env 3 | cypress/screenshots/ 4 | cypress/fixtures/ 5 | node_modules/ 6 | vendor/ 7 | .php-cs-fixer.cache 8 | composer.lock 9 | composer.phar 10 | package-lock.json 11 | studio.json 12 | yarn.lock 13 | yarn-error.log 14 | *.sublime-workspace 15 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | setFinder( 8 | PhpCsFixer\Finder::create() 9 | ->exclude(['vendor', 'node_modules']) 10 | ->in(__DIR__) 11 | ) 12 | ; 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2019 Netgen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Netgen Content Browser user interface 2 | 3 | This repository contains the user interface for Netgen Content Browser. 4 | 5 | ## Requirements 6 | 7 | - [Node.js](https://nodejs.org) 8 | - [Yarn](https://yarnpkg.com) 9 | 10 | ## Development 11 | 12 | After cloning the repository, install the dependencies with: 13 | 14 | ```bash 15 | $ yarn install 16 | ``` 17 | 18 | ## First time build configuration 19 | 20 | Before building the project for the first time, you need to copy `.env.dist` file to `.env`. This file specifies 21 | basic configuration for development and running the tests. 22 | 23 | ## Starting the development server 24 | 25 | The app uses a mock API by default, provided by the included Express server, which you need to start before starting 26 | the Webpack development server: 27 | 28 | ```bash 29 | # Starts the Express server 30 | $ yarn express 31 | 32 | # Starts the Webpack development server 33 | $ yarn start 34 | ``` 35 | 36 | You can now access the app at `http://localhost:8181`. Webpack watches for changes in files and automatically 37 | refreshes the app. 38 | 39 | If you want to use real data from your backend CMS for development of Content Browser, you need to change 40 | the `SITE_URL` parameter inside `.env` file to proxy all API requests to your site. 41 | 42 | In that case, you don't need to start the Express server. Run only the following: 43 | 44 | ```bash 45 | # Starts the Webpack development server 46 | $ yarn start 47 | ``` 48 | 49 | ## Build 50 | 51 | To build the production assets run the following: 52 | 53 | ```bash 54 | $ yarn build 55 | ``` 56 | 57 | This will build the app and place all generated assets into `bundle/Resources/public` folder. 58 | 59 | ## Tests 60 | 61 | For end-to-end testing this repo uses [Cypress](https://www.cypress.io). Tests are mostly written 62 | for test data so Express server needs to be started before running them. To run the tests continuously 63 | in Google Chrome while developing, start Cypress with: 64 | 65 | ```bash 66 | $ yarn cypress 67 | ``` 68 | 69 | This opens a window where you can click on `browser_test.js` which opens its own Google Chrome window 70 | and runs the tests. Tests are automatically ran whenever the app updates (on every file change). 71 | 72 | When ran standalone, tests use the production build of the app: 73 | 74 | ```bash 75 | $ yarn ci 76 | ``` 77 | 78 | This starts the Express server and runs the tests in a headless browser. 79 | -------------------------------------------------------------------------------- /bundle/DependencyInjection/NetgenContentBrowserUIExtension.php: -------------------------------------------------------------------------------- 1 | setParameter( 23 | 'netgen_content_browser.asset.version', 24 | PrettyVersions::getVersion('netgen/content-browser-ui')->getShortReference(), 25 | ); 26 | 27 | $prependConfigs = [ 28 | 'framework/assets.yaml' => 'framework', 29 | ]; 30 | 31 | foreach ($prependConfigs as $configFile => $prependConfig) { 32 | $configFile = __DIR__ . '/../Resources/config/' . $configFile; 33 | $config = Yaml::parse((string) file_get_contents($configFile)); 34 | $container->prependExtensionConfig($prependConfig, $config); 35 | $container->addResource(new FileResource($configFile)); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /bundle/NetgenContentBrowserUIBundle.php: -------------------------------------------------------------------------------- 1 | *{display:flex;align-items:center}.js-multiple-browse .js-remove{padding:0 3px;margin:0 1px 0 0;text-decoration:none}.js-multiple-browse .js-remove i{font-size:1.1666666667em;float:left;line-height:inherit}.js-multiple-browse .item .name{padding:4px 6px}.js-multiple-browse .no-items{padding:4px 0;display:none}.js-multiple-browse.items-empty .no-items{display:block}@font-face{font-family:Roboto;font-weight:300;font-style:normal;src:url(../media/Roboto-300.eot);src:url(../media/Roboto-300.eot?#iefix) format("embedded-opentype"),local("Roboto Light"),local("Roboto-300"),url(../media/Roboto-300.woff2) format("woff2"),url(../media/Roboto-300.woff) format("woff"),url(../media/Roboto-300.ttf) format("truetype"),url(../media/Roboto-300.svg#Roboto) format("svg")}@font-face{font-family:Roboto;font-weight:400;font-style:normal;src:url(../media/Roboto-regular.eot);src:url(../media/Roboto-regular.eot?#iefix) format("embedded-opentype"),local("Roboto"),local("Roboto-regular"),url(../media/Roboto-regular.woff2) format("woff2"),url(../media/Roboto-regular.woff) format("woff"),url(../media/Roboto-regular.ttf) format("truetype"),url(../media/Roboto-regular.svg#Roboto) format("svg")}@font-face{font-family:Roboto;font-weight:500;font-style:normal;src:url(../media/Roboto-500.eot);src:url(../media/Roboto-500.eot?#iefix) format("embedded-opentype"),local("Roboto Medium"),local("Roboto-500"),url(../media/Roboto-500.woff2) format("woff2"),url(../media/Roboto-500.woff) format("woff"),url(../media/Roboto-500.ttf) format("truetype"),url(../media/Roboto-500.svg#Roboto) format("svg")}@font-face{font-family:Roboto;font-weight:700;font-style:normal;src:url(../media/Roboto-700.eot);src:url(../media/Roboto-700.eot?#iefix) format("embedded-opentype"),local("Roboto Bold"),local("Roboto-700"),url(../media/Roboto-700.woff2) format("woff2"),url(../media/Roboto-700.woff) format("woff"),url(../media/Roboto-700.ttf) format("truetype"),url(../media/Roboto-700.svg#Roboto) format("svg")}.Button_button__1pfPt{cursor:pointer;font-family:Roboto,Helvetica Neue,sans-serif;font-weight:400;display:inline-flex;align-items:center;background-image:none;text-align:center;white-space:nowrap;vertical-align:middle;touch-action:manipulation;border:1px solid transparent;padding:.5em 1em;font-size:.875em;line-height:1.5;border-radius:2px;-webkit-user-select:none;user-select:none;transition:background-color .25s,color .25s;color:#333;height:2.6428571429em}.Button_button__1pfPt:focus{outline:none}.Button_prefixed__jvfYb{border-radius:0 2px 2px 0}.Button_sufixed__3QLiS{border-radius:2px 0 0 2px}.Button_button__1pfPt[disabled]{opacity:.65;cursor:not-allowed}.Button_link__1EdEA{border:0;padding:0;white-space:normal;background:transparent;font-size:inherit;line-height:inherit;border-radius:0;color:#2970ef;transition:color .25s;height:auto}.Button_link__1EdEA:hover{text-decoration:underline;color:#1057d5}.Button_primary__2rvzA{background:#2970ef;color:#fff}.Button_primary__2rvzA:hover:not(:disabled){background:#1057d5}.Button_transparent__1wOyR{background-color:initial}.Button_transparent__1wOyR:hover:not(:disabled){background:#dedede}.Button_cancel__6onep{background:transparent;color:#fff}.Button_cancel__6onep:hover{text-decoration:underline}.Tree_wrapper__3FCAV{background:#ededed;flex:1 1;padding:1em;position:absolute;left:1em;right:1em;top:2.75em;bottom:0;overflow-y:auto}.Tree_tree__1kXto{list-style-type:none;margin:0 0 1em;padding:0}.Tree_tree__1kXto .Tree_tree__1kXto{margin-left:1.5em;margin-bottom:0}.Tree_item__3qPn2{margin:0;padding:0;position:relative}.Tree_tree__1kXto .Tree_tree__1kXto>.Tree_item__3qPn2:before{content:"";position:absolute;left:-1em;height:100%;top:0;width:1px;background:#c7c7c7}.Tree_tree__1kXto .Tree_tree__1kXto>.Tree_item__3qPn2:last-child:before{height:50%}.Tree_tree__1kXto .Tree_tree__1kXto>.Tree_item__3qPn2:after{content:"";position:absolute;left:-1em;height:1px;top:.75em;width:.5em;background:#c7c7c7}.Tree_button__1pwmr{background:transparent;cursor:pointer;font-family:Roboto,Helvetica Neue,sans-serif;border:0;padding:.25em 0;font-size:.875em;display:flex;align-items:center;transition:color .2s}.Tree_active__1oqj2{color:#2970ef;font-weight:700}.Tree_button__1pwmr:focus{outline:none}.Tree_icon__10abK{color:#999;display:flex;align-items:center;margin-right:.5em;font-size:1.1428571429em}.Tree_hasItems__1R79E{color:#333;font-size:1.0714285714em;margin-left:1px}.Tree_rotate__1H3of{-webkit-animation:Tree_rotate__1H3of 1.25s linear infinite;animation:Tree_rotate__1H3of 1.25s linear infinite}@-webkit-keyframes Tree_rotate__1H3of{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes Tree_rotate__1H3of{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.Loader_loader__bugWu{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;justify-content:center;align-items:center;z-index:99999;margin:0}.Loader_loader__bugWu span{font-size:.75em;position:relative;top:1em;display:block;text-transform:uppercase;opacity:.8}.Loader_content__ylrK7{color:hsl(0,0,50);text-align:center}.Loader_icon__18aWx{display:inline-block;position:relative;font-size:1em;width:1.375em;height:2.375em;margin:0 1.5em -.3em;-webkit-transform:rotate(-48deg);transform:rotate(-48deg);-webkit-animation:Loader_loadRotate__2o6ua 1.5s cubic-bezier(.45,.05,.55,.95) infinite;animation:Loader_loadRotate__2o6ua 1.5s cubic-bezier(.45,.05,.55,.95) infinite}.Loader_icon__18aWx:after,.Loader_icon__18aWx:before{content:"";display:block;background:currentColor;border-radius:50%;position:absolute;left:50%}.Loader_icon__18aWx:before{width:1em;height:1em;margin-left:-.5em;bottom:1.375em;-webkit-animation:Loader_loadBounceTopSquash__2k92q .75s ease infinite alternate,Loader_loadBounceTopFlow__1xxdW .75s ease infinite alternate;animation:Loader_loadBounceTopSquash__2k92q .75s ease infinite alternate,Loader_loadBounceTopFlow__1xxdW .75s ease infinite alternate}.Loader_icon__18aWx:after{width:1.375em;height:1.375em;margin-left:-.6875em;bottom:0;-webkit-animation:Loader_loadBounceBottomSquash__1Fxre .75s ease infinite alternate,Loader_loadBounceBottomFlow__1dwsV .75s ease infinite alternate;animation:Loader_loadBounceBottomSquash__1Fxre .75s ease infinite alternate,Loader_loadBounceBottomFlow__1dwsV .75s ease infinite alternate}.Loader_fadeEnter__3pX7d{opacity:.01}.Loader_fadeActiveEnter__D0iWY{opacity:1;transition:opacity .25s}.Loader_fadeExit__xQVJO{opacity:1}.Loader_fadeActiveExit__ZNfOf{opacity:.01;transition:opacity .25s}@-webkit-keyframes Loader_loadBounceTopSquash__2k92q{0%{height:.375em;border-radius:3.75em 3.75em 1.25em 1.25em;-webkit-transform:scaleX(2);transform:scaleX(2)}15%{height:1em;border-radius:50%;-webkit-transform:scaleX(1);transform:scaleX(1)}to{height:1em;border-radius:50%;-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes Loader_loadBounceTopSquash__2k92q{0%{height:.375em;border-radius:3.75em 3.75em 1.25em 1.25em;-webkit-transform:scaleX(2);transform:scaleX(2)}15%{height:1em;border-radius:50%;-webkit-transform:scaleX(1);transform:scaleX(1)}to{height:1em;border-radius:50%;-webkit-transform:scaleX(1);transform:scaleX(1)}}@-webkit-keyframes Loader_loadBounceBottomSquash__1Fxre{0%{height:1em;border-radius:1.25em 1.25em 3.75em 3.75em;-webkit-transform:scaleX(1.5);transform:scaleX(1.5)}15%{height:1.375em;border-radius:50%;-webkit-transform:scaleX(1);transform:scaleX(1)}to{height:1.375em;border-radius:50%;-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes Loader_loadBounceBottomSquash__1Fxre{0%{height:1em;border-radius:1.25em 1.25em 3.75em 3.75em;-webkit-transform:scaleX(1.5);transform:scaleX(1.5)}15%{height:1.375em;border-radius:50%;-webkit-transform:scaleX(1);transform:scaleX(1)}to{height:1.375em;border-radius:50%;-webkit-transform:scaleX(1);transform:scaleX(1)}}@-webkit-keyframes Loader_loadBounceTopFlow__1xxdW{0%{bottom:1.125em}50%{bottom:2.25em;-webkit-animation-timing-function:cubic-bezier(.55,.06,.68,.19);animation-timing-function:cubic-bezier(.55,.06,.68,.19)}90%{bottom:1.75em}to{bottom:1.75em}}@keyframes Loader_loadBounceTopFlow__1xxdW{0%{bottom:1.125em}50%{bottom:2.25em;-webkit-animation-timing-function:cubic-bezier(.55,.06,.68,.19);animation-timing-function:cubic-bezier(.55,.06,.68,.19)}90%{bottom:1.75em}to{bottom:1.75em}}@-webkit-keyframes Loader_loadBounceBottomFlow__1dwsV{0%{bottom:.1875em}50%{bottom:-.9375em;-webkit-animation-timing-function:cubic-bezier(.55,.06,.68,.19);animation-timing-function:cubic-bezier(.55,.06,.68,.19)}90%{bottom:0}to{bottom:0}}@keyframes Loader_loadBounceBottomFlow__1dwsV{0%{bottom:.1875em}50%{bottom:-.9375em;-webkit-animation-timing-function:cubic-bezier(.55,.06,.68,.19);animation-timing-function:cubic-bezier(.55,.06,.68,.19)}90%{bottom:0}to{bottom:0}}@-webkit-keyframes Loader_loadRotate__2o6ua{0%{-webkit-transform:rotate(-228deg);transform:rotate(-228deg)}49%{-webkit-transform:rotate(-48deg);transform:rotate(-48deg)}51%{-webkit-transform:rotate(-48deg);transform:rotate(-48deg)}92%{-webkit-transform:rotate(132deg);transform:rotate(132deg)}to{-webkit-transform:rotate(132deg);transform:rotate(132deg)}}@keyframes Loader_loadRotate__2o6ua{0%{-webkit-transform:rotate(-228deg);transform:rotate(-228deg)}49%{-webkit-transform:rotate(-48deg);transform:rotate(-48deg)}51%{-webkit-transform:rotate(-48deg);transform:rotate(-48deg)}92%{-webkit-transform:rotate(132deg);transform:rotate(132deg)}to{-webkit-transform:rotate(132deg);transform:rotate(132deg)}}.Select_select__11lyk{font-family:Roboto,Helvetica Neue,sans-serif;-webkit-appearance:none;appearance:none;border:none;border-radius:2px;font-size:.75em;height:3em;padding:0 2.25em 0 1em;background-color:#dedede;background-image:url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLW1pdGVybGltaXQ9IjEuNDEiPjxwYXRoIGQ9Ik01LjUzIDcuNDlMLjIgMi4xNWEuNjYuNjYgMCAwMTAtLjkzTC44Mi41OWEuNjYuNjYgMCAwMS45MyAwTDYgNC44MiAxMC4yNS42YS42Ni42NiAwIDAxLjkzIDBsLjYzLjYzYy4yNS4yNS4yNS42NyAwIC45M0w2LjQ3IDcuNDlhLjY2LjY2IDAgMDEtLjk0IDB6IiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48L3N2Zz4=");background-repeat:no-repeat;background-position:right .75em center;background-size:.75em auto;cursor:pointer}.Checkbox_checkbox__yoCzk{position:absolute;opacity:0;left:-9999em;pointer-events:all}.Checkbox_label__2Wa0C{line-height:1.2;cursor:pointer;display:inline-flex;margin:0;padding:0;font-weight:400}.Checkbox_disabledLabel__3XU3p{opacity:.5;cursor:not-allowed}.Checkbox_icon__eqMXr{color:grey;font-size:1.1428571429em;margin-right:.35em;display:inline-flex}.Checkbox_iconActive__2qtld{color:#2970ef}.ItemsTable_table__11mZ6{width:100%;border-collapse:collapse;border-spacing:0;border-bottom:1px solid #ccc}.ItemsTable_table__11mZ6 td,.ItemsTable_table__11mZ6 th{padding:.5em .5em .5em 0;font-size:.875em;line-height:1.3;vertical-align:middle;transition:background-color .25s}.ItemsTable_table__11mZ6 th{background:#999;color:#fff;font-size:.75em;text-transform:uppercase;font-weight:400;text-align:left}.ItemsTable_table__11mZ6 thead td{border-bottom:1px solid #ccc}.ItemsTable_table__11mZ6 thead td,.ItemsTable_table__11mZ6 thead th{padding-top:.8333333333em;padding-bottom:.8333333333em}.ItemsTable_table__11mZ6 tr{cursor:default;background:transparent}.ItemsTable_table__11mZ6 tr>td:first-child,.ItemsTable_table__11mZ6 tr>th:first-child{padding-left:1em}.ItemsTable_table__11mZ6 tbody tr:hover td{background:#f2f2f2}.ItemsTable_table__11mZ6 tbody tr.ItemsTable_activeRow__1MDO8 td{background:#e0e0e0}.ItemsTable_table__11mZ6.ItemsTable_indent__2zNAu tbody td:first-child{padding-left:3em}.ItemsTable_checkbox__3mqo2{font-size:1.125em;display:inline-flex;vertical-align:middle;margin-right:.25em}.ItemsTable_table__11mZ6 td img{display:block;max-width:4.2857142857em;max-height:4.2857142857em;padding:2px;background-color:#fff}.ItemsTable_invisibleIcon__1x6hs{font-size:1.1428571429em;display:inline-flex;vertical-align:middle;margin-right:.5em}.Pager_pager__3ZmyX{padding:2em 1em 1em;display:flex}.Pager_pagination__3oXxT{flex:1 1;text-align:center;list-style-type:none;margin:0;padding:0}.Pager_paginatorItem__1muPB{display:inline-block;margin-right:1px}.Breadcrumbs_breadcrumbs__3q5Gt{list-style-type:none;margin:0;padding:0;display:flex;align-items:flex-start;flex-wrap:wrap}.Breadcrumbs_breadcrumbs__3q5Gt li{display:inline-block;font-size:.75em;margin:0;padding:0}.Breadcrumbs_breadcrumbs__3q5Gt li+li{position:relative;padding-left:1.5em}.Breadcrumbs_breadcrumbs__3q5Gt li+li:before{content:"/";display:inline-block;margin:0 .5em;position:absolute;left:0;top:.125em}.Dropdown_dropdown__-SNI4{position:relative;display:inline-block}.Dropdown_toggle__206aM{color:#999;text-decoration:none;font-size:.6875em;font-weight:500;text-transform:uppercase;display:flex;align-items:center}.Dropdown_toggle__206aM:active,.Dropdown_toggle__206aM:focus,.Dropdown_toggle__206aM:hover{color:#2970ef;text-decoration:none}.Dropdown_toggle__206aM svg{margin-left:.35em}.Dropdown_menu__3juzx{position:absolute;right:0;top:100%;background:#fff;box-shadow:0 0 .5em rgba(0,0,0,.5);border-radius:2px;padding:.5em 0;margin:1em 0 0;list-style-type:none;z-index:999;font-size:.874em;min-width:160px;background-clip:padding-box;text-align:left}.Dropdown_menu__3juzx li{padding:.35em .5em;margin:0}.Preview_preview__11IQq{width:18em;position:relative;overflow-x:hidden}.Preview_content__hzQ7j{padding:0 1em;width:18em;position:absolute;left:0;right:0;top:0;bottom:0;overflow-y:auto}.Preview_preview__11IQq a{color:#2970ef;text-decoration:none}.Preview_preview__11IQq a:focus,.Preview_preview__11IQq a:hover{text-decoration:underline}.Preview_preview__11IQq figure{margin:0}.Preview_preview__11IQq img{max-width:100%}.Preview_preview__11IQq p{margin:0 0 1em}.Preview_preview__11IQq .layout-icon{width:90%;height:0;padding-bottom:130%;margin:1em auto 0;border:2px solid #a1a1a1;background-size:95%;border-radius:3px}.Preview_slideEnter__2Ol_c{width:0;opacity:.01}.Preview_slideActiveEnter__3dkZu{width:18em;opacity:1;transition:width .25s ease-out,opacity .15s}.Preview_slideExit__WxkLB{width:18em;opacity:1}.Preview_slideActiveExit__265PC{width:0;opacity:.01;transition:width .25s ease-out,opacity .15s .1s}.Items_items__3Y0zI{flex:1 1;background:#ededed;position:relative;max-height:100%}.Items_header__2cOTb{display:flex;padding:.75em 1em;justify-content:space-between;align-items:center}.Items_fadeEnter__1HvTy{opacity:.01}.Items_fadeActiveEnter__1G_6N{opacity:1;transition:opacity .2s}.Items_fadeExit__-5rK9{opacity:1}.Items_fadeActiveExit__3aLFE{opacity:.01;transition:opacity .2s}.Items_wrapper__2Rw5j{position:absolute;left:0;top:0;right:0;bottom:0;overflow-y:auto}.Items_settings__38PJy{flex:1 1;text-align:right}.Input_input__2FuG5{border:none;box-shadow:none;border-radius:2px;font-size:.875em;padding:.7142857143em .875em;line-height:1}.Input_prefixed__3WO7g{border-radius:0 2px 2px 0}.Input_sufixed__BTVq5{border-radius:2px 0 0 2px}.Search_searchPanel__1e2Sf{flex:0 0 25%;padding:0 1em;display:flex;flex-direction:column;position:relative;max-height:100%;overflow-y:auto}.Search_resultsPanel__tL9jM{flex:1 1;background:#ededed;position:relative;max-height:100%;overflow-y:auto;padding-bottom:1em}.Search_searchWrapper__2B9Bt{flex:1 1;background:#ededed;margin-top:.5em;padding:1em}.Search_search__kehRK{display:flex;width:100%;flex-wrap:wrap}.Search_search__kehRK input{flex:1 1}.Tabs_tabs__33r_k{list-style-type:none;margin:0;display:flex;width:25%;padding:1em 1em .5em}.Tabs_tab__1XZ6E{flex:1 1;text-align:center;font-size:.75em;display:flex;align-items:center;justify-content:center;border:1px solid #2970ef;color:#2970ef;cursor:pointer;padding:.5em .125em;margin:0}.Tabs_tab__1XZ6E+.Tabs_tab__1XZ6E{border-left:0}.Tabs_tab__1XZ6E:first-child{border-radius:2px 0 0 2px}.Tabs_tab__1XZ6E:last-child{border-radius:0 2px 2px 0}.Tabs_tab__1XZ6E svg{margin-right:.125em}.Tabs_active__FvpRL{background:#2970ef;color:#fff;cursor:default}.Tabs_tabsHeader__3FcqX{display:flex;justify-content:space-between;align-items:center}.Tabs_headerContent__1T0AU{padding:1em 1em .5em;flex:1 1;text-align:right}.Toggle_checkbox__rhGcW{position:absolute;opacity:0;left:-9999em;pointer-events:all}.Toggle_label__3lr_v{line-height:1.2;cursor:pointer;display:inline-flex;font-size:.6875em;text-transform:uppercase;font-weight:500;align-items:center;padding:1.0909090909em 0;margin:0}.Toggle_disabledLabel__dWisf{opacity:.5;cursor:not-allowed}.Toggle_icon__1v23t{display:inline-block;margin-left:.75em;width:2.5454545455em;height:1.0909090909em;border-radius:50em;position:relative;background:#c7c7c7;transition:background-color .3s}.Toggle_icon__1v23t:before{content:"";display:inline-block;position:absolute;width:1.4545454545em;height:1.4545454545em;border-radius:50em;left:-2px;top:50%;-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0);transition:left .3s cubic-bezier(.4,0,.2,1),background-color .1s;background:#fff;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.Toggle_iconActive__2ahte{background:#7ea9f5}.Toggle_iconActive__2ahte:before{left:1.2727272727em;background:#2970ef;box-shadow:0 3px 4px 0 rgba(0,0,0,.14),0 3px 3px -2px rgba(0,0,0,.2),0 1px 8px 0 rgba(0,0,0,.12)}.Footer_footer__2NNgL{background:#383838;color:#fff;padding:.75em;display:flex;justify-content:space-between}.Footer_actions__3DKAf{min-width:11em;text-align:right}.Footer_footer__2NNgL button{margin:.25em}.Footer_itemIcon__2SY-W{margin-left:.5em;color:#999;font-size:1.25em;display:inline-flex}.Browser_browser__1ZZaQ{position:fixed;left:0;top:0;width:100%;height:100%;z-index:1000;background:rgba(0,0,0,.5);color:#333;font-family:Roboto,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;overflow-x:hidden;font-size:16px;line-height:1.2}.Browser_browser__1ZZaQ *{box-sizing:border-box}.Browser_dialog__MW-Vw{padding:1em;height:100%;display:flex;flex-direction:column}.Browser_content__38RPK{background:#fff;box-shadow:0 .5em 1em rgba(0,0,0,.5);flex:1 1;display:flex;flex-direction:column;position:relative}.Browser_panels__2fOkv{display:flex;flex:1 1}.Browser_treePanel__3xYgJ{flex:0 0 25%;padding:0 1em;display:flex;flex-direction:column;position:relative;max-height:100%;overflow-y:auto}.Browser_loading__2FF_l{background:#333;color:#999;flex:1 1;box-shadow:0 .5em 1em rgba(0,0,0,.5)}.Browser_slideEnter__1vdKy{opacity:.01;-webkit-transform:translate3d(0,-10%,0);transform:translate3d(0,-10%,0)}.Browser_slideActiveEnter__bEByv{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0);transition:opacity .25s,-webkit-transform .5s;transition:opacity .25s,transform .5s;transition:opacity .25s,transform .5s,-webkit-transform .5s} -------------------------------------------------------------------------------- /bundle/Resources/public/media/Roboto-300.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/bundle/Resources/public/media/Roboto-300.eot -------------------------------------------------------------------------------- /bundle/Resources/public/media/Roboto-300.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/bundle/Resources/public/media/Roboto-300.ttf -------------------------------------------------------------------------------- /bundle/Resources/public/media/Roboto-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/bundle/Resources/public/media/Roboto-300.woff -------------------------------------------------------------------------------- /bundle/Resources/public/media/Roboto-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/bundle/Resources/public/media/Roboto-300.woff2 -------------------------------------------------------------------------------- /bundle/Resources/public/media/Roboto-500.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/bundle/Resources/public/media/Roboto-500.eot -------------------------------------------------------------------------------- /bundle/Resources/public/media/Roboto-500.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/bundle/Resources/public/media/Roboto-500.ttf -------------------------------------------------------------------------------- /bundle/Resources/public/media/Roboto-500.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/bundle/Resources/public/media/Roboto-500.woff -------------------------------------------------------------------------------- /bundle/Resources/public/media/Roboto-500.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/bundle/Resources/public/media/Roboto-500.woff2 -------------------------------------------------------------------------------- /bundle/Resources/public/media/Roboto-700.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/bundle/Resources/public/media/Roboto-700.eot -------------------------------------------------------------------------------- /bundle/Resources/public/media/Roboto-700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/bundle/Resources/public/media/Roboto-700.ttf -------------------------------------------------------------------------------- /bundle/Resources/public/media/Roboto-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/bundle/Resources/public/media/Roboto-700.woff -------------------------------------------------------------------------------- /bundle/Resources/public/media/Roboto-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/bundle/Resources/public/media/Roboto-700.woff2 -------------------------------------------------------------------------------- /bundle/Resources/public/media/Roboto-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/bundle/Resources/public/media/Roboto-regular.eot -------------------------------------------------------------------------------- /bundle/Resources/public/media/Roboto-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/bundle/Resources/public/media/Roboto-regular.ttf -------------------------------------------------------------------------------- /bundle/Resources/public/media/Roboto-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/bundle/Resources/public/media/Roboto-regular.woff -------------------------------------------------------------------------------- /bundle/Resources/public/media/Roboto-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/bundle/Resources/public/media/Roboto-regular.woff2 -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netgen/content-browser-ui", 3 | "description": "Netgen Content Browser user interface", 4 | "license": "MIT", 5 | "type": "symfony-bundle", 6 | "authors": [ 7 | { 8 | "name": "Netgen", 9 | "homepage": "https://netgen.io" 10 | } 11 | ], 12 | "require-dev": { 13 | "netgen/layouts-coding-standard": "^2.0" 14 | }, 15 | "config": { 16 | "allow-plugins": false 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "Netgen\\Bundle\\ContentBrowserUIBundle\\": "bundle/" 21 | } 22 | }, 23 | "minimum-stability": "dev", 24 | "prefer-stable": true, 25 | "extra": { 26 | "branch-alias": { 27 | "dev-master": "1.4.x-dev" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const paths = require('./paths'); 3 | 4 | // Make sure that including paths.js after env.js will read .env variables. 5 | delete require.cache[require.resolve('./paths')]; 6 | 7 | const NODE_ENV = process.env.NODE_ENV; 8 | if (!NODE_ENV) { 9 | throw new Error( 10 | 'The NODE_ENV environment variable is required but was not specified.' 11 | ); 12 | } 13 | 14 | var dotenvFiles = [ 15 | paths.dotenv, 16 | ].filter(Boolean); 17 | 18 | // Load environment variables from .env* files. Suppress warnings using silent 19 | // if this file is missing. dotenv will never modify any environment variables 20 | // that have already been set. Variable expansion is supported in .env files. 21 | // https://github.com/motdotla/dotenv 22 | // https://github.com/motdotla/dotenv-expand 23 | dotenvFiles.forEach(dotenvFile => { 24 | if (fs.existsSync(dotenvFile)) { 25 | require('dotenv-expand')( 26 | require('dotenv').config({ 27 | path: dotenvFile, 28 | }) 29 | ); 30 | } 31 | }); 32 | 33 | process.env.NODE_PATH = (process.env.NODE_PATH || ''); 34 | 35 | function getClientEnvironment() { 36 | const raw = Object.keys(process.env) 37 | .reduce( 38 | (env, key) => { 39 | env[key] = process.env[key]; 40 | return env; 41 | }, 42 | { 43 | // Useful for determining whether we’re running in production mode. 44 | // Most importantly, it switches React into the correct mode. 45 | NODE_ENV: process.env.NODE_ENV || 'development', 46 | } 47 | ); 48 | // Stringify all values so we can feed into Webpack DefinePlugin 49 | const stringified = { 50 | 'process.env': Object.keys(raw).reduce((env, key) => { 51 | env[key] = JSON.stringify(raw[key]); 52 | return env; 53 | }, {}), 54 | }; 55 | 56 | return { raw, stringified }; 57 | } 58 | 59 | module.exports = getClientEnvironment; 60 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const url = require('url'); 4 | 5 | const appDirectory = fs.realpathSync(process.cwd()); 6 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 7 | 8 | const envPublicUrl = process.env.PUBLIC_URL; 9 | const getPublicUrl = appPackageJson => 10 | envPublicUrl || require(appPackageJson).homepage; 11 | 12 | function ensureSlash(inputPath, needsSlash) { 13 | const hasSlash = inputPath.endsWith('/'); 14 | if (hasSlash && !needsSlash) { 15 | return inputPath.substr(0, inputPath.length - 1); 16 | } else if (!hasSlash && needsSlash) { 17 | return `${inputPath}/`; 18 | } else { 19 | return inputPath; 20 | } 21 | } 22 | 23 | function getServedPath(appPackageJson) { 24 | const publicUrl = getPublicUrl(appPackageJson); 25 | const servedUrl = 26 | envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : '/'); 27 | return ensureSlash(servedUrl, true); 28 | } 29 | 30 | module.exports = { 31 | dotenv: path.resolve('.env'), 32 | appBuild: path.resolve('bundle/Resources/public'), 33 | appPublic: path.resolve('public'), 34 | appHtml: path.resolve('public/index.html'), 35 | appIndexJs: path.resolve('src/index.js'), 36 | appPackageJson: path.resolve('package.json'), 37 | appSrc: path.resolve('src'), 38 | proxySetup: path.resolve('src/setupProxy.js'), 39 | publicUrl: getPublicUrl(resolveApp('package.json')), 40 | servedPath: getServedPath(resolveApp('package.json')), 41 | }; 42 | -------------------------------------------------------------------------------- /config/webpackDevServer.config.js: -------------------------------------------------------------------------------- 1 | const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware'); 2 | const evalSourceMapMiddleware = require('react-dev-utils/evalSourceMapMiddleware'); 3 | const ignoredFiles = require('react-dev-utils/ignoredFiles'); 4 | const paths = require('./paths'); 5 | const fs = require('fs'); 6 | 7 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 8 | const host = process.env.HOST || '0.0.0.0'; 9 | 10 | module.exports = function(proxy, allowedHost) { 11 | return { 12 | // WebpackDevServer 2.4.3 introduced a security fix that prevents remote 13 | // websites from potentially accessing local content through DNS rebinding: 14 | // https://github.com/webpack/webpack-dev-server/issues/887 15 | // https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a 16 | // However, it made several existing use cases such as development in cloud 17 | // environment or subdomains in development significantly more complicated: 18 | // https://github.com/facebook/create-react-app/issues/2271 19 | // https://github.com/facebook/create-react-app/issues/2233 20 | // While we're investigating better solutions, for now we will take a 21 | // compromise. Since our WDS configuration only serves files in the `public` 22 | // folder we won't consider accessing them a vulnerability. However, if you 23 | // use the `proxy` feature, it gets more dangerous because it can expose 24 | // remote code execution vulnerabilities in backends like Django and Rails. 25 | // So we will disable the host check normally, but enable it if you have 26 | // specified the `proxy` setting. Finally, we let you override it if you 27 | // really know what you're doing with a special environment variable. 28 | disableHostCheck: 29 | !proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === 'true', 30 | // Enable gzip compression of generated files. 31 | compress: true, 32 | // Silence WebpackDevServer's own logs since they're generally not useful. 33 | // It will still show compile warnings and errors with this setting. 34 | clientLogLevel: 'none', 35 | // By default WebpackDevServer serves physical files from current directory 36 | // in addition to all the virtual build products that it serves from memory. 37 | // This is confusing because those files won’t automatically be available in 38 | // production build folder unless we copy them. However, copying the whole 39 | // project directory is dangerous because we may expose sensitive files. 40 | // Instead, we establish a convention that only files in `public` directory 41 | // get served. Our build script will copy `public` into the `build` folder. 42 | // In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%: 43 | // 44 | // In JavaScript code, you can access it with `process.env.PUBLIC_URL`. 45 | // Note that we only recommend to use `public` folder as an escape hatch 46 | // for files like `favicon.ico`, `manifest.json`, and libraries that are 47 | // for some reason broken when imported through Webpack. If you just want to 48 | // use an image, put it in `src` and `import` it from JavaScript instead. 49 | contentBase: paths.appPublic, 50 | // By default files from `contentBase` will not trigger a page reload. 51 | watchContentBase: true, 52 | // Enable hot reloading server. It will provide /sockjs-node/ endpoint 53 | // for the WebpackDevServer client so it can learn when the files were 54 | // updated. The WebpackDevServer client is included as an entry point 55 | // in the Webpack development configuration. Note that only changes 56 | // to CSS are currently hot reloaded. JS changes will refresh the browser. 57 | hot: true, 58 | // It is important to tell WebpackDevServer to use the same "root" path 59 | // as we specified in the config. In development, we always serve from /. 60 | publicPath: '/', 61 | // WebpackDevServer is noisy by default so we emit custom message instead 62 | // by listening to the compiler events with `compiler.hooks[...].tap` calls above. 63 | quiet: true, 64 | // Reportedly, this avoids CPU overload on some systems. 65 | // https://github.com/facebook/create-react-app/issues/293 66 | // src/node_modules is not ignored to support absolute imports 67 | // https://github.com/facebook/create-react-app/issues/1065 68 | watchOptions: { 69 | ignored: ignoredFiles(paths.appSrc), 70 | }, 71 | // Enable HTTPS if the HTTPS environment variable is set to 'true' 72 | https: protocol === 'https', 73 | host, 74 | overlay: false, 75 | historyApiFallback: { 76 | // Paths with dots should still use the history fallback. 77 | // See https://github.com/facebook/create-react-app/issues/387. 78 | disableDotRule: true, 79 | }, 80 | public: allowedHost, 81 | proxy, 82 | before(app, server) { 83 | if (fs.existsSync(paths.proxySetup)) { 84 | // This registers user provided middleware for proxy reasons 85 | require(paths.proxySetup)(app); 86 | } 87 | 88 | // This lets us fetch source contents from webpack for the error overlay 89 | app.use(evalSourceMapMiddleware(server)); 90 | // This lets us open files from the runtime error overlay. 91 | app.use(errorOverlayMiddleware()); 92 | }, 93 | }; 94 | }; 95 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:8181", 3 | "video": false 4 | } 5 | -------------------------------------------------------------------------------- /cypress/integration/browser_test.js: -------------------------------------------------------------------------------- 1 | /// 2 | /* global Cypress, cy */ 3 | import { withCyTag } from '../utils'; 4 | 5 | describe('Multiple browse test', function() { 6 | Cypress.Commands.add('openContentBrowser', (nr) => { 7 | cy.get(withCyTag('multiple-add')).click(); 8 | cy.get(withCyTag('browser')).should('be.visible'); 9 | }); 10 | 11 | Cypress.Commands.add('checkSelectedItemsLength', (nr) => { 12 | cy.window().its('store').invoke('getState').its('app').its('selectedItems').should('have.length', nr); 13 | }); 14 | 15 | Cypress.Commands.add('checkBreadcrumbsAndTableForText', (text) => { 16 | cy.get(withCyTag('breadcrumbs')).contains(text); 17 | cy.get(withCyTag('items-table-head')).contains(text); 18 | }); 19 | 20 | describe('Open index page', () => { 21 | it('Loads index page', () => { 22 | cy.visit('/'); 23 | cy.get(withCyTag('multiple-browse')).should('be.visible'); 24 | }); 25 | describe('Open Browser', () => { 26 | it('open content browser on button click', () => { 27 | cy.openContentBrowser(); 28 | }); 29 | it('has no selected items in store on load', () => { 30 | cy.checkSelectedItemsLength(0); 31 | }); 32 | }); 33 | describe('Test loaders', () => { 34 | it('shows loader while config loading', () => { 35 | cy.window().its('store').invoke('dispatch', { type: 'CONFIG_LOADED', isLoaded: false }); 36 | cy.get(withCyTag('browser-loading')).should('be.visible'); 37 | cy.window().its('store').invoke('dispatch', { type: 'CONFIG_LOADED', isLoaded: true }); 38 | cy.get(withCyTag('browser-loading')).should('not.be.visible'); 39 | }); 40 | it('shows tree loader while loading', () => { 41 | cy.window().its('store').invoke('dispatch', { type: 'START_TREE_LOAD' }); 42 | cy.get(withCyTag('tree-wrapper')).find(withCyTag('loader')).should('be.visible'); 43 | cy.window().its('store').invoke('dispatch', { type: 'STOP_TREE_LOAD' }); 44 | cy.get(withCyTag('tree-wrapper')).find(withCyTag('loader')).should('not.be.visible'); 45 | }); 46 | it('shows items loader while loading', () => { 47 | cy.window().its('store').invoke('dispatch', { type: 'START_LOCATION_LOAD' }); 48 | cy.get(withCyTag('items')).find(withCyTag('loader')).should('be.visible'); 49 | cy.window().its('store').invoke('dispatch', { type: 'STOP_LOCATION_LOAD' }); 50 | cy.get(withCyTag('items')).find(withCyTag('loader')).should('not.be.visible'); 51 | }); 52 | }) 53 | describe('Test content tree', () => { 54 | it('changes section in select dropdown', () => { 55 | cy.get(withCyTag('tree-panel')).find('select').as('sectionSelect').children().its('length').should('be.greaterThan', 1); 56 | cy.get('@sectionSelect').children().eq(1).then((el) => { 57 | cy.get('@sectionSelect').select(el.val()); 58 | cy.wrap(el).invoke('text').then((text) => { 59 | cy.checkBreadcrumbsAndTableForText(text); 60 | }); 61 | }); 62 | cy.get('@sectionSelect').children().eq(0).then((el) => { 63 | cy.get('@sectionSelect').select(el.val()); 64 | cy.wrap(el).invoke('text').then((text) => { 65 | cy.checkBreadcrumbsAndTableForText(text); 66 | }); 67 | }); 68 | }); 69 | it('opens second level in tree item', () => { 70 | cy.get(withCyTag('tree')).as('tree'); 71 | cy.get('@tree').find(withCyTag('item-icon-has-sub')).as('toggle').click(); 72 | cy.get('@toggle').closest(withCyTag('tree-item')).as('item'); 73 | cy.get('@item').find(withCyTag('tree')).should('be.visible').children().its('length').should('be.greaterThan', 1); 74 | cy.get('@toggle').click(); 75 | cy.get('@item').find(withCyTag('tree')).should('not.be.visible'); 76 | }); 77 | it('clicks item in content tree', () => { 78 | cy.get(withCyTag('tree')).as('tree'); 79 | cy.get('@tree').children().eq(0).as('firstItem').find('button').click(); 80 | cy.get('@firstItem').invoke('text').then(cy.checkBreadcrumbsAndTableForText); 81 | cy.get('@tree').children().eq(2).as('secondItem').find('button').click(); 82 | cy.get('@secondItem').invoke('text').then(cy.checkBreadcrumbsAndTableForText); 83 | }); 84 | }); 85 | describe('Test breadcrumbs', () => { 86 | it('tests three levels of content', () => { 87 | cy.get(withCyTag('breadcrumbs')).as('breadcrumbs').children().eq(0).click(); 88 | cy.get('@breadcrumbs').children().should('have.length', 1); 89 | cy.get(withCyTag('items-table-body')).as('tablebody').children().eq(2).children().eq(0).find('button').click(); 90 | cy.get('@breadcrumbs').children().should('have.length', 2); 91 | cy.get('@tablebody').children().eq(0).children().eq(0).find('button').click(); 92 | cy.get('@breadcrumbs').children().should('have.length', 3); 93 | cy.get('@breadcrumbs').children().eq(2).find('button').should('not.exist'); 94 | cy.get('@breadcrumbs').children().eq(0).find('button').click(); 95 | cy.get('@breadcrumbs').children().eq(0).invoke('text').then((text) => { 96 | cy.get(withCyTag('items-table-head')).contains(text); 97 | }); 98 | cy.get('@breadcrumbs').children().should('have.length', 1); 99 | cy.get('@breadcrumbs').children().eq(0).find('button').should('not.exist'); 100 | }); 101 | }); 102 | describe('Test pagination', () => { 103 | it('changes table items limit', () => { 104 | cy.get(withCyTag('pager')).find('select').as('limiter').select('25'); 105 | cy.window().its('store').invoke('getState').its('app').its('itemsLimit').should('eq', '25'); 106 | cy.get(withCyTag('items-table-body')).as('tablebody').children().should('have.length.greaterThan', 10); 107 | cy.get('@limiter').select('5'); 108 | cy.window().its('store').invoke('getState').its('app').its('itemsLimit').should('eq', '5'); 109 | cy.get('@tablebody').children().should('have.length', 5); 110 | }); 111 | it('tests pagination', () => { 112 | cy.get(withCyTag('pager')).find(withCyTag('pagination')).as('pagination').children().should('have.length.greaterThan', 3); 113 | cy.window().its('store').invoke('getState').its('items').its('currentPage').as('statePage').should('eq', 1); 114 | cy.get('@pagination').children().first().find('button').should('be.disabled'); 115 | cy.get('@pagination').children().last().find('button').should('not.be.disabled'); 116 | cy.get('@pagination').children().last().click(); 117 | cy.window().its('store').invoke('getState').its('items').its('currentPage').as('statePage').should('eq', 2); 118 | cy.get('@pagination').children().first().find('button').should('not.be.disabled'); 119 | cy.get('@pagination').children().last().prev().click(); 120 | cy.get('@pagination').children().first().find('button').should('not.be.disabled'); 121 | cy.get('@pagination').children().last().find('button').should('be.disabled'); 122 | cy.get('@pagination').children().first().click(); 123 | cy.get('@pagination').children().first().find('button').should('not.be.disabled'); 124 | cy.get('@pagination').children().last().find('button').should('not.be.disabled'); 125 | }); 126 | }); 127 | describe('Test table options', () => { 128 | it('opens table options dropdown and checks if it\'s visible', () => { 129 | cy.get(withCyTag('items-header')).find(withCyTag('dropdown')).as('dropdown').find(withCyTag('dropdown-toggle')).as('toggle').click(); 130 | cy.get('@dropdown').find(withCyTag('dropdown-menu')).as('menu').should('be.visible'); 131 | }); 132 | it('checks if selected table columns are visible', () => { 133 | cy.get(withCyTag('items-table-head')).children().eq(0).as('tableHead'); 134 | cy.get(withCyTag('items-header')).find(withCyTag('dropdown-menu')).find('input[type="checkbox"]').each(($checkbox) => { 135 | cy.wrap($checkbox).parent().find('label').invoke('text').then((text) => { 136 | if ($checkbox[0].checked) { 137 | cy.get('@tableHead').should('contain', text); 138 | } else { 139 | cy.get('@tableHead').should('not.contain', text); 140 | } 141 | }); 142 | }); 143 | }); 144 | it('selects first not checked table column and checks if it\'s visible', () => { 145 | cy.get(withCyTag('items-header')).find(withCyTag('dropdown-menu')).find('input[type="checkbox"]:not(:checked)').first().parent().find('label').then($label => { 146 | cy.wrap($label).click().invoke('text').then((text) => { 147 | cy.get(withCyTag('items-table-head')).children().eq(0).should('contain', text); 148 | }); 149 | }); 150 | }); 151 | it('deselects last checked table column and checks if it\'s not visible', () => { 152 | cy.get(withCyTag('items-header')).find(withCyTag('dropdown-menu')).find('input[type="checkbox"]:checked').last().parent().find('label').then($label => { 153 | cy.wrap($label).click().invoke('text').then((text) => { 154 | cy.get(withCyTag('items-table-head')).children().eq(0).should('not.contain', text); 155 | }); 156 | }); 157 | }); 158 | it('closes table options dropdown and checks if it\'s not visible', () => { 159 | cy.get(withCyTag('items-header')).find(withCyTag('dropdown')).as('dropdown').find(withCyTag('dropdown-toggle')).as('toggle').click(); 160 | cy.get('@dropdown').find(withCyTag('dropdown-menu')).as('menu').should('not.be.visible'); 161 | }); 162 | }); 163 | describe('Test preview panel', () => { 164 | it('opens preview on toggle and checks if it\'s visible', () => { 165 | cy.window().its('store').invoke('dispatch', { type: 'TOGGLE_PREVIEW', toggle: false }); 166 | cy.get('#togglePreview').parent().find('label').click(); 167 | cy.get(withCyTag('preview')).should('be.visible'); 168 | cy.window().its('store').invoke('getState').its('app').its('showPreview').should('eq', true); 169 | }); 170 | it('shows preview loader while preview is loading', () => { 171 | cy.window().its('store').invoke('dispatch', { type: 'START_PREVIEW_LOAD' }); 172 | cy.get(withCyTag('preview')).find(withCyTag('loader')).should('be.visible'); 173 | cy.window().its('store').invoke('dispatch', { type: 'STOP_PREVIEW_LOAD' }); 174 | cy.get(withCyTag('preview')).find(withCyTag('loader')).should('not.be.visible'); 175 | }); 176 | it('checks if preview shows clicked item', () => { 177 | cy.get(withCyTag('items-table-head')).children().eq(1).children().eq(0).then(($cell) => { 178 | cy.wrap($cell).click().invoke('text').then((text) => { 179 | cy.get(withCyTag('preview')).should('contain', text); 180 | }); 181 | }); 182 | cy.get(withCyTag('items-table-body')).children().eq(0).children().eq(0).then(($cell) => { 183 | cy.wrap($cell).click().invoke('text').then((text) => { 184 | cy.get(withCyTag('preview')).should('contain', text); 185 | }); 186 | }); 187 | }); 188 | it('closes preview on toggle and checks if it\'s invisible', () => { 189 | cy.get('#togglePreview').parent().find('label').click(); 190 | cy.get('#togglePreview').should('not.be.checked'); 191 | cy.get(withCyTag('preview')).should('not.be.visible'); 192 | cy.window().its('store').invoke('getState').its('app').its('showPreview').should('eq', false); 193 | }); 194 | }); 195 | describe('Test search', () => { 196 | it('tests if search returns results when inputs string', () => { 197 | if (cy.window().its('store').invoke('getState').its('app').its('config').its('has_search')) { 198 | cy.get(withCyTag('tabs')).contains('Search').should('be.visible').click(); 199 | cy.get(withCyTag('tree-panel')).should('not.be.visible'); 200 | cy.get(withCyTag('items-table')).should('not.be.visible'); 201 | cy.get(withCyTag('search-form')).as('form').find('input').as('input').type('test'); 202 | cy.get('@form').submit(); 203 | cy.get(withCyTag('items-table')).should('be.visible').find('tbody').children().its('length').should('be.greaterThan', 3); 204 | cy.get('@input').clear().type('a{enter}'); 205 | cy.get(withCyTag('items-table')).should('be.visible').find('tbody').children().should('not.be.visible'); 206 | cy.get('@input').clear(); 207 | cy.get('@form').find('button[type="submit"]').click(); 208 | cy.get(withCyTag('items-table')).should('not.be.visible'); 209 | } 210 | }); 211 | }); 212 | describe('Test category select', () => { 213 | it('saves category to store when changing it on tree panel', () => { 214 | cy.visit('/'); 215 | cy.openContentBrowser(); 216 | cy.get(withCyTag('tabs')).contains('Browse').should('be.visible').click(); 217 | cy.get(withCyTag('tree-panel')).find('select').find(':selected').contains("Home"); 218 | cy.window().its('store').invoke('getState').its('app').its('sectionId').should('eq', 2); 219 | cy.get(withCyTag('tree-panel')).find('select').select('Users'); 220 | cy.get(withCyTag('tree-panel')).find('select').find(':selected').contains("Users"); 221 | cy.window().its('store').invoke('getState').its('app').its('sectionId').should('eq', 1); 222 | }); 223 | it('saves category to store when changing it on search panel', () => { 224 | cy.visit('/'); 225 | cy.openContentBrowser(); 226 | cy.get(withCyTag('tabs')).contains('Search').should('be.visible').click(); 227 | cy.get(withCyTag('search-panel')).find('select').find(':selected').contains("Home"); 228 | cy.window().its('store').invoke('getState').its('app').its('sectionId').should('eq', 2); 229 | cy.get(withCyTag('search-panel')).find('select').select('Users'); 230 | cy.get(withCyTag('search-panel')).find('select').find(':selected').contains("Users"); 231 | cy.window().its('store').invoke('getState').its('app').its('sectionId').should('eq', 1); 232 | }); 233 | it('perfoms search when category is changed', () => { 234 | cy.visit('/'); 235 | cy.openContentBrowser(); 236 | cy.get(withCyTag('tabs')).contains('Search').should('be.visible').click(); 237 | 238 | cy.get(withCyTag('search-form')).as('form').find('input').as('input').type('test'); 239 | cy.get('@form').submit(); 240 | 241 | cy.get(withCyTag('items-table')).should('be.visible').find('tbody').children().first().contains('Test'); 242 | 243 | cy.get(withCyTag('search-panel')).find('select').first().select('Users'); 244 | cy.get(withCyTag('items-table')).should('be.visible').find('tbody').children().first().contains('User'); 245 | 246 | cy.get(withCyTag('search-form')).as('form').find('input').as('input').clear(); 247 | cy.get(withCyTag('search-panel')).find('select').first().select('Home'); 248 | cy.get(withCyTag('items-table')).should('be.visible').find('tbody').children().first().contains('User'); 249 | 250 | }); 251 | }); 252 | describe('Closes content browser', () => { 253 | it('clicks on Cancel in footer and closes content browser', () => { 254 | cy.get(withCyTag('footer-actions')).contains('Cancel').click(); 255 | cy.get(withCyTag('browser')).should('not.be.visible'); 256 | }); 257 | }); 258 | describe('Open Browser and select items', () => { 259 | it('open content browser on button click', () => { 260 | cy.openContentBrowser(); 261 | }); 262 | it('selects first three items from items list', () => { 263 | cy.get(withCyTag('footer-actions')).contains('Confirm').should('be.disabled'); 264 | Cypress._.times(3, (i) => { 265 | cy.get(withCyTag('items-table-body')).children().eq(i).children().eq(0).find('label').click(); 266 | }) 267 | cy.get(withCyTag('footer-actions')).contains('Confirm').should('not.be.disabled'); 268 | }); 269 | it('has three selected items in store', () => { 270 | cy.checkSelectedItemsLength(3); 271 | }); 272 | describe('Remove items', () => { 273 | it('removes one selected item', () => { 274 | cy.get(withCyTag('footer-items')).find('button').last().click(); 275 | cy.checkSelectedItemsLength(2); 276 | }); 277 | }); 278 | describe('Close browser', () => { 279 | it('closes browser with two selected items', () => { 280 | cy.get(withCyTag('footer-actions')).contains('Confirm').should('not.be.disabled').click(); 281 | }); 282 | it('checks if two selected items are displayed', () => { 283 | cy.get(withCyTag('multiple-items')).as('items').find('.item').its('length').should('be.eq', 2); 284 | cy.window().its('store').invoke('getState').its('app').its('selectedItems').each((item) => { 285 | cy.get('@items').contains(item.name); 286 | }).then((items) => { 287 | cy.wrap(items).its('length').should('be.eq', 2); 288 | }); 289 | }) 290 | }); 291 | }); 292 | describe('Open browser with disabled items', () => { 293 | it('opens content browser and checks if two items disabled', () => { 294 | cy.openContentBrowser(); 295 | cy.window().its('store').invoke('getState').its('app').its('disabledItems').its('length').should('be.eq', 2); 296 | Cypress._.times(2, (i) => { 297 | cy.get(withCyTag('items-table-body')).children().eq(i).children().eq(0).find('input[type="checkbox"]').should('be.disabled'); 298 | }) 299 | }); 300 | it('selects first available item from items', () => { 301 | cy.get(withCyTag('items-table-body')).find('input[type="checkbox"]:not(:disabled)').first().parent().find('label').click().parents('td').invoke('text').then((text) => { 302 | cy.get(withCyTag('footer-items')).should('contain', text); 303 | }); 304 | }); 305 | }) 306 | describe('Close browser', () => { 307 | it('closes browser with three selected items', () => { 308 | cy.get(withCyTag('footer-actions')).contains('Confirm').should('not.be.disabled').click(); 309 | }); 310 | it('checks if three selected items are displayed', () => { 311 | cy.get(withCyTag('multiple-items')).as('items').find('.item').its('length').should('be.eq', 3); 312 | }) 313 | }); 314 | }); 315 | }); 316 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | module.exports = (on, config) => { 15 | // `on` is used to hook into various events Cypress emits 16 | // `config` is the resolved Cypress config 17 | } 18 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /cypress/utils.js: -------------------------------------------------------------------------------- 1 | /* global cy */ 2 | 3 | export const loginToAdmin = (test) => { 4 | const options = { 5 | method: 'POST', 6 | url: '/admin/login_check', 7 | form: true, 8 | body: { 9 | _username: 'admin', 10 | _password: 'publish', 11 | } 12 | } 13 | cy.request(options).then(() => { 14 | cy.getCookie('eZSESSID').then(cookie => test.eZSESSID = cookie.value); 15 | }); 16 | }; 17 | 18 | export const withCyTag = value => `[data-cy=${value}]`; 19 | -------------------------------------------------------------------------------- /express/data/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "item_type":"ezlocation", 3 | "min_selected":1, 4 | "max_selected":0, 5 | "has_tree":true, 6 | "has_search":true, 7 | "has_preview":true, 8 | "default_columns":["name","location_id","visible"], 9 | "available_columns":[ 10 | {"id":"name","name":"Name"}, 11 | {"id":"location_id","name":"Location ID"}, 12 | {"id":"content_id","name":"Content ID"}, 13 | {"id":"visible","name":"Visible"}, 14 | {"id":"priority","name":"Priority"} 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /express/data/items.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "location_id":2, 4 | "id":2, 5 | "value":2, 6 | "name":"Home", 7 | "visible":true, 8 | "selectable":true, 9 | "has_sub_items":true, 10 | "has_sub_locations": false, 11 | "parent_id": null, 12 | "preview": "

Title

Home
", 13 | "columns":{ 14 | "name":"Home", 15 | "location_id":"2", 16 | "id":2, 17 | "content_id":"11", 18 | "visible":"Yes", 19 | "priority":"0" 20 | } 21 | }, 22 | { 23 | "location_id":1, 24 | "id":1, 25 | "value":1, 26 | "name":"Users", 27 | "visible":true, 28 | "selectable":true, 29 | "has_sub_items":true, 30 | "has_sub_locations": false, 31 | "parent_id": null, 32 | "preview": "

Title

Users
", 33 | "columns":{ 34 | "name":"Users", 35 | "location_id":"1", 36 | "id":1, 37 | "content_id":"11", 38 | "visible":"Yes", 39 | "priority":"0" 40 | } 41 | }, 42 | { 43 | "location_id":100, 44 | "id":100, 45 | "value":100, 46 | "name":"Test item 100", 47 | "visible":true, 48 | "selectable":true, 49 | "has_sub_items":false, 50 | "has_sub_locations": false, 51 | "parent_id": 2, 52 | "preview": "

Title

Test item 100
", 53 | "columns":{ 54 | "name":"Test item 100", 55 | "location_id":"100", 56 | "id":1, 57 | "content_id":"200", 58 | "visible":"Yes", 59 | "priority":"0" 60 | } 61 | }, 62 | { 63 | "location_id":101, 64 | "id":101, 65 | "value":101, 66 | "name":"Test item 101", 67 | "visible":true, 68 | "selectable":true, 69 | "has_sub_items":true, 70 | "has_sub_locations": true, 71 | "parent_id": 2, 72 | "preview": "

Title

Test item 101
", 73 | "columns":{ 74 | "name":"Test item 101", 75 | "location_id":"101", 76 | "id":1, 77 | "content_id":"200", 78 | "visible":"Yes", 79 | "priority":"0" 80 | } 81 | }, 82 | { 83 | "location_id":102, 84 | "id":102, 85 | "value":102, 86 | "name":"Test item 102", 87 | "visible":true, 88 | "selectable":true, 89 | "has_sub_items":true, 90 | "has_sub_locations": false, 91 | "parent_id": 2, 92 | "preview": "

Title

Test item 102
", 93 | "columns":{ 94 | "name":"Test item 102", 95 | "location_id":"102", 96 | "id":1, 97 | "content_id":"202", 98 | "visible":"Yes", 99 | "priority":"0" 100 | } 101 | }, 102 | { 103 | "location_id":103, 104 | "id":103, 105 | "value":103, 106 | "name":"Test item 103", 107 | "visible":true, 108 | "selectable":true, 109 | "has_sub_items":false, 110 | "has_sub_locations": false, 111 | "parent_id": 2, 112 | "preview": "

Title

Test item 103
", 113 | "columns":{ 114 | "name":"Test item 103", 115 | "location_id":"103", 116 | "id":1, 117 | "content_id":"203", 118 | "visible":"Yes", 119 | "priority":"0" 120 | } 121 | }, 122 | { 123 | "location_id":104, 124 | "id":104, 125 | "value":104, 126 | "name":"Test item 104", 127 | "visible":true, 128 | "selectable":true, 129 | "has_sub_items":false, 130 | "has_sub_locations": false, 131 | "parent_id": 2, 132 | "preview": "

Title

Test item 104
", 133 | "columns":{ 134 | "name":"Test item 104", 135 | "location_id":"104", 136 | "id":1, 137 | "content_id":"204", 138 | "visible":"Yes", 139 | "priority":"0" 140 | } 141 | }, 142 | { 143 | "location_id":105, 144 | "id":105, 145 | "value":105, 146 | "name":"Test item 105", 147 | "visible":true, 148 | "selectable":true, 149 | "has_sub_items":false, 150 | "has_sub_locations": false, 151 | "parent_id": 2, 152 | "preview": "

Title

Test item 105
", 153 | "columns":{ 154 | "name":"Test item 105", 155 | "location_id":"105", 156 | "id":1, 157 | "content_id":"205", 158 | "visible":"Yes", 159 | "priority":"0" 160 | } 161 | }, 162 | { 163 | "location_id":106, 164 | "id":106, 165 | "value":106, 166 | "name":"Test item 106", 167 | "visible":true, 168 | "selectable":true, 169 | "has_sub_items":false, 170 | "has_sub_locations": false, 171 | "parent_id": 2, 172 | "preview": "

Title

Test item 106
", 173 | "columns":{ 174 | "name":"Test item 106", 175 | "location_id":"106", 176 | "id":1, 177 | "content_id":"206", 178 | "visible":"Yes", 179 | "priority":"0" 180 | } 181 | }, 182 | { 183 | "location_id":107, 184 | "id":107, 185 | "value":107, 186 | "name":"Test item 107", 187 | "visible":true, 188 | "selectable":true, 189 | "has_sub_items":false, 190 | "has_sub_locations": false, 191 | "parent_id": 2, 192 | "preview": "

Title

Test item 107
", 193 | "columns":{ 194 | "name":"Test item 107", 195 | "location_id":"107", 196 | "id":1, 197 | "content_id":"207", 198 | "visible":"Yes", 199 | "priority":"0" 200 | } 201 | }, 202 | { 203 | "location_id":108, 204 | "id":108, 205 | "value":108, 206 | "name":"Test item 108", 207 | "visible":true, 208 | "selectable":true, 209 | "has_sub_items":false, 210 | "has_sub_locations": false, 211 | "parent_id": 2, 212 | "preview": "

Title

Test item 108
", 213 | "columns":{ 214 | "name":"Test item 108", 215 | "location_id":"108", 216 | "id":1, 217 | "content_id":"208", 218 | "visible":"Yes", 219 | "priority":"0" 220 | } 221 | }, 222 | { 223 | "location_id":109, 224 | "id":109, 225 | "value":109, 226 | "name":"Test item 109", 227 | "visible":true, 228 | "selectable":true, 229 | "has_sub_items":false, 230 | "has_sub_locations": false, 231 | "parent_id": 2, 232 | "preview": "

Title

Test item 109
", 233 | "columns":{ 234 | "name":"Test item 109", 235 | "location_id":"109", 236 | "id":1, 237 | "content_id":"209", 238 | "visible":"Yes", 239 | "priority":"0" 240 | } 241 | }, 242 | { 243 | "location_id":110, 244 | "id":110, 245 | "value":110, 246 | "name":"Test item 110", 247 | "visible":true, 248 | "selectable":true, 249 | "has_sub_items":false, 250 | "has_sub_locations": false, 251 | "parent_id": 2, 252 | "preview": "

Title

Test item 110
", 253 | "columns":{ 254 | "name":"Test item 110", 255 | "location_id":"110", 256 | "id":1, 257 | "content_id":"210", 258 | "visible":"Yes", 259 | "priority":"0" 260 | } 261 | }, 262 | { 263 | "location_id":111, 264 | "id":111, 265 | "value":111, 266 | "name":"Test item 111", 267 | "visible":true, 268 | "selectable":true, 269 | "has_sub_items":false, 270 | "has_sub_locations": false, 271 | "parent_id": 2, 272 | "preview": "

Title

Test item 111
", 273 | "columns":{ 274 | "name":"Test item 111", 275 | "location_id":"111", 276 | "id":1, 277 | "content_id":"211", 278 | "visible":"Yes", 279 | "priority":"0" 280 | } 281 | }, 282 | { 283 | "location_id":112, 284 | "id":112, 285 | "value":112, 286 | "name":"Test item 112", 287 | "visible":true, 288 | "selectable":true, 289 | "has_sub_items":false, 290 | "has_sub_locations": false, 291 | "parent_id": 2, 292 | "preview": "

Title

Test item 112
", 293 | "columns":{ 294 | "name":"Test item 112", 295 | "location_id":"112", 296 | "id":1, 297 | "content_id":"212", 298 | "visible":"Yes", 299 | "priority":"0" 300 | } 301 | }, 302 | { 303 | "location_id":113, 304 | "id":113, 305 | "value":113, 306 | "name":"Test item 113", 307 | "visible":true, 308 | "selectable":true, 309 | "has_sub_items":false, 310 | "has_sub_locations": false, 311 | "parent_id": 101, 312 | "preview": "

Title

Test item 113
", 313 | "columns":{ 314 | "name":"Test item 113", 315 | "location_id":"113", 316 | "id":1, 317 | "content_id":"212", 318 | "visible":"Yes", 319 | "priority":"0" 320 | } 321 | }, 322 | { 323 | "location_id":114, 324 | "id":114, 325 | "value":114, 326 | "name":"Test item 114", 327 | "visible":true, 328 | "selectable":true, 329 | "has_sub_items":false, 330 | "has_sub_locations": false, 331 | "parent_id": 101, 332 | "preview": "

Title

Test item 114
", 333 | "columns":{ 334 | "name":"Test item 114", 335 | "location_id":"114", 336 | "id":1, 337 | "content_id":"212", 338 | "visible":"Yes", 339 | "priority":"0" 340 | } 341 | }, 342 | { 343 | "location_id":115, 344 | "id":115, 345 | "value":115, 346 | "name":"Test item 115", 347 | "visible":true, 348 | "selectable":true, 349 | "has_sub_items":false, 350 | "has_sub_locations": false, 351 | "parent_id": 101, 352 | "preview": "

Title

Test item 115
", 353 | "columns":{ 354 | "name":"Test item 115", 355 | "location_id":"115", 356 | "id":1, 357 | "content_id":"212", 358 | "visible":"Yes", 359 | "priority":"0" 360 | } 361 | }, 362 | { 363 | "location_id":116, 364 | "id":116, 365 | "value":116, 366 | "name":"Test item 116", 367 | "visible":true, 368 | "selectable":true, 369 | "has_sub_items":true, 370 | "has_sub_locations": false, 371 | "parent_id": 102, 372 | "preview": "

Title

Test item 116
", 373 | "columns":{ 374 | "name":"Test item 116", 375 | "location_id":"116", 376 | "id":1, 377 | "content_id":"212", 378 | "visible":"Yes", 379 | "priority":"0" 380 | } 381 | }, 382 | { 383 | "location_id":117, 384 | "id":117, 385 | "value":117, 386 | "name":"Test item 117", 387 | "visible":true, 388 | "selectable":true, 389 | "has_sub_items":false, 390 | "has_sub_locations": false, 391 | "parent_id": 116, 392 | "preview": "

Title

Test item 117
", 393 | "columns":{ 394 | "name":"Test item 117", 395 | "location_id":"117", 396 | "id":1, 397 | "content_id":"212", 398 | "visible":"Yes", 399 | "priority":"0" 400 | } 401 | }, 402 | { 403 | "location_id":118, 404 | "id":118, 405 | "value":118, 406 | "name":"Test item 118", 407 | "visible":true, 408 | "selectable":true, 409 | "has_sub_items":false, 410 | "has_sub_locations": false, 411 | "parent_id": 102, 412 | "preview": "

Title

Test item 118
", 413 | "columns":{ 414 | "name":"Test item 118", 415 | "location_id":"118", 416 | "id":1, 417 | "content_id":"212", 418 | "visible":"Yes", 419 | "priority":"0" 420 | } 421 | }, 422 | { 423 | "location_id":200, 424 | "id":200, 425 | "value":200, 426 | "name":"User item 200", 427 | "visible":true, 428 | "selectable":true, 429 | "has_sub_items":true, 430 | "has_sub_locations": false, 431 | "parent_id":1, 432 | "preview": "

Title

User item 200
", 433 | "columns":{ 434 | "name":"User item 200", 435 | "location_id":"200", 436 | "id":1, 437 | "content_id":"200", 438 | "visible":"Yes", 439 | "priority":"0" 440 | } 441 | }, 442 | { 443 | "location_id":202, 444 | "id":202, 445 | "value":202, 446 | "name":"User item 202", 447 | "visible":true, 448 | "selectable":true, 449 | "has_sub_items":true, 450 | "has_sub_locations": false, 451 | "parent_id":1, 452 | "preview": "

Title

User item 202
", 453 | "columns":{ 454 | "name":"User item 202", 455 | "location_id":"202", 456 | "id":1, 457 | "content_id":"202", 458 | "visible":"Yes", 459 | "priority":"0" 460 | } 461 | }, 462 | { 463 | "location_id":203, 464 | "id":203, 465 | "value":203, 466 | "name":"User item 203", 467 | "visible":true, 468 | "selectable":true, 469 | "has_sub_items":false, 470 | "has_sub_locations": false, 471 | "parent_id":1, 472 | "preview": "

Title

User item 203
", 473 | "columns":{ 474 | "name":"User item 203", 475 | "location_id":"203", 476 | "id":1, 477 | "content_id":"203", 478 | "visible":"Yes", 479 | "priority":"0" 480 | } 481 | }, 482 | { 483 | "location_id":204, 484 | "id":204, 485 | "value":204, 486 | "name":"User item 204", 487 | "visible":true, 488 | "selectable":true, 489 | "has_sub_items":false, 490 | "has_sub_locations": false, 491 | "parent_id":1, 492 | "preview": "

Title

User item 204
", 493 | "columns":{ 494 | "name":"User item 204", 495 | "location_id":"204", 496 | "id":1, 497 | "content_id":"204", 498 | "visible":"Yes", 499 | "priority":"0" 500 | } 501 | }, 502 | { 503 | "location_id":205, 504 | "id":205, 505 | "value":205, 506 | "name":"User item 205", 507 | "visible":true, 508 | "selectable":true, 509 | "has_sub_items":false, 510 | "has_sub_locations": false, 511 | "parent_id":1, 512 | "preview": "

Title

User item 205
", 513 | "columns":{ 514 | "name":"User item 205", 515 | "location_id":"205", 516 | "id":1, 517 | "content_id":"205", 518 | "visible":"Yes", 519 | "priority":"0" 520 | } 521 | }, 522 | { 523 | "location_id":206, 524 | "id":206, 525 | "value":206, 526 | "name":"User item 206", 527 | "visible":true, 528 | "selectable":true, 529 | "has_sub_items":false, 530 | "has_sub_locations": false, 531 | "parent_id":1, 532 | "preview": "

Title

User item 206
", 533 | "columns":{ 534 | "name":"User item 206", 535 | "location_id":"206", 536 | "id":1, 537 | "content_id":"206", 538 | "visible":"Yes", 539 | "priority":"0" 540 | } 541 | }, 542 | { 543 | "location_id":207, 544 | "id":207, 545 | "value":207, 546 | "name":"User item 207", 547 | "visible":true, 548 | "selectable":true, 549 | "has_sub_items":false, 550 | "has_sub_locations": false, 551 | "parent_id":1, 552 | "preview": "

Title

User item 207
", 553 | "columns":{ 554 | "name":"User item 207", 555 | "location_id":"207", 556 | "id":1, 557 | "content_id":"207", 558 | "visible":"Yes", 559 | "priority":"0" 560 | } 561 | }, 562 | { 563 | "location_id":208, 564 | "id":208, 565 | "value":208, 566 | "name":"User item 208", 567 | "visible":true, 568 | "selectable":true, 569 | "has_sub_items":false, 570 | "has_sub_locations": false, 571 | "parent_id":1, 572 | "preview": "

Title

User item 208
", 573 | "columns":{ 574 | "name":"User item 208", 575 | "location_id":"208", 576 | "id":1, 577 | "content_id":"208", 578 | "visible":"Yes", 579 | "priority":"0" 580 | } 581 | }, 582 | { 583 | "location_id":209, 584 | "id":209, 585 | "value":209, 586 | "name":"User item 209", 587 | "visible":true, 588 | "selectable":true, 589 | "has_sub_items":false, 590 | "has_sub_locations": false, 591 | "parent_id":1, 592 | "preview": "

Title

User item 209
", 593 | "columns":{ 594 | "name":"User item 209", 595 | "location_id":"209", 596 | "id":1, 597 | "content_id":"209", 598 | "visible":"Yes", 599 | "priority":"0" 600 | } 601 | }, 602 | { 603 | "location_id":210, 604 | "id":210, 605 | "value":210, 606 | "name":"User item 210", 607 | "visible":true, 608 | "selectable":true, 609 | "has_sub_items":false, 610 | "has_sub_locations": false, 611 | "parent_id":1, 612 | "preview": "

Title

User item 210
", 613 | "columns":{ 614 | "name":"User item 210", 615 | "location_id":"210", 616 | "id":1, 617 | "content_id":"210", 618 | "visible":"Yes", 619 | "priority":"0" 620 | } 621 | }, 622 | { 623 | "location_id":211, 624 | "id":211, 625 | "value":211, 626 | "name":"User item 211", 627 | "visible":true, 628 | "selectable":true, 629 | "has_sub_items":false, 630 | "has_sub_locations": false, 631 | "parent_id":1, 632 | "preview": "

Title

User item 211
", 633 | "columns":{ 634 | "name":"User item 211", 635 | "location_id":"211", 636 | "id":1, 637 | "content_id":"211", 638 | "visible":"Yes", 639 | "priority":"0" 640 | } 641 | }, 642 | { 643 | "location_id":212, 644 | "id":212, 645 | "value":212, 646 | "name":"User item 212", 647 | "visible":true, 648 | "selectable":true, 649 | "has_sub_items":false, 650 | "has_sub_locations": false, 651 | "parent_id":1, 652 | "preview": "

Title

User item 212
", 653 | "columns":{ 654 | "name":"User item 212", 655 | "location_id":"212", 656 | "id":1, 657 | "content_id":"212", 658 | "visible":"Yes", 659 | "priority":"0" 660 | } 661 | } 662 | ] 663 | -------------------------------------------------------------------------------- /express/helpers.js: -------------------------------------------------------------------------------- 1 | const items = require('./data/items.json'); 2 | const config = require('./data/config.json'); 3 | 4 | const getItem = (id) => { 5 | return items.find(item => item.location_id === id); 6 | } 7 | 8 | const getChildrenItems = (req) => { 9 | const parentLocation = parseInt(req.params.id, 10); 10 | let children = []; 11 | items.forEach(item => { 12 | if (item.parent_id === parentLocation) { 13 | const newItem = {...item}; 14 | delete newItem.id; 15 | delete newItem.parent_id; 16 | delete newItem.has_sub_locations; 17 | delete newItem.preview; 18 | children.push(newItem); 19 | } 20 | }); 21 | const children_count = children.length; 22 | const parent = getItem(parentLocation); 23 | const path = getPath(parent, []); 24 | const page = req.query.page || 1; 25 | const limit = req.query.limit || children_count; 26 | children = children.slice((page - 1) * limit, page * limit); 27 | const data = { 28 | path, 29 | parent, 30 | children, 31 | children_count, 32 | }; 33 | return data; 34 | } 35 | 36 | const getLocationItems = (req) => { 37 | const parentLocation = parseInt(req.params.id, 10); 38 | const children = []; 39 | items.forEach(item => { 40 | if (item.parent_id === parentLocation) { 41 | const newItem = {...item}; 42 | delete newItem.selectable; 43 | delete newItem.value; 44 | delete newItem.location_id; 45 | delete newItem.columns; 46 | delete newItem.preview; 47 | newItem.columns = { 48 | "name": item.columns.name, 49 | }; 50 | children.push(newItem); 51 | } 52 | }); 53 | return { 54 | children, 55 | }; 56 | } 57 | 58 | const getSections = () => { 59 | const sections = []; 60 | items.forEach((item) => { 61 | if (item.parent_id === null) { 62 | sections.push({ 63 | "id":item.id, 64 | "parent_id":item.parent_id, 65 | "name":item.name, 66 | "has_sub_items":item.has_sub_items, 67 | "has_sub_locations":item.has_sub_locations, 68 | "visible":item.visible, 69 | "columns":{ 70 | "name":item.columns.name, 71 | }, 72 | }); 73 | } 74 | }); 75 | return sections; 76 | } 77 | 78 | const getPath = (item, path) => { 79 | path.unshift({ 80 | "id":item.id, 81 | "name": item.name, 82 | }); 83 | if (item.parent_id) { 84 | getPath(getItem(item.parent_id), path); 85 | } 86 | return path; 87 | } 88 | 89 | const getConfig = () => { 90 | const sections = getSections(); 91 | return { 92 | ...config, 93 | sections, 94 | }; 95 | } 96 | 97 | const getPreview = (req) => { 98 | const id = parseInt(req.params.id, 10); 99 | const item = getItem(id); 100 | return item.preview; 101 | } 102 | 103 | const getSearchResults = (req) => { 104 | const parentLocation = parseInt(req.query.sectionId, 10); 105 | let children = []; 106 | if (req.query.searchText.length > 2) items.forEach(item => { 107 | if (children.length < 12 && item.parent_id === parentLocation) { 108 | const newItem = {...item}; 109 | delete newItem.id; 110 | delete newItem.parent_id; 111 | delete newItem.has_sub_locations; 112 | delete newItem.preview; 113 | children.push(newItem); 114 | } 115 | }); 116 | const children_count = children.length; 117 | const page = req.query.page || 1; 118 | const limit = req.query.limit || children_count; 119 | children = children.slice((page - 1) * limit, page * limit); 120 | const data = { 121 | children, 122 | children_count, 123 | }; 124 | return data; 125 | } 126 | 127 | module.exports = { 128 | getChildrenItems, 129 | getLocationItems, 130 | getPath, 131 | getConfig, 132 | getItem, 133 | getPreview, 134 | getSearchResults, 135 | } 136 | -------------------------------------------------------------------------------- /express/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Netgen Content Browser 9 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
27 |
28 | 34 |
35 | 36 | 37 | 38 |
39 | clear 40 | 41 | 42 | (No item selected) 43 | 44 |
45 |
46 |
47 | 48 |
49 |
59 | 64 | 65 | 66 | 67 |
68 |
69 | 70 |
71 |
76 |
77 |
78 | (NO ITEMS SELECTED) 79 |
80 |
81 | 82 | Add items 83 |
84 |
85 |
86 | 87 | 88 | -------------------------------------------------------------------------------- /express/server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const model = require('./helpers'); 4 | 5 | const app = express(); 6 | 7 | app.get('/cb/api/:name/config', function (req, res) { 8 | const data = model.getConfig(req); 9 | res.send(data); 10 | }); 11 | 12 | app.get('/cb/api/:name/browse/:id/items', function (req, res) { 13 | const data = model.getChildrenItems(req); 14 | res.send(data); 15 | }); 16 | 17 | app.get('/cb/api/:name/browse/:id/locations', function (req, res) { 18 | const data = model.getLocationItems(req); 19 | res.send(data); 20 | }); 21 | 22 | app.get('/cb/api/:name/render/:id', function (req, res) { 23 | const data = model.getPreview(req); 24 | res.send(data); 25 | }); 26 | 27 | app.get('/cb/api/:name/search', function (req, res) { 28 | const data = model.getSearchResults(req); 29 | res.send(data); 30 | }); 31 | 32 | app.use(express.static(path.join(__dirname, '../bundle/Resources/public'), { 33 | index: false 34 | })); 35 | 36 | app.get('*', function(req, res) { 37 | res.sendFile(path.join(__dirname, 'public', 'index.html')); 38 | }); 39 | 40 | app.listen('8282'); 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@netgen/content-browser-ui", 3 | "description": "Netgen Content Browser user interface", 4 | "version": "1.4.0", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/netgen-layouts/content-browser-ui.git" 8 | }, 9 | "private": true, 10 | "main": "bundle/Resources/public/js/main.js", 11 | "scripts": { 12 | "start": "nodemon --watch .env --watch config --watch scripts node scripts/start.js", 13 | "build": "node scripts/build.js", 14 | "cypress": "cypress open", 15 | "express": "nodemon express/server.js -V -w express", 16 | "test": "CYPRESS_baseUrl=http://localhost:8282 cypress run", 17 | "ci": "start-test 'node express/server.js' 8282 test" 18 | }, 19 | "eslintConfig": { 20 | "extends": "react-app" 21 | }, 22 | "browserslist": [ 23 | ">0.2%", 24 | "not dead", 25 | "not ie <= 10", 26 | "not op_mini all" 27 | ], 28 | "babel": { 29 | "presets": [ 30 | "@babel/react", 31 | "@babel/env" 32 | ] 33 | }, 34 | "devDependencies": { 35 | "@babel/core": "^7.7.5", 36 | "@svgr/webpack": "^4.3.3", 37 | "babel-core": "^6.26.3", 38 | "babel-eslint": "^10.0.3", 39 | "babel-loader": "^8.0.6", 40 | "babel-plugin-named-asset-import": "^0.3.5", 41 | "babel-preset-react-app": "^9.1.0", 42 | "css-loader": "^3.4.0", 43 | "cypress": "^3.8.0", 44 | "dotenv": "^8.2.0", 45 | "dotenv-expand": "^5.1.0", 46 | "eslint": "^6.7.2", 47 | "eslint-config-react-app": "^5.1.0", 48 | "eslint-loader": "^3.0.3", 49 | "eslint-plugin-flowtype": "^4.5.2", 50 | "eslint-plugin-import": "^2.19.1", 51 | "eslint-plugin-jsx-a11y": "^6.2.3", 52 | "eslint-plugin-react": "^7.17.0", 53 | "eslint-plugin-react-hooks": "^2.3.0", 54 | "express": "^4.17.1", 55 | "file-loader": "^5.0.2", 56 | "html-webpack-plugin": "^3.2.0", 57 | "http-proxy-middleware": "^0.20.0", 58 | "mini-css-extract-plugin": "^0.8.1", 59 | "nodemon": "^2.0.2", 60 | "optimize-css-assets-webpack-plugin": "^5.0.3", 61 | "postcss-flexbugs-fixes": "^4.1.0", 62 | "postcss-loader": "^3.0.0", 63 | "postcss-preset-env": "^6.7.0", 64 | "postcss-safe-parser": "^4.0.1", 65 | "sass-loader": "^8.0.0", 66 | "start-server-and-test": "^1.10.6", 67 | "style-loader": "^1.0.2", 68 | "terser-webpack-plugin": "^2.3.1", 69 | "url-loader": "^3.0.0", 70 | "webpack": "^4.41.3", 71 | "webpack-dev-server": "^3.9.0" 72 | }, 73 | "dependencies": { 74 | "@material-ui/core": "^4.8.0", 75 | "@material-ui/icons": "^4.5.1", 76 | "cross-fetch": "^3.0.4", 77 | "react": "^16.12.0", 78 | "react-addons-css-transition-group": "^15.6.2", 79 | "react-app-polyfill": "^1.0.5", 80 | "react-dev-utils": "^10.0.0", 81 | "react-dom": "^16.12.0", 82 | "react-redux": "^7.1.3", 83 | "react-transition-group": "^4.3.0", 84 | "redux": "^4.0.4", 85 | "redux-devtools-extension": "^2.13.8", 86 | "redux-thunk": "^2.3.0" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Netgen Content Browser 9 | 15 | 16 | 17 | 18 |
19 |
20 |
26 |
27 | 33 |
34 | 35 | 36 | 37 |
38 | clear 39 | 40 | 41 | (No item selected) 42 | 43 |
44 |
45 |
46 | 47 |
48 |
58 | 63 | 64 | 65 | 66 |
67 |
68 | 69 |
70 |
75 |
76 |
77 | (NO ITEMS SELECTED) 78 |
79 |
80 | 81 | Add items 82 |
83 |
84 |
85 | 86 | 87 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | // Do this as the first thing so that any code reading it knows the right env. 2 | process.env.BABEL_ENV = 'production'; 3 | process.env.NODE_ENV = 'production'; 4 | 5 | process.on('unhandledRejection', err => { 6 | throw err; 7 | }); 8 | 9 | // Ensure environment variables are read. 10 | require('../config/env'); 11 | 12 | const chalk = require('react-dev-utils/chalk'); 13 | const webpack = require('webpack'); 14 | const configFactory = require('../config/webpack.config'); 15 | const paths = require('../config/paths'); 16 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 17 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); 18 | const printBuildError = require('react-dev-utils/printBuildError'); 19 | 20 | const measureFileSizesBeforeBuild = 21 | FileSizeReporter.measureFileSizesBeforeBuild; 22 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 23 | 24 | // These sizes are pretty large. We'll warn for bundles exceeding them. 25 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; 26 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; 27 | 28 | // Generate configuration 29 | const config = configFactory('production'); 30 | 31 | measureFileSizesBeforeBuild(paths.appBuild) 32 | .then(previousFileSizes => { 33 | // Start the webpack build 34 | return build(previousFileSizes); 35 | }) 36 | .then( 37 | ({ stats, previousFileSizes, warnings }) => { 38 | if (warnings.length) { 39 | console.log(chalk.yellow('Compiled with warnings.\n')); 40 | console.log(warnings.join('\n\n')); 41 | console.log( 42 | '\nSearch for the ' + 43 | chalk.underline(chalk.yellow('keywords')) + 44 | ' to learn more about each warning.' 45 | ); 46 | console.log( 47 | 'To ignore, add ' + 48 | chalk.cyan('// eslint-disable-next-line') + 49 | ' to the line before.\n' 50 | ); 51 | } else { 52 | console.log(chalk.green('Compiled successfully.\n')); 53 | } 54 | 55 | console.log('File sizes after gzip:\n'); 56 | printFileSizesAfterBuild( 57 | stats, 58 | previousFileSizes, 59 | paths.appBuild, 60 | WARN_AFTER_BUNDLE_GZIP_SIZE, 61 | WARN_AFTER_CHUNK_GZIP_SIZE 62 | ); 63 | console.log(); 64 | }, 65 | err => { 66 | console.log(chalk.red('Failed to compile.\n')); 67 | printBuildError(err); 68 | process.exit(1); 69 | } 70 | ) 71 | .catch(err => { 72 | if (err && err.message) { 73 | console.log(err.message); 74 | } 75 | process.exit(1); 76 | }); 77 | 78 | // Create the production build and print the deployment instructions. 79 | function build(previousFileSizes) { 80 | console.log('Creating an optimized production build...'); 81 | 82 | let compiler = webpack(config); 83 | return new Promise((resolve, reject) => { 84 | compiler.run((err, stats) => { 85 | let messages; 86 | if (err) { 87 | if (!err.message) { 88 | return reject(err); 89 | } 90 | messages = formatWebpackMessages({ 91 | errors: [err.message], 92 | warnings: [], 93 | }); 94 | } else { 95 | messages = formatWebpackMessages( 96 | stats.toJson({ all: false, warnings: true, errors: true }) 97 | ); 98 | } 99 | if (messages.errors.length) { 100 | // Only keep the first error. Others are often indicative 101 | // of the same problem, but confuse the reader with noise. 102 | if (messages.errors.length > 1) { 103 | messages.errors.length = 1; 104 | } 105 | return reject(new Error(messages.errors.join('\n\n'))); 106 | } 107 | if ( 108 | process.env.CI && 109 | (typeof process.env.CI !== 'string' || 110 | process.env.CI.toLowerCase() !== 'false') && 111 | messages.warnings.length 112 | ) { 113 | console.log( 114 | chalk.yellow( 115 | '\nTreating warnings as errors because process.env.CI = true.\n' + 116 | 'Most CI servers set it automatically.\n' 117 | ) 118 | ); 119 | return reject(new Error(messages.warnings.join('\n\n'))); 120 | } 121 | 122 | const resolveArgs = { 123 | stats, 124 | previousFileSizes, 125 | warnings: messages.warnings, 126 | }; 127 | 128 | return resolve(resolveArgs); 129 | }); 130 | }); 131 | } 132 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | // Do this as the first thing so that any code reading it knows the right env. 2 | process.env.BABEL_ENV = 'development'; 3 | process.env.NODE_ENV = 'development'; 4 | 5 | // Makes the script crash on unhandled rejections instead of silently 6 | // ignoring them. In the future, promise rejections that are not handled will 7 | // terminate the Node.js process with a non-zero exit code. 8 | process.on('unhandledRejection', err => { 9 | throw err; 10 | }); 11 | 12 | // Ensure environment variables are read. 13 | require('../config/env'); 14 | 15 | const chalk = require('react-dev-utils/chalk'); 16 | const webpack = require('webpack'); 17 | const WebpackDevServer = require('webpack-dev-server'); 18 | const { 19 | choosePort, 20 | createCompiler, 21 | prepareProxy, 22 | prepareUrls, 23 | } = require('react-dev-utils/WebpackDevServerUtils'); 24 | const openBrowser = require('react-dev-utils/openBrowser'); 25 | const paths = require('../config/paths'); 26 | const configFactory = require('../config/webpack.config'); 27 | const createDevServerConfig = require('../config/webpackDevServer.config'); 28 | 29 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 30 | const HOST = process.env.HOST || '0.0.0.0'; 31 | 32 | choosePort(HOST, DEFAULT_PORT) 33 | .then(port => { 34 | if (port == null) { 35 | // We have not found a port. 36 | return; 37 | } 38 | const config = configFactory('development'); 39 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 40 | const appName = require(paths.appPackageJson).name; 41 | const urls = prepareUrls(protocol, HOST, port); 42 | const devSocket = { 43 | warnings: warnings => 44 | devServer.sockWrite(devServer.sockets, 'warnings', warnings), 45 | errors: errors => 46 | devServer.sockWrite(devServer.sockets, 'errors', errors), 47 | }; 48 | // Create a webpack compiler that is configured with custom messages. 49 | const compiler = createCompiler({ 50 | appName, 51 | config, 52 | devSocket, 53 | urls, 54 | webpack, 55 | }); 56 | // Load proxy config 57 | const proxySetting = require(paths.appPackageJson).proxy; 58 | const proxyConfig = prepareProxy(proxySetting, paths.appPublic); 59 | // Serve webpack assets generated by the compiler over a web server. 60 | const serverConfig = createDevServerConfig( 61 | proxyConfig, 62 | urls.lanUrlForConfig 63 | ); 64 | const devServer = new WebpackDevServer(compiler, serverConfig); 65 | // Launch WebpackDevServer. 66 | devServer.listen(port, HOST, err => { 67 | if (err) { 68 | return console.log(err); 69 | } 70 | console.log(chalk.cyan('Starting the development server...\n')); 71 | openBrowser(urls.localUrlForBrowser); 72 | }); 73 | 74 | ['SIGINT', 'SIGTERM'].forEach(function(sig) { 75 | process.on(sig, function() { 76 | devServer.close(); 77 | process.exit(); 78 | }); 79 | }); 80 | }) 81 | .catch(err => { 82 | if (err && err.message) { 83 | console.log(err.message); 84 | } 85 | process.exit(1); 86 | }); 87 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Browser from './containers/Browser'; 3 | import { Provider as ReduxProvider } from 'react-redux'; 4 | import configureStore from './store/store'; 5 | import { initialSetup } from './store/actions/app'; 6 | 7 | function App({ overrides = {}, onCancel, onConfirm, itemType, disabledItems = [] }) { 8 | const reduxStore = configureStore(); 9 | reduxStore.dispatch(initialSetup({ 10 | itemType, 11 | onCancel, 12 | onConfirm, 13 | config: overrides, 14 | disabledItems, 15 | itemsLimit: localStorage.getItem('cb_itemsLimit') || 10, 16 | showPreview: localStorage.getItem('cb_showPreview') ? JSON.parse(localStorage.getItem('cb_showPreview')) : false, 17 | })) 18 | 19 | return ( 20 | 21 | 22 | 23 | ); 24 | } 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /src/App.module.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/src/App.module.css -------------------------------------------------------------------------------- /src/assets/fonts/Roboto/Roboto-300.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/src/assets/fonts/Roboto/Roboto-300.eot -------------------------------------------------------------------------------- /src/assets/fonts/Roboto/Roboto-300.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/src/assets/fonts/Roboto/Roboto-300.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Roboto/Roboto-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/src/assets/fonts/Roboto/Roboto-300.woff -------------------------------------------------------------------------------- /src/assets/fonts/Roboto/Roboto-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/src/assets/fonts/Roboto/Roboto-300.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Roboto/Roboto-500.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/src/assets/fonts/Roboto/Roboto-500.eot -------------------------------------------------------------------------------- /src/assets/fonts/Roboto/Roboto-500.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/src/assets/fonts/Roboto/Roboto-500.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Roboto/Roboto-500.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/src/assets/fonts/Roboto/Roboto-500.woff -------------------------------------------------------------------------------- /src/assets/fonts/Roboto/Roboto-500.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/src/assets/fonts/Roboto/Roboto-500.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Roboto/Roboto-700.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/src/assets/fonts/Roboto/Roboto-700.eot -------------------------------------------------------------------------------- /src/assets/fonts/Roboto/Roboto-700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/src/assets/fonts/Roboto/Roboto-700.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Roboto/Roboto-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/src/assets/fonts/Roboto/Roboto-700.woff -------------------------------------------------------------------------------- /src/assets/fonts/Roboto/Roboto-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/src/assets/fonts/Roboto/Roboto-700.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/Roboto/Roboto-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/src/assets/fonts/Roboto/Roboto-regular.eot -------------------------------------------------------------------------------- /src/assets/fonts/Roboto/Roboto-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/src/assets/fonts/Roboto/Roboto-regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Roboto/Roboto-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/src/assets/fonts/Roboto/Roboto-regular.woff -------------------------------------------------------------------------------- /src/assets/fonts/Roboto/Roboto-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netgen-layouts/content-browser-ui/df625ae599d9c71aac2493ca7fbd03cef4d781f1/src/assets/fonts/Roboto/Roboto-regular.woff2 -------------------------------------------------------------------------------- /src/components/Breadcrumbs.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from './utils/Button'; 3 | import S from './Breadcrumbs.module.css'; 4 | 5 | function Breadcrumbs({ items, setId }) { 6 | if (!items) { 7 | return ''; 8 | } else { 9 | return ( 10 | 21 | ); 22 | } 23 | } 24 | 25 | export default Breadcrumbs; 26 | -------------------------------------------------------------------------------- /src/components/Breadcrumbs.module.css: -------------------------------------------------------------------------------- 1 | .breadcrumbs { 2 | list-style-type: none; 3 | margin: 0; 4 | padding: 0; 5 | display: flex; 6 | align-items: flex-start; 7 | flex-wrap: wrap; 8 | } 9 | 10 | .breadcrumbs li { 11 | display: inline-block; 12 | font-size: .75em; 13 | margin: 0; 14 | padding: 0; 15 | } 16 | 17 | .breadcrumbs li + li { 18 | position: relative; 19 | padding-left: 1.5em; 20 | } 21 | 22 | .breadcrumbs li + li::before { 23 | content: '/'; 24 | display: inline-block; 25 | margin: 0 .5em; 26 | position: absolute; 27 | left: 0; 28 | top: .125em; 29 | } 30 | -------------------------------------------------------------------------------- /src/components/Browser.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { CSSTransition } from 'react-transition-group'; 3 | import Tree from '../containers/Tree'; 4 | import Items from '../containers/Items'; 5 | import Search from '../containers/Search'; 6 | import Tabs from './Tabs'; 7 | import TogglePreview from './TogglePreview'; 8 | import Footer from './Footer'; 9 | import ListIcon from '@material-ui/icons/List'; 10 | import SearchIcon from '@material-ui/icons/Search'; 11 | import Loader from './utils/Loader'; 12 | import S from './Browser.module.css'; 13 | 14 | function BrowserContent(props) { 15 | return ( 16 |
17 | : ''}> 18 |
}> 19 |
20 | {props.config.has_tree ? 21 |
22 | 23 |
24 | : null 25 | } 26 | 27 |
28 |
29 | {props.config.has_search && 30 | 35 | } 36 |
37 |
38 |
39 | ); 40 | } 41 | 42 | function Browser(props) { 43 | useEffect(() => { 44 | window.addEventListener('keydown', handleKeypress); 45 | }); 46 | useEffect(() => { 47 | return () => { 48 | window.removeEventListener('keydown', handleKeypress); 49 | } 50 | }); 51 | 52 | const handleKeypress = (e) => { 53 | e.keyCode === 27 && props.onCancel(); 54 | } 55 | 56 | return ( 57 |
58 | 71 |
72 | {props.error ? 73 |
Error: {props.error.message}
74 | : 75 | !props.isLoaded ? 76 |
77 | : 78 | 79 | } 80 |
81 |
82 |
83 | ); 84 | } 85 | 86 | export default Browser; 87 | -------------------------------------------------------------------------------- /src/components/Browser.module.css: -------------------------------------------------------------------------------- 1 | @value variables: './Variables.module.css'; 2 | @value gray60, gray20 from variables; 3 | 4 | .browser { 5 | position: fixed; 6 | left: 0; 7 | top: 0; 8 | width: 100%; 9 | height: 100%; 10 | z-index: 1000; 11 | background: rgba(0, 0, 0, .5); 12 | color: gray20; 13 | font-family: 'Roboto', 'Helvetica Neue', sans-serif; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | overflow-x: hidden; 17 | font-size: 16px; 18 | line-height: 1.2; 19 | } 20 | 21 | .browser * { 22 | box-sizing: border-box; 23 | } 24 | 25 | .dialog { 26 | padding: 1em; 27 | height: 100%; 28 | display: flex; 29 | flex-direction: column; 30 | } 31 | 32 | .content { 33 | background: #fff; 34 | box-shadow: 0 .5em 1em rgba(0, 0, 0, .5); 35 | flex: 1; 36 | display: flex; 37 | flex-direction: column; 38 | position: relative; 39 | } 40 | 41 | .panels { 42 | display: flex; 43 | flex: 1; 44 | } 45 | 46 | .treePanel { 47 | flex: 0 0 25%; 48 | padding: 0 1em; 49 | display: flex; 50 | flex-direction: column; 51 | position: relative; 52 | max-height: 100%; 53 | overflow-y: auto; 54 | } 55 | 56 | .loading { 57 | background: gray20; 58 | color: gray60; 59 | flex: 1; 60 | box-shadow: 0 .5em 1em rgba(0, 0, 0, .5); 61 | } 62 | 63 | .slideEnter { 64 | opacity: .01; 65 | transform: translate3d(0, -10%, 0); 66 | } 67 | 68 | .slideActiveEnter { 69 | opacity: 1; 70 | transform: translate3d(0, 0, 0); 71 | transition: opacity .25s, transform .5s; 72 | } 73 | -------------------------------------------------------------------------------- /src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from './utils/Button'; 3 | import FooterItem from './FooterItem'; 4 | import S from './Footer.module.css'; 5 | import { connect } from 'react-redux'; 6 | import { setSelectedItem } from '../store/actions/app'; 7 | 8 | const mapsStateToProps = state => ({ 9 | selectedItems: state.app.selectedItems, 10 | min_selected: parseInt(state.app.config.min_selected, 10), 11 | onCancel: state.app.onCancel, 12 | onConfirm: state.app.onConfirm, 13 | }); 14 | 15 | const mapDispatchToProps = dispatch => ({ 16 | setSelectedItem: (id, toggle) => { 17 | dispatch(setSelectedItem(id, toggle)); 18 | }, 19 | }); 20 | 21 | function Footer(props) { 22 | return ( 23 |
24 |
25 | {props.selectedItems.map(item => props.setSelectedItem(item, false)} />)} 26 |
27 |
28 | 29 | 30 |
31 |
32 | ); 33 | } 34 | 35 | export default connect( 36 | mapsStateToProps, 37 | mapDispatchToProps 38 | )(Footer); 39 | -------------------------------------------------------------------------------- /src/components/Footer.module.css: -------------------------------------------------------------------------------- 1 | @value variables: './Variables.module.css'; 2 | @value gray22, gray60 from variables; 3 | 4 | .footer { 5 | background: gray22; 6 | color: #fff; 7 | padding: .75em; 8 | display: flex; 9 | justify-content: space-between; 10 | } 11 | 12 | .actions { 13 | min-width: 11em; 14 | text-align: right; 15 | } 16 | 17 | .footer button { 18 | margin: .25em; 19 | } 20 | 21 | .itemIcon { 22 | margin-left: .5em; 23 | color: gray60; 24 | font-size: 1.25em; 25 | display: inline-flex; 26 | } 27 | -------------------------------------------------------------------------------- /src/components/FooterItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from './utils/Button'; 3 | import ClearIcon from '@material-ui/icons/Clear'; 4 | import S from './Footer.module.css'; 5 | 6 | function FooterItem({ item, onClick }) { 7 | return ( 8 | 14 | ); 15 | } 16 | 17 | export default FooterItem; 18 | -------------------------------------------------------------------------------- /src/components/Item.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from './utils/Button'; 3 | import Checkbox from './utils/Checkbox'; 4 | import VisibilityOffIcon from '@material-ui/icons/VisibilityOff'; 5 | import S from './ItemsTable.module.css'; 6 | import { connect } from 'react-redux'; 7 | import { setSelectedItem } from '../store/actions/app'; 8 | 9 | const mapsStateToProps = state => ({ 10 | selectedItems: state.app.selectedItems, 11 | max_selected: parseInt(state.app.config.max_selected, 10), 12 | disabledItems: state.app.disabledItems, 13 | }); 14 | 15 | const mapDispatchToProps = dispatch => ({ 16 | setSelectedItem: (id, checked) => { 17 | dispatch(setSelectedItem(id, checked)); 18 | }, 19 | }); 20 | 21 | function Item(props) { 22 | const { item } = props; 23 | const isItemDisabled = props.disabledItems.indexOf(item.value) > -1; 24 | const isChecked = isItemDisabled || props.selectedItems.findIndex(selectedItem => selectedItem.value === item.value) > -1; 25 | const isDisabled = !item.selectable || isItemDisabled || (!isChecked && props.max_selected !== 0 && props.max_selected > 1 && props.selectedItems.length >= props.max_selected); 26 | return ( 27 | {if (item.value) props.setPreviewItem(item.value)}} className={props.previewItem === item.value ? S.activeRow : undefined}> 28 | 29 | 30 | props.setSelectedItem(item, e.target.checked)} 34 | checked={isChecked} 35 | disabled={isDisabled} 36 | /> 37 | 38 | {!item.visible && } 39 | {item.has_sub_items && props.setId ? : {item.name}} 40 | 41 | {props.columns.map((column) => { 42 | if (column.id === 'name') return false; 43 | return ; 44 | })} 45 | 46 | ); 47 | } 48 | 49 | export default connect( 50 | mapsStateToProps, 51 | mapDispatchToProps 52 | )(Item); 53 | -------------------------------------------------------------------------------- /src/components/Items.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CSSTransition } from 'react-transition-group'; 3 | import ItemsTable from './ItemsTable'; 4 | import Breadcrumbs from './Breadcrumbs'; 5 | import TableSettings from './TableSettings'; 6 | import Preview from './Preview'; 7 | import Loader from './utils/Loader'; 8 | import S from './Items.module.css'; 9 | 10 | function ItemsContent(props) { 11 | if (!props.items) { 12 | return ''; 13 | } else if (props.isLoading) { 14 | return ; 15 | } else { 16 | return ( 17 | 30 |
31 |
32 | 33 |
34 | 35 |
36 |
37 | 38 |
39 |
40 | ); 41 | } 42 | } 43 | 44 | function Items(props) { 45 | return ( 46 | <> 47 |
48 | 49 |
50 | 51 | 52 | ); 53 | 54 | } 55 | 56 | export default Items; 57 | -------------------------------------------------------------------------------- /src/components/Items.module.css: -------------------------------------------------------------------------------- 1 | @value variables: './Variables.module.css'; 2 | @value gray93 from variables; 3 | 4 | .items { 5 | flex: 1; 6 | background: gray93; 7 | position: relative; 8 | max-height: 100%; 9 | } 10 | 11 | .header { 12 | display: flex; 13 | padding: .75em 1em; 14 | justify-content: space-between; 15 | align-items: center; 16 | } 17 | 18 | .fadeEnter { 19 | opacity: .01; 20 | } 21 | 22 | .fadeActiveEnter { 23 | opacity: 1; 24 | transition: opacity .2s; 25 | } 26 | 27 | .fadeExit { 28 | opacity: 1; 29 | } 30 | 31 | .fadeActiveExit { 32 | opacity: .01; 33 | transition: opacity .2s; 34 | } 35 | 36 | .wrapper { 37 | position: absolute; 38 | left: 0; 39 | top: 0; 40 | right: 0; 41 | bottom: 0; 42 | overflow-y: auto; 43 | } 44 | 45 | .settings { 46 | flex: 1; 47 | text-align: right; 48 | } 49 | -------------------------------------------------------------------------------- /src/components/ItemsTable.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Item from './Item'; 4 | import Pager from './utils/Pager'; 5 | import S from './ItemsTable.module.css'; 6 | 7 | const mapsStateToProps = state => ({ 8 | availableColumns: state.app.config.available_columns, 9 | activeColumns: state.app.activeColumns, 10 | }); 11 | 12 | function ItemsTable(props) { 13 | const visibleColumns = props.availableColumns.filter(column => props.activeColumns.includes(column.id)); 14 | const showParent = props.showParentItem && props.items.parent && !!props.items.parent.value; 15 | 16 | let className = S.table; 17 | if (showParent) className += ` ${S.indent}`; 18 | 19 | if (!props.items.children) { 20 | return ''; 21 | } else { 22 | return ( 23 | <> 24 | 25 | 26 | 27 | {visibleColumns.map((column) => )} 28 | 29 | {showParent && 35 | } 36 | 37 | 38 | {props.items.children.map(child => ( 39 | 47 | ))} 48 | 49 |
{column.name}
50 | 55 | 56 | ); 57 | } 58 | } 59 | 60 | export default connect( 61 | mapsStateToProps, 62 | )(ItemsTable); 63 | -------------------------------------------------------------------------------- /src/components/ItemsTable.module.css: -------------------------------------------------------------------------------- 1 | .table { 2 | width: 100%; 3 | border-collapse: collapse; 4 | border-spacing: 0; 5 | border-bottom: 1px solid #cccccc; 6 | } 7 | 8 | .table th, 9 | .table td { 10 | padding: .5em .5em .5em 0; 11 | font-size: .875em; 12 | line-height: 1.3; 13 | vertical-align: middle; 14 | transition: background-color .25s; 15 | } 16 | 17 | .table th { 18 | background: #999999; 19 | color: #fff; 20 | font-size: .75em; 21 | text-transform: uppercase; 22 | font-weight: 400; 23 | text-align: left; 24 | } 25 | 26 | .table thead td { 27 | border-bottom: 1px solid #cccccc; 28 | } 29 | 30 | .table thead th, 31 | .table thead td { 32 | padding-top: .8333333333em; 33 | padding-bottom: .8333333333em; 34 | } 35 | 36 | .table tr { 37 | cursor: default; 38 | background: transparent; 39 | } 40 | 41 | .table tr > th:first-child, 42 | .table tr > td:first-child { 43 | padding-left: 1em; 44 | } 45 | 46 | .table tbody tr:hover td { 47 | background: #f2f2f2; 48 | } 49 | 50 | .table tbody tr.activeRow td { 51 | background: #e0e0e0; 52 | } 53 | 54 | .table.indent tbody td:first-child { 55 | padding-left: 3em; 56 | } 57 | 58 | .checkbox { 59 | font-size: 1.125em; 60 | display: inline-flex; 61 | vertical-align: middle; 62 | margin-right: .25em; 63 | } 64 | 65 | .table td img { 66 | display: block; 67 | max-width: 4.2857142857em; 68 | max-height: 4.2857142857em; 69 | padding: 2px; 70 | background-color: #fff; 71 | } 72 | 73 | .invisibleIcon { 74 | font-size: 1.1428571429em; 75 | display: inline-flex; 76 | vertical-align: middle; 77 | margin-right: .5em; 78 | } 79 | -------------------------------------------------------------------------------- /src/components/Preview.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import Loader from './utils/Loader'; 3 | import { CSSTransition } from 'react-transition-group'; 4 | import S from './Preview.module.css'; 5 | import { connect } from 'react-redux'; 6 | import { fetchPreview } from '../store/actions/app'; 7 | 8 | const mapsStateToProps = state => ({ 9 | previews: state.app.previews, 10 | showPreview: state.app.showPreview, 11 | isLoading: state.app.isPreviewLoading, 12 | has_preview: state.app.config.has_preview, 13 | }); 14 | 15 | const mapDispatchToProps = dispatch => ({ 16 | fetchPreview: (id) => { 17 | dispatch(fetchPreview(id)); 18 | }, 19 | }); 20 | 21 | function PreviewContent(props) { 22 | if (props.isLoading) { 23 | return 24 | } else if (props.previewItem === null || !props.previews[props.previewItem]) { 25 | return ( 26 |
This item does not have a preview.
27 | ); 28 | } else { 29 | return ( 30 |
31 | ); 32 | } 33 | } 34 | 35 | function Preview(props) { 36 | const {showPreview, previewItem, fetchPreview} = props; 37 | useEffect(() => { 38 | if (showPreview && previewItem) fetchPreview(previewItem); 39 | }, [previewItem, showPreview, fetchPreview]); 40 | 41 | if (!props.has_preview) return null; 42 | return ( 43 | 54 |
55 | 56 |
57 |
58 | ) 59 | } 60 | 61 | export default connect( 62 | mapsStateToProps, 63 | mapDispatchToProps, 64 | )(Preview); 65 | -------------------------------------------------------------------------------- /src/components/Preview.module.css: -------------------------------------------------------------------------------- 1 | @value variables: './Variables.module.css'; 2 | @value linkColor from variables; 3 | 4 | .preview { 5 | width: 18em; 6 | position: relative; 7 | overflow-x: hidden; 8 | } 9 | 10 | .content { 11 | padding: 0 1em; 12 | width: 18em; 13 | position: absolute; 14 | left: 0; 15 | right: 0; 16 | top: 0; 17 | bottom: 0; 18 | overflow-y: auto; 19 | } 20 | 21 | .preview a { 22 | color: linkColor; 23 | text-decoration: none; 24 | } 25 | 26 | .preview a:hover, 27 | .preview a:focus { 28 | text-decoration: underline; 29 | } 30 | 31 | .preview figure { 32 | margin: 0; 33 | } 34 | 35 | .preview img { 36 | max-width: 100%; 37 | } 38 | 39 | .preview p { 40 | margin: 0 0 1em; 41 | } 42 | 43 | .preview :global .layout-icon { 44 | width: 90%; 45 | height: 0; 46 | padding-bottom: 130%; 47 | margin: 1em auto 0; 48 | border: 2px solid #a1a1a1; 49 | background-size: 95%; 50 | border-radius: 3px; 51 | } 52 | 53 | .slideEnter { 54 | width: 0; 55 | opacity: .01; 56 | } 57 | 58 | .slideActiveEnter { 59 | width: 18em; 60 | opacity: 1; 61 | transition: width .25s ease-out, opacity .15s; 62 | } 63 | 64 | .slideExit { 65 | width: 18em; 66 | opacity: 1; 67 | } 68 | 69 | .slideActiveExit { 70 | width: 0; 71 | opacity: .01; 72 | transition: width .25s ease-out, opacity .15s .1s; 73 | } 74 | -------------------------------------------------------------------------------- /src/components/Search.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SearchItems from '../containers/SearchItems'; 3 | import Select from './utils/Select'; 4 | import Input from './utils/Input'; 5 | import Button from './utils/Button'; 6 | import S from './Search.module.css'; 7 | 8 | function Search(props) { 9 | const handleSearchSubmit = (e) => { 10 | e.preventDefault(); 11 | props.fetchItems(); 12 | }; 13 | 14 | const handleSectionChange = id => { 15 | props.setSectionId(id); 16 | 17 | if (props.searchTerm){ 18 | props.fetchItems() 19 | } 20 | } 21 | 22 | return ( 23 | <> 24 |
25 | props.setSearchTerm(e.target.value)} 34 | value={props.searchTerm} 35 | placeholder='Search...' 36 | sufixed={true} 37 | /> 38 | 39 | 40 |
41 | 42 | 43 | 44 | ); 45 | } 46 | 47 | export default Search; 48 | -------------------------------------------------------------------------------- /src/components/Search.module.css: -------------------------------------------------------------------------------- 1 | @value variables: './Variables.module.css'; 2 | @value gray93 from variables; 3 | 4 | .searchPanel { 5 | flex: 0 0 25%; 6 | padding: 0 1em; 7 | display: flex; 8 | flex-direction: column; 9 | position: relative; 10 | max-height: 100%; 11 | overflow-y: auto; 12 | } 13 | 14 | .resultsPanel { 15 | flex: 1; 16 | background: gray93; 17 | position: relative; 18 | max-height: 100%; 19 | overflow-y: auto; 20 | padding-bottom: 1em; 21 | } 22 | 23 | .searchWrapper { 24 | flex: 1; 25 | background: gray93; 26 | margin-top: .5em; 27 | padding: 1em; 28 | } 29 | 30 | .search { 31 | display: flex; 32 | width: 100%; 33 | flex-wrap: wrap; 34 | } 35 | 36 | .search input { 37 | flex: 1; 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Tab.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import S from './Tabs.module.css'; 3 | 4 | function Tab(props) { 5 | let className = S.tab; 6 | if (props.isActive) className += ` ${S.active}`; 7 | return ( 8 |
  • 9 | {props.icon ? props.icon : ''}{props.label} 10 |
  • 11 | ); 12 | } 13 | 14 | export default Tab; 15 | -------------------------------------------------------------------------------- /src/components/TableSettings.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Dropdown from './utils/Dropdown'; 3 | import Checkbox from './utils/Checkbox'; 4 | import SettingsIcon from '@material-ui/icons/Settings'; 5 | import { connect } from 'react-redux'; 6 | import { toggleColumn } from '../store/actions/app'; 7 | 8 | const mapsStateToProps = state => ({ 9 | availableColumns: state.app.config.available_columns, 10 | activeColumns: state.app.activeColumns, 11 | }); 12 | 13 | const mapDispatchToProps = dispatch => ({ 14 | toggleColumn: (id, toggle) => { 15 | dispatch(toggleColumn(id, toggle)); 16 | }, 17 | }); 18 | 19 | function TableSettings(props) { 20 | return ( 21 | }> 22 | {props.availableColumns.map(column => { 23 | if (column.id === 'name') return false; 24 | return ( 25 |
  • 26 | props.toggleColumn(column.id, e.target.checked)} 31 | checked={props.activeColumns.includes(column.id)} 32 | /> 33 |
  • 34 | ); 35 | })} 36 |
    37 | ); 38 | } 39 | 40 | export default connect( 41 | mapsStateToProps, 42 | mapDispatchToProps 43 | )(TableSettings); 44 | -------------------------------------------------------------------------------- /src/components/Tabs.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Tab from './Tab'; 3 | import S from './Tabs.module.css'; 4 | 5 | function Tabs(props) { 6 | const [activeTab, setActiveTab] = useState(props.initialTab || props.children[0].props.id); 7 | const children = props.children.filter(child => !!child); 8 | 9 | return ( 10 | <> 11 |
    12 | {children.length > 1 && 13 |
      14 | {children.map((child) => { 15 | if (!child) return false; 16 | return ( 17 | setActiveTab(child.props.id)} 22 | icon={child.props.icon} 23 | /> 24 | ); 25 | })} 26 |
    27 | } 28 | {props.headerContent &&
    29 | {props.headerContent} 30 |
    31 | } 32 |
    33 | <> 34 | {children.map((child) => { 35 | if (!child) return false; 36 | if (child.props.id !== activeTab) return undefined; 37 | return child.props.children; 38 | })} 39 | 40 | 41 | ); 42 | } 43 | 44 | export default Tabs; 45 | -------------------------------------------------------------------------------- /src/components/Tabs.module.css: -------------------------------------------------------------------------------- 1 | @value variables: './Variables.module.css'; 2 | @value linkColor from variables; 3 | 4 | .tabs { 5 | list-style-type: none; 6 | margin: 0; 7 | padding: 0; 8 | display: flex; 9 | width: 25%; 10 | padding: 1em 1em .5em; 11 | } 12 | 13 | .tab { 14 | flex: 1; 15 | text-align: center; 16 | font-size: .75em; 17 | display: flex; 18 | align-items: center; 19 | justify-content: center; 20 | border: 1px solid linkColor; 21 | color: linkColor; 22 | cursor: pointer; 23 | padding: .5em .125em; 24 | margin: 0; 25 | } 26 | 27 | .tab + .tab { 28 | border-left: 0; 29 | } 30 | 31 | .tab:first-child { 32 | border-radius: 2px 0 0 2px; 33 | } 34 | 35 | .tab:last-child { 36 | border-radius: 0 2px 2px 0; 37 | } 38 | 39 | .tab svg { 40 | margin-right: .125em; 41 | } 42 | 43 | .active { 44 | background: linkColor; 45 | color: #fff; 46 | cursor: default; 47 | } 48 | 49 | .tabsHeader { 50 | display: flex; 51 | justify-content: space-between; 52 | align-items: center; 53 | } 54 | 55 | .headerContent { 56 | padding: 1em 1em .5em; 57 | flex: 1; 58 | text-align: right; 59 | } 60 | -------------------------------------------------------------------------------- /src/components/TogglePreview.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Toggle from './utils/Toggle'; 3 | import { connect } from 'react-redux'; 4 | import { togglePreview } from '../store/actions/app'; 5 | 6 | const mapsStateToProps = state => ({ 7 | showPreview: state.app.showPreview, 8 | }); 9 | 10 | const mapDispatchToProps = dispatch => ({ 11 | togglePreview: (id, toggle) => { 12 | dispatch(togglePreview(id, toggle)); 13 | }, 14 | }); 15 | 16 | function TogglePreview(props) { 17 | return ( 18 | props.togglePreview(e.target.checked)} 22 | checked={props.showPreview} 23 | label="Preview" 24 | /> 25 | ); 26 | } 27 | 28 | export default connect( 29 | mapsStateToProps, 30 | mapDispatchToProps 31 | )(TogglePreview); 32 | -------------------------------------------------------------------------------- /src/components/Tree.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TreeItems from './TreeItems'; 3 | import Loader from './utils/Loader'; 4 | import Select from './utils/Select'; 5 | import S from './Tree.module.css'; 6 | 7 | function Tree(props) { 8 | return ( 9 | <> 10 | 10 | 16 | 17 | ); 18 | } 19 | 20 | export default Checkbox; 21 | -------------------------------------------------------------------------------- /src/components/utils/Checkbox.module.css: -------------------------------------------------------------------------------- 1 | @value variables: '../Variables.module.css'; 2 | @value linkColor from variables; 3 | 4 | .checkbox { 5 | position: absolute; 6 | opacity: 0; 7 | left: -9999em; 8 | pointer-events: all; 9 | } 10 | 11 | .label { 12 | line-height: 1.2; 13 | cursor: pointer; 14 | display: inline-flex; 15 | margin: 0; 16 | padding: 0; 17 | font-weight: normal; 18 | } 19 | 20 | .disabledLabel { 21 | composes: label; 22 | opacity: .5; 23 | cursor: not-allowed; 24 | } 25 | 26 | .icon { 27 | color: #808080; 28 | font-size: 1.1428571429em; 29 | margin-right: .35em; 30 | display: inline-flex; 31 | } 32 | 33 | .iconActive { 34 | composes: icon; 35 | color: linkColor; 36 | } 37 | -------------------------------------------------------------------------------- /src/components/utils/Dropdown.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import S from './Dropdown.module.css'; 3 | 4 | class Dropdown extends Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.state = { 9 | isOpen: props.isOpen || false, 10 | }; 11 | 12 | this.dropdownRef = React.createRef(); 13 | this.toggleDropdown = this.toggleDropdown.bind(this); 14 | this.handleClick = this.handleClick.bind(this); 15 | } 16 | 17 | toggleDropdown(e) { 18 | e && e.preventDefault(); 19 | this.state.isOpen ? this.closeDropdown() : this.openDropdown(); 20 | } 21 | 22 | openDropdown() { 23 | this.setState({isOpen: true}); 24 | document.addEventListener('mousedown', this.handleClick, false); 25 | } 26 | 27 | closeDropdown() { 28 | this.setState({isOpen: false}); 29 | document.removeEventListener('mousedown', this.handleClick, false); 30 | } 31 | 32 | handleClick(e) { 33 | if (this.dropdownRef.current.contains(e.target)) return; 34 | this.closeDropdown(); 35 | } 36 | 37 | render() { 38 | return ( 39 |
    40 | {this.props.label}{this.props.icon ? this.props.icon : ''} 41 | {this.state.isOpen &&
      {this.props.children}
    } 42 |
    43 | ); 44 | } 45 | } 46 | 47 | export default Dropdown; 48 | -------------------------------------------------------------------------------- /src/components/utils/Dropdown.module.css: -------------------------------------------------------------------------------- 1 | @value variables: '../Variables.module.css'; 2 | @value gray60, linkColor from variables; 3 | 4 | .dropdown { 5 | position: relative; 6 | display: inline-block; 7 | } 8 | 9 | .toggle { 10 | color: gray60; 11 | text-decoration: none; 12 | font-size: .6875em; 13 | font-weight: 500; 14 | text-transform: uppercase; 15 | display: flex; 16 | align-items: center; 17 | } 18 | 19 | .toggle:hover, 20 | .toggle:active, 21 | .toggle:focus { 22 | color: linkColor; 23 | text-decoration: none; 24 | } 25 | 26 | .toggle svg { 27 | margin-left: .35em; 28 | } 29 | 30 | .menu { 31 | position: absolute; 32 | right: 0; 33 | top: 100%; 34 | background: #fff; 35 | box-shadow: 0 0 .5em rgba(0, 0, 0, .5); 36 | border-radius: 2px; 37 | padding: .5em 0; 38 | margin: 1em 0 0; 39 | list-style-type: none; 40 | z-index: 999; 41 | font-size: .874em; 42 | min-width: 160px; 43 | background-clip: padding-box; 44 | text-align: left; 45 | } 46 | 47 | .menu li { 48 | padding: .35em .5em; 49 | margin: 0; 50 | } 51 | -------------------------------------------------------------------------------- /src/components/utils/Input.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import S from './Input.module.css'; 3 | 4 | function Input({ onChange, value, name, id, label, type = 'text', disabled, placeholder, prefixed, sufixed}) { 5 | let className = `${S.input}`; 6 | if (prefixed) className += ` ${S.prefixed}`; 7 | if (sufixed) className += ` ${S.sufixed}`; 8 | return ( 9 | <> 10 | {label && 13 | } 14 | 24 | 25 | ); 26 | } 27 | 28 | export default Input; 29 | -------------------------------------------------------------------------------- /src/components/utils/Input.module.css: -------------------------------------------------------------------------------- 1 | .input { 2 | border: none; 3 | box-shadow: none; 4 | border-radius: 2px; 5 | font-size: .875em; 6 | padding: .7142857143em .875em; 7 | line-height: 1; 8 | } 9 | 10 | .prefixed { 11 | border-radius: 0 2px 2px 0; 12 | } 13 | 14 | .sufixed { 15 | border-radius: 2px 0 0 2px; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/utils/Loader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CSSTransition } from 'react-transition-group'; 3 | import S from './Loader.module.css'; 4 | 5 | function Loader(props) { 6 | return ( 7 | 20 |
    21 |
    22 | 23 | Loading 24 |
    25 |
    26 |
    27 | ); 28 | } 29 | 30 | export default Loader; 31 | -------------------------------------------------------------------------------- /src/components/utils/Loader.module.css: -------------------------------------------------------------------------------- 1 | .loader { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | z-index: 99999; 11 | margin: 0; 12 | } 13 | 14 | .loader span { 15 | font-size: .75em; 16 | position: relative; 17 | top: 1em; 18 | display: block; 19 | text-transform: uppercase; 20 | opacity: .8; 21 | } 22 | 23 | .content { 24 | color: hsl(0, 0, 50); 25 | text-align: center; 26 | } 27 | 28 | .icon { 29 | display: inline-block; 30 | position: relative; 31 | font-size: 1em; 32 | width: 1.375em; 33 | height: 2.375em; 34 | margin: 0 1.5em -.3em; 35 | transform: rotate(-48deg); 36 | animation: loadRotate 1.5s infinite cubic-bezier(.45, .05, .55, .95); 37 | } 38 | 39 | .icon::before, 40 | .icon::after { 41 | content: ''; 42 | display: block; 43 | background: currentColor; 44 | border-radius: 50%; 45 | position: absolute; 46 | left: 50%; 47 | } 48 | .icon::before { 49 | width: 1em; 50 | height: 1em; 51 | margin-left: -.5em; 52 | bottom: 1.375em; 53 | animation: loadBounceTopSquash .75s alternate infinite ease, loadBounceTopFlow .75s alternate infinite ease; 54 | } 55 | .icon::after { 56 | width: 1.375em; 57 | height: 1.375em; 58 | margin-left: -.6875em; 59 | bottom: 0; 60 | animation: loadBounceBottomSquash .75s alternate infinite ease, loadBounceBottomFlow .75s alternate infinite ease; 61 | } 62 | 63 | .fadeEnter { 64 | opacity: .01; 65 | } 66 | 67 | .fadeActiveEnter { 68 | opacity: 1; 69 | transition: opacity .25s; 70 | } 71 | 72 | .fadeExit { 73 | opacity: 1; 74 | } 75 | 76 | .fadeActiveExit { 77 | opacity: .01; 78 | transition: opacity .25s; 79 | } 80 | 81 | @keyframes loadBounceTopSquash { 82 | 0% { 83 | height: .375em; 84 | border-radius: 3.75em 3.75em 1.25em 1.25em; 85 | transform: scaleX(2); 86 | } 87 | 15% { 88 | height: 1em; 89 | border-radius: 50%; 90 | transform: scaleX(1); 91 | } 92 | 100% { 93 | height: 1em; 94 | border-radius: 50%; 95 | transform: scaleX(1); 96 | } 97 | } 98 | @keyframes loadBounceBottomSquash { 99 | 0% { 100 | height: 1em; 101 | border-radius: 1.25em 1.25em 3.75em 3.75em; 102 | transform: scaleX(1.5); 103 | } 104 | 15% { 105 | height: 1.375em; 106 | border-radius: 50%; 107 | transform: scaleX(1); 108 | } 109 | 100% { 110 | height: 1.375em; 111 | border-radius: 50%; 112 | transform: scaleX(1); 113 | } 114 | } 115 | @keyframes loadBounceTopFlow { 116 | 0% { 117 | bottom: 1.125em; 118 | } 119 | 50% { 120 | bottom: 2.25em; 121 | animation-timing-function: cubic-bezier(.55, .06, .68, .19); 122 | } 123 | 90% { 124 | bottom: 1.75em; 125 | } 126 | 100% { 127 | bottom: 1.75em; 128 | } 129 | } 130 | @keyframes loadBounceBottomFlow { 131 | 0% { 132 | bottom: .1875em; 133 | } 134 | 50% { 135 | bottom: -.9375em; 136 | animation-timing-function: cubic-bezier(.55, .06, .68, .19); 137 | } 138 | 90% { 139 | bottom: 0; 140 | } 141 | 100% { 142 | bottom: 0; 143 | } 144 | } 145 | @keyframes loadRotate { 146 | 0% { 147 | transform: rotate(-228deg); 148 | } 149 | 49% { 150 | transform: rotate(-48deg); 151 | } 152 | 51% { 153 | transform: rotate(-48deg); 154 | } 155 | 92% { 156 | transform: rotate(132deg); 157 | } 158 | 100% { 159 | transform: rotate(132deg); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/components/utils/Pager.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Select from './Select'; 3 | import Button from './Button'; 4 | import ArrowLeftIcon from '@material-ui/icons/ArrowLeft'; 5 | import ArrowRightIcon from '@material-ui/icons/ArrowRight'; 6 | import S from './Pager.module.css'; 7 | import { connect } from 'react-redux'; 8 | import { setItemsLimit } from '../../store/actions/app'; 9 | 10 | const calculatePages = (total, limit) => { 11 | if (total === 0) return 1; 12 | const totalPages = parseInt(total / limit, 10); 13 | return (total % limit) === 0 ? totalPages : totalPages + 1; 14 | } 15 | 16 | const mapsStateToProps = state => ({ 17 | limits: state.app.limits, 18 | itemsLimit: state.app.itemsLimit, 19 | }); 20 | 21 | const mapDispatchToProps = dispatch => ({ 22 | setItemsLimit: (limit) => { 23 | dispatch(setItemsLimit(limit)); 24 | }, 25 | }); 26 | 27 | function Pager(props) { 28 | const pagesNr = calculatePages(props.itemsNr, props.itemsLimit); 29 | const getPageButtons = () => { 30 | const pages = []; 31 | for (let i = 1; i <= pagesNr; i++) { 32 | pages.push( 33 |
  • 34 | 38 |
  • 39 | ); 40 | } 41 | return pages; 42 | } 43 | 44 | return ( 45 |
    46 |
      47 |
    • 48 | 53 |
    • 54 | {getPageButtons()} 55 |
    • 56 | 61 |
    • 62 |
    63 | 14 | {options.map(option => )} 15 | 16 | ); 17 | } 18 | 19 | export default Select; 20 | -------------------------------------------------------------------------------- /src/components/utils/Select.module.css: -------------------------------------------------------------------------------- 1 | @value variables: '../Variables.module.css'; 2 | @value fontPrimary, gray87 from variables; 3 | 4 | .select { 5 | font-family: fontPrimary; 6 | appearance: none; 7 | border: none; 8 | border-radius: 2px; 9 | font-size: .75em; 10 | height: 3em; 11 | padding: 0 2.25em 0 1em; 12 | background-color: gray87; 13 | background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLW1pdGVybGltaXQ9IjEuNDEiPjxwYXRoIGQ9Ik01LjUzIDcuNDlMLjIgMi4xNWEuNjYuNjYgMCAwIDEgMC0uOTNMLjgyLjU5YS42Ni42NiAwIDAgMSAuOTMgMEw2IDQuODIgMTAuMjUuNmEuNjYuNjYgMCAwIDEgLjkzIDBsLjYzLjYzYy4yNS4yNS4yNS42NyAwIC45M0w2LjQ3IDcuNDlhLjY2LjY2IDAgMCAxLS45NCAweiIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+'); 14 | background-repeat: no-repeat; 15 | background-position: right .75em center; 16 | background-size: .75em auto; 17 | cursor: pointer; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/utils/Toggle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import S from './Toggle.module.css'; 3 | 4 | function Toggle({ onChange, checked, name, id, label, disabled }) { 5 | return ( 6 | <> 7 | 8 | 12 | 13 | ); 14 | } 15 | 16 | export default Toggle; 17 | -------------------------------------------------------------------------------- /src/components/utils/Toggle.module.css: -------------------------------------------------------------------------------- 1 | @value variables: '../Variables.module.css'; 2 | @value gray78, linkColor from variables; 3 | 4 | .checkbox { 5 | position: absolute; 6 | opacity: 0; 7 | left: -9999em; 8 | pointer-events: all; 9 | } 10 | 11 | .label { 12 | line-height: 1.2; 13 | cursor: pointer; 14 | display: inline-flex; 15 | font-size: .6875em; 16 | text-transform: uppercase; 17 | font-weight: 500; 18 | align-items: center; 19 | padding: 1.0909090909em 0; 20 | margin: 0; 21 | } 22 | 23 | .disabledLabel { 24 | composes: label; 25 | opacity: .5; 26 | cursor: not-allowed; 27 | } 28 | 29 | .icon { 30 | display: inline-block; 31 | margin-left: .75em; 32 | width: 2.5454545455em; 33 | height: 1.0909090909em; 34 | border-radius: 50em; 35 | position: relative; 36 | background: gray78; 37 | transition: background-color .3s; 38 | } 39 | 40 | .icon::before { 41 | content: ''; 42 | display: inline-block; 43 | position: absolute; 44 | width: 1.4545454545em; 45 | height: 1.4545454545em; 46 | border-radius: 50em; 47 | left: -2px; 48 | top: 50%; 49 | transform: translate3d(0, -50%, 0); 50 | transition: left .3s cubic-bezier(.4, 0, .2, 1), background-color .1s; 51 | background: #fff; 52 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12); 53 | } 54 | 55 | .iconActive { 56 | composes: icon; 57 | background: #7EA9F5; 58 | } 59 | 60 | .iconActive::before { 61 | left: 1.2727272727em; 62 | background: linkColor; 63 | box-shadow: 0 3px 4px 0 rgba(0, 0, 0, .14), 0 3px 3px -2px rgba(0, 0, 0, .2), 0 1px 8px 0 rgba(0, 0, 0, .12); 64 | } 65 | -------------------------------------------------------------------------------- /src/containers/Browser.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import Browser from '../components/Browser'; 3 | 4 | const mapsStateToProps = state => ({ 5 | isLoaded: state.app.isLoaded, 6 | config: state.app.config, 7 | onCancel: state.app.onCancel, 8 | }); 9 | 10 | export default connect( 11 | mapsStateToProps, 12 | )(Browser); 13 | -------------------------------------------------------------------------------- /src/containers/Items.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { fetchLocationItems, setPage, setLocationId, setPreviewItem } from '../store/actions/items'; 3 | import Items from '../components/Items'; 4 | 5 | const mapsStateToProps = state => ({ 6 | isLoading: state.items.isLocationLoading, 7 | items: state.items.locationItems, 8 | id: state.items.locationId, 9 | currentPage: state.items.currentPage, 10 | previewItem: state.items.previewItem, 11 | }); 12 | 13 | const mapDispatchToProps = dispatch => ({ 14 | fetchItems: () => { 15 | dispatch(fetchLocationItems()); 16 | }, 17 | setPage: (page) => { 18 | dispatch(setPage(page)); 19 | }, 20 | setId: (id) => { 21 | dispatch(setLocationId(id)); 22 | }, 23 | setPreviewItem: (item) => { 24 | dispatch(setPreviewItem(item)); 25 | }, 26 | }); 27 | 28 | export default connect( 29 | mapsStateToProps, 30 | mapDispatchToProps 31 | )(Items); 32 | -------------------------------------------------------------------------------- /src/containers/Search.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { setSearchTerm, fetchItems, setSearchPage, setPreviewItem } from '../store/actions/search'; 3 | import Search from '../components/Search'; 4 | import {setSectionId} from '../store/actions/app'; 5 | 6 | const mapsStateToProps = state => ({ 7 | isLoading: state.search.isLoading, 8 | items: state.search.items, 9 | searchTerm: state.search.searchTerm, 10 | currentPage: state.search.currentPage, 11 | previewItem: state.search.previewItem, 12 | sections: state.app.config.sections, 13 | id: state.app.sectionId, 14 | }); 15 | 16 | const mapDispatchToProps = dispatch => ({ 17 | setSearchTerm: (term) => { 18 | dispatch(setSearchTerm(term)); 19 | }, 20 | fetchItems: () => { 21 | dispatch(fetchItems()); 22 | }, 23 | setPage: (page) => { 24 | dispatch(setSearchPage(page)); 25 | }, 26 | setPreviewItem: (item) => { 27 | dispatch(setPreviewItem(item)); 28 | }, 29 | setSectionId: (id) => { 30 | dispatch(setSectionId(id)); 31 | }, 32 | }); 33 | 34 | export default connect( 35 | mapsStateToProps, 36 | mapDispatchToProps 37 | )(Search); 38 | -------------------------------------------------------------------------------- /src/containers/SearchItems.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { fetchItems, setSearchPage, setPreviewItem } from '../store/actions/search'; 3 | import Items from '../components/Items'; 4 | 5 | const mapsStateToProps = state => ({ 6 | isLoading: state.search.isLoading, 7 | items: state.search.items, 8 | currentPage: state.search.currentPage, 9 | previewItem: state.search.previewItem, 10 | }); 11 | 12 | const mapDispatchToProps = dispatch => ({ 13 | fetchItems: () => { 14 | dispatch(fetchItems()); 15 | }, 16 | setPage: (page) => { 17 | dispatch(setSearchPage(page)); 18 | }, 19 | setPreviewItem: (item) => { 20 | dispatch(setPreviewItem(item)); 21 | }, 22 | }); 23 | 24 | export default connect( 25 | mapsStateToProps, 26 | mapDispatchToProps 27 | )(Items); 28 | -------------------------------------------------------------------------------- /src/containers/Tree.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { fetchTreeItems, setLocationId } from '../store/actions/items'; 3 | import { setSectionId } from '../store/actions/app'; 4 | import Tree from '../components/Tree'; 5 | 6 | const mapsStateToProps = state => ({ 7 | isLoading: state.items.isTreeLoading, 8 | items: state.items.treeItems, 9 | id: state.app.sectionId, 10 | locationId: state.items.locationId, 11 | sections: state.app.config.sections, 12 | }); 13 | 14 | const mapDispatchToProps = dispatch => ({ 15 | fetchItems: () => { 16 | dispatch(fetchTreeItems()); 17 | }, 18 | setSectionId: (id) => { 19 | dispatch(setSectionId(id)); 20 | }, 21 | setLocationId: (id) => { 22 | dispatch(setLocationId(id)); 23 | }, 24 | }); 25 | 26 | export default connect( 27 | mapsStateToProps, 28 | mapDispatchToProps 29 | )(Tree); 30 | -------------------------------------------------------------------------------- /src/fonts.css: -------------------------------------------------------------------------------- 1 | /* Roboto */ 2 | @font-face { 3 | font-family: 'Roboto'; 4 | font-weight: 300; 5 | font-style: normal; 6 | src: url('./assets/fonts/Roboto/Roboto-300.eot'); 7 | src: url('./assets/fonts/Roboto/Roboto-300.eot?#iefix') format('embedded-opentype'), 8 | local('Roboto Light'), 9 | local('Roboto-300'), 10 | url('./assets/fonts/Roboto/Roboto-300.woff2') format('woff2'), 11 | url('./assets/fonts/Roboto/Roboto-300.woff') format('woff'), 12 | url('./assets/fonts/Roboto/Roboto-300.ttf') format('truetype'), 13 | url('./assets/fonts/Roboto/Roboto-300.svg#Roboto') format('svg'); 14 | } 15 | 16 | @font-face { 17 | font-family: 'Roboto'; 18 | font-weight: 400; 19 | font-style: normal; 20 | src: url('./assets/fonts/Roboto/Roboto-regular.eot'); 21 | src: url('./assets/fonts/Roboto/Roboto-regular.eot?#iefix') format('embedded-opentype'), 22 | local('Roboto'), 23 | local('Roboto-regular'), 24 | url('./assets/fonts/Roboto/Roboto-regular.woff2') format('woff2'), 25 | url('./assets/fonts/Roboto/Roboto-regular.woff') format('woff'), 26 | url('./assets/fonts/Roboto/Roboto-regular.ttf') format('truetype'), 27 | url('./assets/fonts/Roboto/Roboto-regular.svg#Roboto') format('svg'); 28 | } 29 | 30 | @font-face { 31 | font-family: 'Roboto'; 32 | font-weight: 500; 33 | font-style: normal; 34 | src: url('./assets/fonts/Roboto/Roboto-500.eot'); 35 | src: url('./assets/fonts/Roboto/Roboto-500.eot?#iefix') format('embedded-opentype'), 36 | local('Roboto Medium'), 37 | local('Roboto-500'), 38 | url('./assets/fonts/Roboto/Roboto-500.woff2') format('woff2'), 39 | url('./assets/fonts/Roboto/Roboto-500.woff') format('woff'), 40 | url('./assets/fonts/Roboto/Roboto-500.ttf') format('truetype'), 41 | url('./assets/fonts/Roboto/Roboto-500.svg#Roboto') format('svg'); 42 | } 43 | 44 | @font-face { 45 | font-family: 'Roboto'; 46 | font-weight: 700; 47 | font-style: normal; 48 | src: url('./assets/fonts/Roboto/Roboto-700.eot'); 49 | src: url('./assets/fonts/Roboto/Roboto-700.eot?#iefix') format('embedded-opentype'), 50 | local('Roboto Bold'), 51 | local('Roboto-700'), 52 | url('./assets/fonts/Roboto/Roboto-700.woff2') format('woff2'), 53 | url('./assets/fonts/Roboto/Roboto-700.woff') format('woff'), 54 | url('./assets/fonts/Roboto/Roboto-700.ttf') format('truetype'), 55 | url('./assets/fonts/Roboto/Roboto-700.svg#Roboto') format('svg'); 56 | } 57 | -------------------------------------------------------------------------------- /src/helpers/index.js: -------------------------------------------------------------------------------- 1 | const buildUrlParams = (parameters, isCustomParam) => { 2 | let qs = ''; 3 | for (const key in parameters) { 4 | if (parameters.hasOwnProperty(key)) { 5 | const value = parameters[key]; 6 | qs += key === 'customParams' 7 | ? buildUrlParams(value, true) 8 | : `${encodeURIComponent(isCustomParam ? `customParams[${key}]` : key)}=${encodeURIComponent(value)}&`; 9 | } 10 | } 11 | if (qs.length > 0) { 12 | if (qs.match(/&$/)) qs = qs.slice(0, -1); //chop off last "&" 13 | if (!isCustomParam) qs = `?${qs}`; 14 | } 15 | return qs; 16 | }; 17 | 18 | const cbBasePath = document.querySelector('meta[name=ngcb-base-path]').getAttribute('content'); 19 | const cbBaseApiPath = '/api/'; 20 | 21 | export const buildUrl = (getState, path, params = {}, sendCustomParams = true) => { 22 | const urlParams = sendCustomParams ? buildUrlParams({ 23 | ...params, 24 | customParams: getState().app.customParams, 25 | }) : ''; 26 | return `${cbBasePath}${cbBaseApiPath}${getState().app.itemType}/${path}${urlParams}`.replace(/\/{2,}/g, '/'); 27 | }; 28 | 29 | export const extractCustomParams = (params) => { 30 | const customParams = {}; 31 | for (const key in params) { 32 | const m = key.match(/^custom(.*)/); 33 | let param_name; 34 | if (m) { 35 | param_name = m[1].trim(); 36 | if (param_name !== '' && param_name !== '-'){ 37 | param_name = param_name.charAt(0).toLowerCase() + param_name.slice(1); 38 | customParams[param_name] = params[key]; 39 | } 40 | } 41 | }; 42 | return customParams; 43 | }; 44 | 45 | export const htmlParser = (domstring) => { 46 | const html = new DOMParser().parseFromString(domstring, 'text/html'); 47 | return Array.from(html.body.childNodes); 48 | }; 49 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | .input-browse { 2 | display: flex; 3 | margin: 0 0 1em; 4 | } 5 | .input-browse .js-clear { 6 | display: flex; 7 | align-items: center; 8 | cursor: pointer; 9 | padding: 0 .25em; 10 | transition: background .15s ease, color .15s ease; 11 | } 12 | .input-browse .js-clear i { 13 | font-size: .875em; 14 | } 15 | .input-browse .js-trigger { 16 | flex: 1; 17 | display: flex; 18 | align-items: center; 19 | height: 3em; 20 | font-size: .75em; 21 | max-width: 100%; 22 | text-decoration: none; 23 | } 24 | .input-browse .js-name { 25 | flex: 1; 26 | overflow: hidden; 27 | text-overflow: ellipsis; 28 | white-space: nowrap; 29 | padding: 0 .75em; 30 | } 31 | .input-browse .js-clear { 32 | width: 40px; 33 | } 34 | .input-browse .js-trigger:hover { 35 | text-decoration: none; 36 | } 37 | .input-browse .js-trigger:hover::after { 38 | background: #999; 39 | } 40 | .item-empty .input-browse .js-clear { 41 | display: none; 42 | } 43 | 44 | .js-multiple-browse .js-trigger { 45 | font-size: .75em; 46 | padding: 0 1.5em; 47 | line-height: 3; 48 | display: inline-block; 49 | text-decoration: none; 50 | } 51 | .js-multiple-browse .js-trigger:hover { 52 | background: #999; 53 | text-decoration: none; 54 | } 55 | .js-multiple-browse .items { 56 | min-height: 3em; 57 | margin: 0 0 .5em; 58 | padding: .5em 0 0 .5em; 59 | font-size: .75em; 60 | line-height: 1.4166666; 61 | display: flex; 62 | flex-wrap: wrap; 63 | } 64 | .js-multiple-browse .item { 65 | margin: 0 .5em .5em 0; 66 | display: flex; 67 | } 68 | .js-multiple-browse .item > * { 69 | display: flex; 70 | align-items: center; 71 | } 72 | .js-multiple-browse .js-remove { 73 | padding: 0 3px; 74 | margin: 0 1px 0 0; 75 | text-decoration: none; 76 | } 77 | .js-multiple-browse .js-remove i { 78 | font-size: 1.1666666667em; 79 | float: left; 80 | line-height: inherit; 81 | } 82 | .js-multiple-browse .item .name { 83 | padding: 4px 6px; 84 | } 85 | .js-multiple-browse .no-items { 86 | padding: 4px 0; 87 | display: none; 88 | } 89 | .js-multiple-browse.items-empty .no-items { 90 | display: block; 91 | } 92 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './index.css'; 2 | import './fonts.css'; 3 | import InputBrowse from './plugins/InputBrowse'; 4 | import MultipleBrowse from './plugins/MultipleBrowse'; 5 | import Browser from './plugins/Browser'; 6 | 7 | window.addEventListener('load', () => { 8 | [...document.getElementsByClassName('js-input-browse')].forEach(el => new InputBrowse(el)); 9 | [...document.getElementsByClassName('js-multiple-browse')].forEach(el => new MultipleBrowse(el)); 10 | }); 11 | 12 | export { InputBrowse, MultipleBrowse, Browser }; 13 | -------------------------------------------------------------------------------- /src/plugins/Browser.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from '../App'; 4 | 5 | export default class Browser { 6 | constructor(opts = {}) { 7 | this.overrides = opts.overrides || {}; 8 | this.itemType = opts.itemType || ''; 9 | this.onCancel = opts.onCancel; 10 | this.onConfirm = opts.onConfirm; 11 | this.disabledItems = opts.disabledItems || []; 12 | 13 | this.cancel = this.cancel.bind(this); 14 | this.confirm = this.confirm.bind(this); 15 | this.el = document.createElement('div'); 16 | } 17 | 18 | open() { 19 | document.body.appendChild(this.el); 20 | ReactDOM.render( 21 | , 28 | this.el 29 | ); 30 | } 31 | 32 | confirm(data) { 33 | this.onConfirm && this.onConfirm(data); 34 | this.close(); 35 | } 36 | 37 | cancel(e) { 38 | e && e.preventDefault(); 39 | this.onCancel && this.onCancel(); 40 | this.close(); 41 | } 42 | 43 | close() { 44 | ReactDOM.unmountComponentAtNode(this.el); 45 | document.body.removeChild(this.el); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/plugins/InputBrowse.js: -------------------------------------------------------------------------------- 1 | import Browser from './Browser'; 2 | 3 | export default class InputBrowse { 4 | constructor(el, opts = {}) { 5 | if (el.dataset.browser) return; 6 | this.el = el; 7 | const overrides = {...el.dataset}; 8 | for (const key in overrides) { 9 | const val = overrides[key]; 10 | if (!isNaN(val)) { 11 | overrides[key] = parseInt(val, 10); 12 | } else if (val === 'false' || val === 'true') { 13 | overrides[key] = val === 'true'; 14 | } 15 | } 16 | this.overrides = { 17 | min_selected: 1, 18 | max_selected: 1, 19 | ...overrides, 20 | ...opts.overrides, 21 | }; 22 | this.selectedItems = []; 23 | [this.nameEl] = el.getElementsByClassName('js-name'); 24 | [this.valueEl] = el.getElementsByClassName('js-value'); 25 | this.itemType = el.getElementsByClassName('js-item-type')[0].value; 26 | this.browser = new Browser({ 27 | overrides: this.overrides, 28 | itemType: this.itemType, 29 | onCancel: this.cancel.bind(this), 30 | onConfirm: this.onConfirm.bind(this), 31 | }); 32 | 33 | this.el.dataset.browser = true; 34 | 35 | this.setupEvents(); 36 | } 37 | 38 | setupEvents() { 39 | [...this.el.getElementsByClassName('js-trigger')].forEach(el => el.addEventListener('click', this.open.bind(this))); 40 | [...this.el.getElementsByClassName('js-item-type')].forEach(el => el.addEventListener('change', this.changeItemType.bind(this))); 41 | [...this.el.getElementsByClassName('js-clear')].forEach(el => el.addEventListener('click', this.clear.bind(this))); 42 | } 43 | 44 | open(e) { 45 | e && e.preventDefault(); 46 | this.browser.open(); 47 | } 48 | 49 | close() { 50 | this.browser.close(); 51 | } 52 | 53 | cancel() { 54 | this.el.dispatchEvent(new CustomEvent('browser:cancel', { bubbles: true, cancelable: true, detail: { instance: this } })); 55 | } 56 | 57 | changeItemType(e) { 58 | this.clear(); 59 | this.itemType = e.target.value; 60 | this.browser.itemType = e.target.value; 61 | } 62 | 63 | clear() { 64 | this.selectedItems = []; 65 | this.nameEl.innerHTML = this.nameEl.dataset.emptyNote; 66 | this.valueEl.value = ''; 67 | this.el.classList.add('item-empty'); 68 | this.triggerChangeEvent(); 69 | } 70 | 71 | onConfirm(selected) { 72 | this.selectedItems = selected; 73 | this.nameEl.innerHTML = selected[0].name; 74 | this.valueEl.value = selected[0].value; 75 | this.el.classList.remove('item-empty'); 76 | this.triggerChangeEvent(); 77 | } 78 | 79 | triggerChangeEvent() { 80 | this.el.dispatchEvent(new CustomEvent('browser:change', { bubbles: true, cancelable: true, detail: { instance: this } })); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/plugins/MultipleBrowse.js: -------------------------------------------------------------------------------- 1 | import { htmlParser } from '../helpers'; 2 | import Browser from './Browser'; 3 | 4 | export default class MultipleBrowse { 5 | constructor(el, opts = {}) { 6 | if (el.dataset.browser) return; 7 | this.el = el; 8 | const overrides = {...el.dataset}; 9 | for (const key in overrides) { 10 | const val = overrides[key]; 11 | if (!isNaN(val)) { 12 | overrides[key] = parseInt(val, 10); 13 | } else if (val === 'false' || val === 'true') { 14 | overrides[key] = val === 'true'; 15 | } 16 | } 17 | this.overrides = { 18 | min_selected: 1, 19 | max_selected: 0, 20 | ...overrides, 21 | ...opts.overrides, 22 | }; 23 | [this.valueEl] = el.getElementsByClassName('js-value'); 24 | this.itemType = el.getElementsByClassName('js-item-type')[0].value; 25 | this.inputTemplate = el.dataset.browserPrototype; 26 | [this.itemsEl] = el.getElementsByClassName('items'); 27 | this.selectedItems = this.getPreselectedItems(); 28 | this.browser = new Browser({ 29 | overrides: this.overrides, 30 | itemType: this.itemType, 31 | onCancel: this.cancel.bind(this), 32 | onConfirm: this.onConfirm.bind(this), 33 | disabledItems: this.selectedItems.map(item => item.value), 34 | }); 35 | 36 | this.el.dataset.browser = true; 37 | 38 | this.setupEvents(); 39 | } 40 | 41 | setupEvents() { 42 | [...this.el.getElementsByClassName('js-trigger')].forEach(el => el.addEventListener('click', this.open.bind(this))); 43 | [...this.el.getElementsByClassName('js-item-type')].forEach(el => el.addEventListener('change', this.changeItemType.bind(this))); 44 | [...this.el.getElementsByClassName('js-clear')].forEach(el => el.addEventListener('click', this.clear.bind(this))); 45 | this.el.addEventListener('click', (e) => { 46 | if (e.target.closest('.js-remove')) { 47 | this.removeItem(e); 48 | } 49 | }); 50 | } 51 | 52 | open(e) { 53 | e && e.preventDefault(); 54 | this.browser.open(); 55 | } 56 | 57 | close() { 58 | this.browser.close(); 59 | } 60 | 61 | cancel() { 62 | this.el.dispatchEvent(new CustomEvent('browser:cancel', { bubbles: true, cancelable: true, detail: { instance: this } })); 63 | } 64 | 65 | changeItemType(e) { 66 | this.clear(); 67 | this.itemType = e.target.value; 68 | this.browser.itemType = e.target.value; 69 | } 70 | 71 | clear() { 72 | this.selectedItems = []; 73 | [...this.itemsEl.getElementsByClassName('item')].forEach(itemEl => { 74 | this.itemsEl.removeChild(itemEl); 75 | }); 76 | this.toggleEmptyMsg(); 77 | this.triggerChangeEvent(); 78 | } 79 | 80 | onConfirm(selected) { 81 | this.selectedItems = this.selectedItems.concat(selected); 82 | this.renderAddedItems(selected); 83 | this.toggleEmptyMsg(); 84 | this.triggerChangeEvent(); 85 | } 86 | 87 | renderAddedItems(items) { 88 | items.forEach(item => this.itemsEl.appendChild(this.renderItem(item))); 89 | } 90 | 91 | renderItem(item) { 92 | const newItem = htmlParser(this.inputTemplate.replace(/__name__/g, item.value))[0]; 93 | newItem.getElementsByClassName('name')[0].innerHTML = item.name; 94 | newItem.getElementsByTagName('INPUT')[0].value = item.value; 95 | return newItem; 96 | } 97 | 98 | triggerChangeEvent() { 99 | this.browser.disabledItems = this.selectedItems.map(item => item.value); 100 | this.el.dispatchEvent(new CustomEvent('browser:change', { bubbles: true, cancelable: true, detail: { instance: this } })); 101 | } 102 | 103 | removeItem(e) { 104 | e.preventDefault(); 105 | const itemEl = e.target.closest('.item'); 106 | const id = parseInt(itemEl.getElementsByTagName('INPUT')[0].value, 10); 107 | this.itemsEl.removeChild(itemEl); 108 | this.selectedItems = this.selectedItems.filter(item => item.value !== id); 109 | this.toggleEmptyMsg(); 110 | this.triggerChangeEvent(); 111 | } 112 | 113 | toggleEmptyMsg() { 114 | this.selectedItems.length ? this.el.classList.remove('items-empty') : this.el.classList.add('items-empty'); 115 | } 116 | 117 | getPreselectedItems() { 118 | return [...this.itemsEl.getElementsByTagName('INPUT')].map(input => { 119 | return {value: parseInt(input.value, 10)}; 120 | }); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/setupProxy.js: -------------------------------------------------------------------------------- 1 | const proxy = require('http-proxy-middleware'); 2 | 3 | module.exports = function(app) { 4 | app.use('/cb/api', proxy({ target: process.env.SITE_URL, changeOrigin: true })); 5 | } 6 | -------------------------------------------------------------------------------- /src/store/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const INITIAL_SETUP = 'INITIAL_SETUP'; 2 | export const CONFIG_LOADED = 'CONFIG_LOADED'; 3 | export const FETCH_CONFIG = 'FETCH_CONFIG'; 4 | export const TOGGLE_COLUMN = 'TOGGLE_COLUMN'; 5 | export const SET_SELECTED_ITEM = 'SET_SELECTED_ITEM'; 6 | export const SET_ITEMS_LIMIT = 'SET_ITEMS_LIMIT'; 7 | export const TOGGLE_PREVIEW = 'TOGGLE_PREVIEW'; 8 | 9 | export const FETCH_TREE = 'FETCH_TREE'; 10 | export const START_TREE_LOAD = 'START_TREE_LOAD'; 11 | export const STOP_TREE_LOAD = 'STOP_TREE_LOAD'; 12 | export const START_LOCATION_LOAD = 'START_LOCATION_LOAD'; 13 | export const STOP_LOCATION_LOAD = 'STOP_LOCATION_LOAD'; 14 | export const SET_SECTION_ID = 'SET_SECTION_ID'; 15 | export const SET_LOCATION_ID = 'SET_LOCATION_ID'; 16 | export const FETCH_LOCATION_ITEMS = 'FETCH_LOCATION_ITEMS'; 17 | export const SET_PAGE = 'SET_PAGE'; 18 | export const SET_PREVIEW_ITEM = 'SET_PREVIEW_ITEM'; 19 | export const START_PREVIEW_LOAD = 'START_PREVIEW_LOAD'; 20 | export const STOP_PREVIEW_LOAD = 'STOP_PREVIEW_LOAD'; 21 | export const FETCH_PREVIEW = 'FETCH_PREVIEW'; 22 | 23 | export const START_SEARCH_LOAD = 'START_SEARCH_LOAD'; 24 | export const STOP_SEARCH_LOAD = 'STOP_SEARCH_LOAD'; 25 | export const FETCH_SEARCH = 'FETCH_SEARCH'; 26 | export const SET_SEARCH_TERM = 'SET_SEARCH_TERM'; 27 | export const SET_SEARCH_PAGE = 'SET_SEARCH_PAGE'; 28 | export const SET_SEARCH_PREVIEW_ITEM = 'SET_SEARCH_PREVIEW_ITEM'; 29 | -------------------------------------------------------------------------------- /src/store/actions/app.js: -------------------------------------------------------------------------------- 1 | import fetch from 'cross-fetch'; 2 | import { setLocationId, fetchTreeItems, setPage, savePage } from './items'; 3 | import { setSearchPage } from './search'; 4 | import { buildUrl } from '../../helpers'; 5 | import { 6 | INITIAL_SETUP, 7 | CONFIG_LOADED, 8 | TOGGLE_COLUMN, 9 | FETCH_CONFIG, 10 | SET_SECTION_ID, 11 | SET_SELECTED_ITEM, 12 | SET_ITEMS_LIMIT, 13 | TOGGLE_PREVIEW, 14 | START_PREVIEW_LOAD, 15 | STOP_PREVIEW_LOAD, 16 | FETCH_PREVIEW, 17 | } from '../actionTypes'; 18 | 19 | export const initialSetup = (data) => { 20 | return dispatch => { 21 | dispatch(receiveOverrides(data)); 22 | dispatch(fetchConfig()); 23 | } 24 | }; 25 | 26 | const receiveOverrides = (data) => { 27 | return { 28 | type: INITIAL_SETUP, 29 | data, 30 | } 31 | }; 32 | 33 | const configLoaded = (isLoaded) => { 34 | return { 35 | type: CONFIG_LOADED, 36 | isLoaded, 37 | }; 38 | }; 39 | 40 | const receiveConfig = (config) => { 41 | return { 42 | type: FETCH_CONFIG, 43 | config, 44 | } 45 | }; 46 | 47 | export const toggleColumn = (id, toggle) => { 48 | return { 49 | type: TOGGLE_COLUMN, 50 | id, 51 | toggle, 52 | }; 53 | }; 54 | 55 | export const saveSectionId = (id) => { 56 | return { 57 | type: SET_SECTION_ID, 58 | id: isNaN(id) ? id : +id 59 | } 60 | }; 61 | 62 | export const setSectionId = (id) => { 63 | return dispatch => { 64 | dispatch(savePage(1)); 65 | dispatch(saveSectionId(id)); 66 | dispatch(setLocationId(id)); 67 | dispatch(fetchTreeItems()); 68 | } 69 | }; 70 | 71 | const fetchConfig = () => { 72 | return (dispatch, getState) => { 73 | return fetch(buildUrl(getState, 'config')) 74 | .then(res => res.json()) 75 | .then( 76 | (config) => { 77 | dispatch(receiveConfig(config)); 78 | dispatch(saveSectionId(getState().app.config.start_location || config.sections[0].id)); 79 | dispatch(setLocationId(getState().app.sectionId)); 80 | dispatch(fetchTreeItems()); 81 | dispatch(configLoaded(true)); 82 | }, 83 | ) 84 | } 85 | }; 86 | 87 | export const setSelectedItem = (item, selected) => { 88 | return { 89 | type: SET_SELECTED_ITEM, 90 | item, 91 | selected, 92 | } 93 | }; 94 | 95 | const saveItemsLimit = (limit) => { 96 | return { 97 | type: SET_ITEMS_LIMIT, 98 | limit, 99 | }; 100 | }; 101 | 102 | export const setItemsLimit = (limit) => { 103 | return dispatch => { 104 | dispatch(saveItemsLimit(limit)); 105 | dispatch(setPage(1)); 106 | dispatch(setSearchPage(1)); 107 | } 108 | }; 109 | 110 | export const togglePreview = (toggle) => { 111 | return dispatch => { 112 | dispatch({ 113 | type: TOGGLE_PREVIEW, 114 | toggle, 115 | }); 116 | } 117 | }; 118 | 119 | const startPreviewLoad = () => { 120 | return { 121 | type: START_PREVIEW_LOAD 122 | }; 123 | }; 124 | 125 | const stopPreviewLoad = () => { 126 | return { 127 | type: STOP_PREVIEW_LOAD 128 | }; 129 | }; 130 | 131 | const storePreview = (preview) => { 132 | return { 133 | type: FETCH_PREVIEW, 134 | preview, 135 | } 136 | }; 137 | 138 | export const fetchPreview = (item) => { 139 | return (dispatch, getState) => { 140 | if (!getState().app.config.has_preview || !getState().app.showPreview || getState().app.previews[item] || item === null) return; 141 | dispatch(startPreviewLoad()); 142 | const url = buildUrl(getState, `render/${item}`, {}, false); 143 | return fetch(url) 144 | .then(res => { 145 | if (!res.ok) { 146 | return res.text().then((data) => { 147 | dispatch(storePreview({[item]: null})); 148 | dispatch(stopPreviewLoad()); 149 | }); 150 | } 151 | return res.text(); 152 | }) 153 | .then( 154 | (result) => { 155 | dispatch(storePreview({[item]: result.trim()})); 156 | dispatch(stopPreviewLoad()); 157 | }, 158 | ) 159 | } 160 | }; 161 | -------------------------------------------------------------------------------- /src/store/actions/items.js: -------------------------------------------------------------------------------- 1 | import fetch from 'cross-fetch'; 2 | import { buildUrl } from '../../helpers/index'; 3 | import { 4 | FETCH_TREE, 5 | START_TREE_LOAD, 6 | STOP_TREE_LOAD, 7 | SET_LOCATION_ID, 8 | FETCH_LOCATION_ITEMS, 9 | START_LOCATION_LOAD, 10 | STOP_LOCATION_LOAD, 11 | SET_PAGE, 12 | SET_PREVIEW_ITEM, 13 | } from '../actionTypes'; 14 | 15 | const startTreeLoad = () => { 16 | return { 17 | type: START_TREE_LOAD 18 | }; 19 | }; 20 | 21 | const stopTreeLoad = () => { 22 | return { 23 | type: STOP_TREE_LOAD 24 | }; 25 | }; 26 | 27 | const getTreeItems = (items) => { 28 | return { 29 | type: FETCH_TREE, 30 | items, 31 | } 32 | }; 33 | 34 | export const fetchTreeItems = () => { 35 | return (dispatch, getState) => { 36 | dispatch(startTreeLoad()); 37 | const url = buildUrl(getState, `browse/${getState().app.sectionId}/locations`); 38 | return fetch(url) 39 | .then(res => res.json()) 40 | .then( 41 | (result) => { 42 | dispatch(getTreeItems(result.children)); 43 | dispatch(stopTreeLoad()); 44 | }, 45 | ) 46 | } 47 | }; 48 | 49 | const saveLocationId = (id) => { 50 | return { 51 | type: SET_LOCATION_ID, 52 | id, 53 | } 54 | }; 55 | 56 | export const setLocationId = (id) => { 57 | return dispatch => { 58 | dispatch(savePage(1)); 59 | dispatch(saveLocationId(id)); 60 | dispatch(fetchLocationItems()); 61 | } 62 | }; 63 | 64 | const getLocationItems = (items) => { 65 | return { 66 | type: FETCH_LOCATION_ITEMS, 67 | items, 68 | } 69 | }; 70 | 71 | const startLocationLoad = () => { 72 | return { 73 | type: START_LOCATION_LOAD 74 | }; 75 | }; 76 | 77 | const stopLocationLoad = () => { 78 | return { 79 | type: STOP_LOCATION_LOAD 80 | }; 81 | }; 82 | 83 | export const fetchLocationItems = () => { 84 | return (dispatch, getState) => { 85 | dispatch(startLocationLoad()); 86 | const url = buildUrl(getState, `browse/${getState().items.locationId}/items`, {limit: getState().app.itemsLimit, page: getState().items.currentPage}); 87 | return fetch(url) 88 | .then(res => res.json()) 89 | .then( 90 | (result) => { 91 | dispatch(getLocationItems(result)); 92 | dispatch(stopLocationLoad()); 93 | dispatch(setPreviewItem(result.parent.value ? result.parent.value : null)); 94 | }, 95 | ) 96 | } 97 | }; 98 | 99 | export const savePage = (page) => { 100 | return { 101 | type: SET_PAGE, 102 | page, 103 | }; 104 | }; 105 | 106 | export const setPage = (page) => { 107 | return dispatch => { 108 | dispatch(savePage(page)); 109 | dispatch(fetchLocationItems()); 110 | } 111 | }; 112 | 113 | export const setPreviewItem = (id) => { 114 | return { 115 | type: SET_PREVIEW_ITEM, 116 | id, 117 | }; 118 | }; 119 | -------------------------------------------------------------------------------- /src/store/actions/search.js: -------------------------------------------------------------------------------- 1 | import fetch from 'cross-fetch'; 2 | import { buildUrl } from '../../helpers/index'; 3 | import { 4 | START_SEARCH_LOAD, 5 | STOP_SEARCH_LOAD, 6 | SET_SEARCH_TERM, 7 | FETCH_SEARCH, 8 | SET_SEARCH_PAGE, 9 | SET_SEARCH_PREVIEW_ITEM, 10 | } from '../actionTypes'; 11 | 12 | const startLoad = () => { 13 | return { 14 | type: START_SEARCH_LOAD 15 | }; 16 | }; 17 | 18 | const stopLoad = () => { 19 | return { 20 | type: STOP_SEARCH_LOAD 21 | }; 22 | }; 23 | 24 | const getItems = (items) => { 25 | return { 26 | type: FETCH_SEARCH, 27 | items, 28 | } 29 | }; 30 | 31 | export const setSearchTerm = (term) => { 32 | return { 33 | type: SET_SEARCH_TERM, 34 | term, 35 | } 36 | }; 37 | 38 | export const fetchItems = (samePage = false) => { 39 | return (dispatch, getState) => { 40 | if (!getState().search.searchTerm) return dispatch(getItems({})); 41 | dispatch(startLoad()); 42 | if (!samePage) dispatch(savePage(1)); 43 | const url = buildUrl(getState, 'search', {searchText: getState().search.searchTerm, limit: getState().app.itemsLimit, page: getState().search.currentPage, sectionId: getState().app.sectionId}); 44 | return fetch(url) 45 | .then(res => res.json()) 46 | .then( 47 | (result) => { 48 | dispatch(getItems(result)); 49 | dispatch(stopLoad()); 50 | }, 51 | ) 52 | } 53 | }; 54 | 55 | const savePage = (page) => { 56 | return { 57 | type: SET_SEARCH_PAGE, 58 | page, 59 | } 60 | }; 61 | 62 | export const setSearchPage = (page) => { 63 | return dispatch => { 64 | dispatch(savePage(page)); 65 | dispatch(fetchItems(true)); 66 | } 67 | }; 68 | 69 | export const setPreviewItem = (id) => { 70 | return { 71 | type: SET_SEARCH_PREVIEW_ITEM, 72 | id, 73 | }; 74 | }; 75 | -------------------------------------------------------------------------------- /src/store/reducers/app.js: -------------------------------------------------------------------------------- 1 | import { extractCustomParams } from '../../helpers'; 2 | import { 3 | CONFIG_LOADED, 4 | FETCH_CONFIG, 5 | INITIAL_SETUP, 6 | TOGGLE_COLUMN, 7 | SET_SECTION_ID, 8 | SET_SELECTED_ITEM, 9 | SET_ITEMS_LIMIT, 10 | TOGGLE_PREVIEW, 11 | FETCH_PREVIEW, 12 | START_PREVIEW_LOAD, 13 | STOP_PREVIEW_LOAD, 14 | } from '../actionTypes'; 15 | 16 | const INITIAL_STATE = { 17 | itemType: '', 18 | isLoaded: false, 19 | config: {}, 20 | itemsLimit: null, 21 | activeColumns: [], 22 | selectedItems: [], 23 | onCancel: null, 24 | onConfirm: null, 25 | showPreview: null, 26 | limits: [ 27 | {id: 5, name: 5}, 28 | {id: 10, name: 10}, 29 | {id: 25, name: 25}, 30 | ], 31 | previews: {}, 32 | isPreviewLoading: false, 33 | customParams: {}, 34 | sectionId: null, 35 | }; 36 | 37 | export default (state = INITIAL_STATE, action) => { 38 | switch (action.type) { 39 | case CONFIG_LOADED: 40 | return { 41 | ...state, 42 | isLoaded: action.isLoaded, 43 | }; 44 | 45 | case INITIAL_SETUP: 46 | return { 47 | ...state, 48 | ...action.data, 49 | customParams: extractCustomParams(action.data.config), 50 | }; 51 | 52 | case FETCH_CONFIG: 53 | const storedColumns = localStorage.getItem('cb_activeColumns'); 54 | const activeColumns = storedColumns ? JSON.parse(storedColumns) : action.config.default_columns; 55 | return { 56 | ...state, 57 | activeColumns, 58 | config: { 59 | ...action.config, 60 | ...state.config, 61 | }, 62 | }; 63 | 64 | case TOGGLE_COLUMN: 65 | let columns; 66 | if (action.toggle) { 67 | columns = [...state.activeColumns, action.id]; 68 | } else { 69 | columns = state.activeColumns.filter(column => column !== action.id); 70 | } 71 | localStorage.setItem('cb_activeColumns', JSON.stringify(columns)); 72 | return { 73 | ...state, 74 | activeColumns: columns, 75 | }; 76 | 77 | case SET_SELECTED_ITEM: 78 | let selectedItems; 79 | if (action.selected) { 80 | selectedItems = parseInt(state.config.max_selected, 10) === 1 ? [action.item] : [...state.selectedItems, action.item]; 81 | } else { 82 | selectedItems = state.selectedItems.filter(item => item !== action.item); 83 | } 84 | return { 85 | ...state, 86 | selectedItems, 87 | }; 88 | 89 | case SET_SECTION_ID: 90 | return { 91 | ...state, 92 | sectionId: action.id, 93 | }; 94 | 95 | case SET_ITEMS_LIMIT: 96 | localStorage.setItem('cb_itemsLimit', action.limit); 97 | return { 98 | ...state, 99 | itemsLimit: action.limit, 100 | }; 101 | 102 | case TOGGLE_PREVIEW: 103 | localStorage.setItem('cb_showPreview', action.toggle); 104 | return { 105 | ...state, 106 | showPreview: action.toggle, 107 | }; 108 | 109 | case FETCH_PREVIEW: 110 | return { 111 | ...state, 112 | previews: { 113 | ...state.previews, 114 | ...action.preview, 115 | }, 116 | }; 117 | 118 | case START_PREVIEW_LOAD: 119 | return { 120 | ...state, 121 | isPreviewLoading: true, 122 | }; 123 | 124 | case STOP_PREVIEW_LOAD: 125 | return { 126 | ...state, 127 | isPreviewLoading: false, 128 | }; 129 | 130 | default: 131 | return state; 132 | } 133 | }; 134 | -------------------------------------------------------------------------------- /src/store/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import app from './app'; 3 | import items from './items'; 4 | import search from './search'; 5 | 6 | export default combineReducers({ 7 | app, 8 | items, 9 | search, 10 | }); 11 | -------------------------------------------------------------------------------- /src/store/reducers/items.js: -------------------------------------------------------------------------------- 1 | import { 2 | START_TREE_LOAD, 3 | STOP_TREE_LOAD, 4 | START_LOCATION_LOAD, 5 | STOP_LOCATION_LOAD, 6 | FETCH_TREE, 7 | SET_LOCATION_ID, 8 | FETCH_LOCATION_ITEMS, 9 | SET_PAGE, 10 | SET_PREVIEW_ITEM, 11 | } from '../actionTypes'; 12 | 13 | const INITIAL_STATE = { 14 | isTreeLoading: false, 15 | isLocationLoading: false, 16 | locationId: null, 17 | treeItems: [], 18 | locationItems: {}, 19 | currentPage: 1, 20 | previewItem: '', 21 | }; 22 | 23 | export default (state = INITIAL_STATE, action) => { 24 | switch (action.type) { 25 | case START_TREE_LOAD: 26 | return { 27 | ...state, 28 | isTreeLoading: true, 29 | }; 30 | 31 | case STOP_TREE_LOAD: 32 | return { 33 | ...state, 34 | isTreeLoading: false, 35 | }; 36 | 37 | case START_LOCATION_LOAD: 38 | return { 39 | ...state, 40 | isLocationLoading: true, 41 | }; 42 | 43 | case STOP_LOCATION_LOAD: 44 | return { 45 | ...state, 46 | isLocationLoading: false, 47 | }; 48 | 49 | case FETCH_TREE: 50 | return { 51 | ...state, 52 | treeItems: action.items, 53 | }; 54 | 55 | case SET_LOCATION_ID: 56 | return { 57 | ...state, 58 | locationId: action.id, 59 | }; 60 | 61 | case FETCH_LOCATION_ITEMS: 62 | return { 63 | ...state, 64 | locationItems: action.items, 65 | }; 66 | 67 | case SET_PAGE: 68 | return { 69 | ...state, 70 | currentPage: action.page, 71 | }; 72 | 73 | case SET_PREVIEW_ITEM: 74 | return { 75 | ...state, 76 | previewItem: action.id, 77 | }; 78 | 79 | default: 80 | return state; 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /src/store/reducers/search.js: -------------------------------------------------------------------------------- 1 | import { 2 | START_SEARCH_LOAD, 3 | STOP_SEARCH_LOAD, 4 | SET_SEARCH_TERM, 5 | FETCH_SEARCH, 6 | SET_SEARCH_PAGE, 7 | SET_SEARCH_PREVIEW_ITEM, 8 | } from '../actionTypes'; 9 | 10 | const INITIAL_STATE = { 11 | isLoading: false, 12 | items: {}, 13 | searchTerm: '', 14 | currentPage: 1, 15 | previewItem: '', 16 | }; 17 | 18 | export default (state = INITIAL_STATE, action) => { 19 | switch (action.type) { 20 | case START_SEARCH_LOAD: 21 | return { 22 | ...state, 23 | isLoading: true, 24 | }; 25 | 26 | case STOP_SEARCH_LOAD: 27 | return { 28 | ...state, 29 | isLoading: false, 30 | }; 31 | 32 | case SET_SEARCH_TERM: 33 | return { 34 | ...state, 35 | searchTerm: action.term, 36 | }; 37 | 38 | case FETCH_SEARCH: 39 | return { 40 | ...state, 41 | items: action.items, 42 | }; 43 | 44 | case SET_SEARCH_PAGE: 45 | return { 46 | ...state, 47 | currentPage: action.page, 48 | }; 49 | 50 | case SET_SEARCH_PREVIEW_ITEM: 51 | return { 52 | ...state, 53 | previewItem: action.id, 54 | }; 55 | 56 | default: 57 | return state; 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /src/store/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunkMiddleware from 'redux-thunk'; 3 | import { composeWithDevTools } from 'redux-devtools-extension'; 4 | import reducer from './reducers'; 5 | 6 | let middleware = [thunkMiddleware]; 7 | if (process.env.NODE_ENV !== 'production') { 8 | middleware = [...middleware]; 9 | } 10 | export default function configureStore() { 11 | const store = createStore(reducer, composeWithDevTools(applyMiddleware(...middleware))); 12 | if (window.Cypress) { 13 | window.store = store 14 | } 15 | return store; 16 | } 17 | --------------------------------------------------------------------------------