├── .babelrc
├── .eslintrc
├── .gitignore
├── .jest
├── jest.setup.js
└── register-context.js
├── .storybook
├── Storyshots.test.js
├── addons.js
├── config.js
└── webpack.config.js
├── .travis.script.sh
├── .travis.yml
├── LICENSE
├── README.md
├── __mocks__
├── react-select
│ ├── index.js
│ └── lib
│ │ ├── Async.js
│ │ └── default.js
└── styleMock.js
├── demo-site
├── README.md
├── _config.yml
├── _includes
│ ├── head.html
│ └── required_static.html
├── demo.data.json
├── demos.jsx
├── demos.sass
├── index.html
├── list-demo.jsx
├── package.json
├── simple-demo.jsx
├── table-demo.jsx
├── webpack.config.js
└── yarn.lock
├── jest.config.js
├── package.json
├── src
├── ObjectList.demo.stories.js
├── ObjectList.js
├── ObjectList.stories.js
├── __snapshots__
│ ├── ObjectList.demo.stories.storyshot
│ └── ObjectList.stories.storyshot
├── __tests__
│ └── ObjectList.test.js
├── actions-filters
│ ├── ActionsFiltersContainer.js
│ ├── ActionsFiltersContainer.stories.js
│ ├── Favourites.js
│ ├── Favourites.stories.js
│ ├── FavouritesItem.js
│ ├── FiltersContainer.js
│ ├── FiltersContainer.stories.js
│ ├── OptionalField.js
│ ├── OptionalFields.js
│ ├── OptionalFields.stories.js
│ ├── SelectAllAction.js
│ ├── SelectFilters.js
│ ├── SelectFilters.stories.js
│ ├── __snapshots__
│ │ ├── ActionsFiltersContainer.stories.storyshot
│ │ ├── Favourites.stories.storyshot
│ │ ├── FiltersContainer.stories.storyshot
│ │ ├── OptionalFields.stories.storyshot
│ │ └── SelectFilters.stories.storyshot
│ └── __tests__
│ │ ├── Favourites.test.js
│ │ ├── FavouritesItem.test.js
│ │ ├── FiltersContainer.test.js
│ │ ├── OptionalField.test.js
│ │ ├── OptionalFields.test.js
│ │ ├── SelectAllAction.test.js
│ │ ├── SelectFilters.test.js
│ │ └── __snapshots__
│ │ ├── Favourites.test.js.snap
│ │ ├── FiltersContainer.test.js.snap
│ │ ├── OptionalField.test.js.snap
│ │ ├── OptionalFields.test.js.snap
│ │ ├── SelectAllAction.test.js.snap
│ │ └── SelectFilters.test.js.snap
├── data-renderers
│ ├── HeaderField.js
│ ├── List.js
│ ├── List.stories.js
│ ├── ListCard.js
│ ├── Overlay.js
│ ├── Table.js
│ ├── TableHeader.js
│ ├── WidthHandle.js
│ ├── __snapshots__
│ │ ├── List.stories.storyshot
│ │ └── table.stories.storyshot
│ ├── __tests__
│ │ ├── HeaderField.test.js
│ │ ├── List.test.js
│ │ ├── Overlay.test.js
│ │ ├── Table.test.js
│ │ ├── TableHeader.test.js
│ │ ├── WidthHandle.test.js
│ │ └── __snapshots__
│ │ │ ├── HeaderField.test.js.snap
│ │ │ ├── List.test.js.snap
│ │ │ ├── Overlay.test.js.snap
│ │ │ ├── Table.test.js.snap
│ │ │ ├── TableHeader.test.js.snap
│ │ │ └── WidthHandle.test.js.snap
│ ├── index.js
│ ├── table.stories.js
│ └── utils
│ │ ├── __tests__
│ │ └── functions.test.js
│ │ ├── functions.js
│ │ └── index.js
├── demo.data.json
├── filters
│ ├── ChoiceFilter.js
│ ├── CurrencyFilter.js
│ ├── DateFilter.js
│ ├── DayFilter.js
│ ├── MultiChoiceFilter.js
│ ├── NumberSliderFilter.js
│ ├── RemoteChoiceFilter.js
│ ├── RemoteMultiChoiceFilter.js
│ ├── SearchFilter.js
│ ├── TextContainsFilter.js
│ ├── __snapshots__
│ │ └── filters.stories.storyshot
│ ├── filters.stories.js
│ ├── index.js
│ ├── types
│ │ ├── Boolean.js
│ │ ├── Choice.js
│ │ ├── Currency.js
│ │ ├── Date.js
│ │ ├── Day.js
│ │ ├── Month.js
│ │ ├── NumberSlider.js
│ │ ├── Search.js
│ │ ├── __tests__
│ │ │ ├── Boolean.test.js
│ │ │ ├── Choice.test.js
│ │ │ ├── Currency.test.js
│ │ │ ├── Date.test.js
│ │ │ ├── Day.test.js
│ │ │ ├── Month.test.js
│ │ │ ├── NumberSlider.test.js
│ │ │ ├── Search.test.js
│ │ │ └── __snapshots__
│ │ │ │ ├── Boolean.test.js.snap
│ │ │ │ ├── Choice.test.js.snap
│ │ │ │ ├── Currency.test.js.snap
│ │ │ │ ├── Date.test.js.snap
│ │ │ │ ├── Day.test.js.snap
│ │ │ │ ├── Month.test.js.snap
│ │ │ │ ├── NumberSlider.test.js.snap
│ │ │ │ └── Search.test.js.snap
│ │ └── index.js
│ └── utils
│ │ ├── FilterComparison.js
│ │ ├── FilterLabel.js
│ │ ├── RemoveFilter.js
│ │ ├── __mocks__
│ │ └── FilterComparison.js
│ │ ├── __tests__
│ │ ├── FilterComparison.test.js
│ │ ├── FilterLabel.test.js
│ │ ├── RemoveFilter.test.js
│ │ ├── __snapshots__
│ │ │ ├── FilterComparison.test.js.snap
│ │ │ ├── FilterLabel.test.js.snap
│ │ │ ├── RemoveFilter.test.js.snap
│ │ │ └── makeFilter.test.js.snap
│ │ └── makeFilter.test.js
│ │ ├── index.js
│ │ ├── makeFilter.js
│ │ └── utils.js
├── icons
│ ├── FontAwesome.js
│ └── index.js
├── index.js
├── pagination
│ ├── Page.js
│ ├── Pagination.js
│ ├── Pagination.stories.js
│ ├── __snapshots__
│ │ └── Pagination.stories.storyshot
│ ├── __tests__
│ │ ├── Page.test.js
│ │ ├── Pagination.test.js
│ │ └── __snapshots__
│ │ │ ├── Page.test.js.snap
│ │ │ └── Pagination.test.js.snap
│ └── index.js
├── readme.md
├── resources
│ ├── colours.sass
│ ├── dark-table.sass
│ ├── input-range.sass
│ └── main.sass
├── types
│ ├── AllSelector.js
│ ├── BooleanType.js
│ ├── Currency.js
│ ├── DatePart.js
│ ├── DateTime.js
│ ├── Empty.js
│ ├── Links.js
│ ├── RelativeDate.js
│ ├── RelativeDateTime.js
│ ├── Selector.js
│ ├── TextAttr.js
│ ├── __tests__
│ │ ├── AllSelector.test.js
│ │ ├── BooleanType.test.js
│ │ ├── Currency.test.js
│ │ ├── DatePart.test.js
│ │ ├── DateTime.test.js
│ │ ├── Empty.test.js
│ │ ├── Links.test.js
│ │ ├── RelativeDate.test.js
│ │ ├── Selector.test.js
│ │ ├── TextAttr.test.js
│ │ ├── __snapshots__
│ │ │ ├── AllSelector.test.js.snap
│ │ │ ├── BooleanType.test.js.snap
│ │ │ ├── Currency.test.js.snap
│ │ │ ├── DateTime.test.js.snap
│ │ │ ├── Empty.test.js.snap
│ │ │ ├── Links.test.js.snap
│ │ │ ├── RelativeDate.test.js.snap
│ │ │ ├── Selector.test.js.snap
│ │ │ └── TextAttr.test.js.snap
│ │ └── utils.test.js
│ ├── index.js
│ └── utils.js
└── utils
│ ├── ErrorMessage.js
│ ├── Select.js
│ ├── Select.stories.js
│ ├── __snapshots__
│ ├── Select.stories.storyshot
│ └── loading.stories.storyshot
│ ├── __tests__
│ ├── ErrorMessage.test.js
│ ├── __snapshots__
│ │ └── ErrorMessage.test.js.snap
│ ├── functions.test.js
│ └── proptypes.test.js
│ ├── constants.js
│ ├── functions.js
│ ├── index.js
│ └── proptypes.js
├── utils
└── tests.js
├── webpack.config.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react","env","stage-0"],
3 | "env": {
4 | "test": {
5 | "plugins": ["require-context-hook"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "babel"
4 | ],
5 | "parser": "babel-eslint",
6 | "rules": {
7 | "comma-dangle": ["error", "always-multiline"],
8 | "space-before-function-paren": ["error", {
9 | "anonymous": "never",
10 | "named": "never",
11 | "asyncArrow": "always"
12 | }],
13 | "jsx-quotes": ["error", "prefer-double"],
14 | "no-var": "error",
15 | "prefer-const": "warn",
16 |
17 | "babel/no-invalid-this": "warn",
18 | "babel/semi": ["error", "never"],
19 | "react/prop-types": "warn",
20 | "react/no-unused-state": "warn",
21 | "react/no-access-state-in-setstate": "warn",
22 | "object-curly-spacing": "off"
23 | },
24 | "overrides": [
25 | {
26 | "files": [ "*.test.js", "*.stories.js" ],
27 | "rules": {
28 | "react/prop-types": "off"
29 | }
30 | }
31 | ],
32 | "extends": ["standard", "standard-react"],
33 | "env": {
34 | "browser": true,
35 | "jest": true,
36 | "jasmine": true
37 | },
38 | "globals": {
39 | "mapboxgl": true
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 | demo-site/_site/
4 | /cells.js
5 | /filters.js
6 | /main.js
7 | /renderers.js
8 | /utils.js
9 | /icons.js
10 | coverage/
11 | .vscode/
12 | .DS_Store
13 | **~
14 |
15 | *.log
16 |
17 | demo-site/.jekyll-cache
18 |
--------------------------------------------------------------------------------
/.jest/jest.setup.js:
--------------------------------------------------------------------------------
1 | import 'raf/polyfill'
2 | import {configure} from 'enzyme'
3 | import moment from 'moment'
4 | import Adapter from 'enzyme-adapter-react-16'
5 |
6 | configure({ adapter: new Adapter() })
7 | Date.now = jest.fn(() => new Date(Date.UTC(2017, 6, 15)).valueOf())
8 | moment.prototype.local = function() { return this.utcOffset('+10:00') }
9 |
10 | jest.mock('react-select', () => require('__mocks__/react-select'))
11 |
--------------------------------------------------------------------------------
/.jest/register-context.js:
--------------------------------------------------------------------------------
1 | import registerRequireContextHook from 'babel-plugin-require-context-hook/register'
2 | registerRequireContextHook()
3 |
--------------------------------------------------------------------------------
/.storybook/Storyshots.test.js:
--------------------------------------------------------------------------------
1 | import initStoryshots, {multiSnapshotWithOptions} from '@storybook/addon-storyshots'
2 |
3 | jest.mock('@storybook/addon-info', () => ({
4 | withInfo: () => jest.fn(story => story),
5 | }))
6 |
7 | jest.mock('react-select', () => require('__mocks__/react-select'))
8 |
9 | initStoryshots({
10 | test: multiSnapshotWithOptions({
11 | createNodeMock: elem => {
12 | if (elem.type === 'input' && elem.props.type === 'checkbox') {
13 | const ref = document.createElement('input')
14 | ref.setAttribute('type', 'checkbox')
15 | elem.checkbox = ref
16 | return elem
17 | }
18 | }
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/.storybook/addons.js:
--------------------------------------------------------------------------------
1 | import '@storybook/addon-actions/register'
2 |
--------------------------------------------------------------------------------
/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill'
2 | import { configure } from '@storybook/react';
3 | import '../src/resources/main.sass'
4 | import '../node_modules/@fortawesome/fontawesome-free/css/all.css'
5 |
6 | const appStories = require.context('../src/', true, /stories\.js$/)
7 |
8 | const requireAll = (requireContext) => requireContext.keys().map(requireContext)
9 |
10 | function loadStories() {
11 | requireAll(appStories)
12 | }
13 |
14 |
15 | configure(loadStories, module)
16 |
--------------------------------------------------------------------------------
/.storybook/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | module: {
5 | rules: [
6 | {
7 | test: /\.(scss|sass|css)$/,
8 | use: [{
9 | loader: 'style-loader' // creates style nodes from JS strings
10 | }, {
11 | loader: 'css-loader' // translates CSS into CommonJS
12 | }, {
13 | loader: 'resolve-url-loader' // resolve relative urls inside the css files
14 | },{
15 | loader: 'sass-loader?sourceMap' // compiles Sass to CSS
16 | }],
17 | include: [
18 | path.resolve(__dirname, '../src'),
19 | path.resolve(__dirname, '../node_modules/@fortawesome'),
20 | ],
21 | }, {
22 | test: /\.(woff2?|eot|ttf|svg|otf)(\?.+)?$/i,
23 | use: [
24 | {
25 | loader: 'url-loader',
26 | options: {
27 | limit: 10000,
28 | name: '[name].[ext]',
29 | },
30 | },
31 | ],
32 | }
33 | ]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/.travis.script.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # set exit on exception
3 | set -e
4 |
5 | if [ "${TASK}" = "jest" ]; then
6 | yarn jest --coverage
7 | fi
8 |
9 | if [ "${TASK}" = "eslint" ]; then
10 | eslint src/
11 | fi
12 |
13 | if [ "${TASK}" = "build" ]; then
14 | yarn build
15 | fi
16 |
17 | if [ "${TASK}" = "nojsx" ]; then
18 | if find src -type f -name "*.jsx" | grep ".jsx"; then
19 | echo "Extension *.jsx is not encouraged on this project. Please use *.js"
20 | exit 1
21 | else
22 | echo "All good"
23 | fi
24 | fi
25 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js: 10.16.3
4 |
5 | branches:
6 | only:
7 | - master
8 |
9 | env:
10 | matrix:
11 | - TASK=nojsx
12 | - TASK=jest NODE_VERSION=v8.15.0 YARN_VERSION=1.13.0
13 | - TASK=eslint NODE_VERSION=v8.15.0
14 | - TASK=build
15 |
16 | install:
17 | - if [ $TASK == jest ];
18 | then npm i -g yarn@1.3.2;
19 | yarn install;
20 |
21 | elif [ $TASK = eslint ]; then
22 | npm i eslint
23 | eslint-plugin-react-hooks
24 | eslint-plugin-promise
25 | eslint-plugin-standard
26 | eslint-config-standard
27 | eslint-config-standard-react
28 | eslint-config-standard-jsx
29 | eslint-plugin-react
30 | eslint-plugin-babel
31 | babel-eslint
32 | eslint-plugin-import
33 | eslint-plugin-node;
34 |
35 | elif [ $TASK == build ]; then npm i -g yarn@1.3.2; yarn install; fi
36 |
37 | script: ./.travis.script.sh
38 |
39 | after_script:
40 | - if [ $TASK == jest ]; then yarn codecov; fi
41 |
42 |
43 | notifications:
44 | email: false
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Uptick
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-object-list
2 |
3 | [](http://badge.fury.io/js/react-object-list)
4 | 
5 | [](https://travis-ci.org/uptick/react-object-list)
6 | [](https://codecov.io/gh/uptick/react-object-list)
7 |
8 | Component used to display an array of object based data in a sortable, filterable, paginated, list based view, powered by React.
9 |
10 | Custom components can be passed in if they are designed to handle the same props as the default components. This functionality is available for the DataRenderer, Pagination and ErrorMessage and your component can simply be passed in through the props with one of the above keys.
11 |
12 | ## Live Demo
13 |
14 | Check out the live demo here: http://uptick.github.io/react-object-list/
15 |
16 | ## Installation
17 |
18 | Install the package:
19 |
20 | ```
21 | yarn add react-object-list
22 | ```
23 | Ensure you have all peer dependencies installed
24 | ```
25 | yarn add classnames moment prop-types react-month-picker react-select
26 | ```
27 |
28 |
29 | Include icons from FontAwesome 4:
30 |
31 | ```javascript
32 | import React from 'react'
33 | import ReactDOM from 'react-dom'
34 |
35 | import ObjectList from 'react-object-list'
36 | import {FontAwesome} from 'react-object-list/icons'
37 |
38 | var mount = document.querySelectorAll('div.browser-mount');
39 | ReactDOM.render(
40 | ,
43 | mount[0]
44 | );
45 | ```
46 |
47 | or your own icons by specifying as so:
48 | ```javascript
49 | ,
52 | Favourites: ,
53 | RemoveFavourite: ,
54 | RemoveFilter: ,
55 | DropdownOpen: ,
56 | DropdownClose: ,
57 | SortAsc: ,
58 | SortDesc: ,
59 | Unsorted: ,
60 | Loading: ,
61 | CheckboxChecked: ,
62 | CheckboxUnchecked: ,
63 | }}
64 | />
65 | ```
66 |
67 | Unspecified icons will not show (excl. RemoveFavourite, SortAsc, SortDesc, CheckboxChecked, CheckboxUnchecked, RemoveFilter).
68 |
--------------------------------------------------------------------------------
/__mocks__/react-select/index.js:
--------------------------------------------------------------------------------
1 | import Select from './lib/default'
2 | export default Select
3 | export const components = {Option: 'Option'}
4 |
--------------------------------------------------------------------------------
/__mocks__/react-select/lib/Async.js:
--------------------------------------------------------------------------------
1 | const Async = 'Select.Async'
2 | export default Async
3 |
--------------------------------------------------------------------------------
/__mocks__/react-select/lib/default.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const Select = props => React.createElement('Select', props, props.children)
5 | /* eslint-disable-next-line */
6 | Select.propTypes = {value: PropTypes.any, children: PropTypes.any}
7 |
8 | export default Select
9 |
--------------------------------------------------------------------------------
/__mocks__/styleMock.js:
--------------------------------------------------------------------------------
1 | module.exports = {}
2 |
--------------------------------------------------------------------------------
/demo-site/README.md:
--------------------------------------------------------------------------------
1 | ```
2 | rm yarn.lock && rm -rf node_modules
3 | yarn
4 | yarn build
5 | jekyll build
6 | jekyll serve
7 | ```
8 |
--------------------------------------------------------------------------------
/demo-site/_config.yml:
--------------------------------------------------------------------------------
1 | layouts_dir: 'node_modules/uptick-demo-site/dist'
2 |
3 | package_name: React Object List
4 | package_github_url: https://github.com/uptick/react-object-list
5 | package_npm_url: https://www.npmjs.com/package/react-object-list
6 |
--------------------------------------------------------------------------------
/demo-site/_includes/head.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/demo-site/_includes/required_static.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/demo-site/demos.jsx:
--------------------------------------------------------------------------------
1 | import { init } from 'uptick-demo-site'
2 |
3 | import SimpleDemo from './simple-demo.jsx'
4 | import Table from './table-demo.jsx'
5 | import List from './list-demo.jsx'
6 | import './demos.sass'
7 |
8 | init()
9 |
--------------------------------------------------------------------------------
/demo-site/demos.sass:
--------------------------------------------------------------------------------
1 | @import 'node_modules/uptick-demo-site/dist/uptick-demo-site'
2 | @import 'node_modules/react-object-list/styles/main'
3 | @import 'node_modules/font-awesome/css/font-awesome.min'
4 |
--------------------------------------------------------------------------------
/demo-site/index.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: base
3 | ---
4 |
5 |
8 |
To participate in the demonstration you will need:
9 |
10 | - Javascript enabled
11 | - A modern browser
12 |
13 |
All examples given are written with babel loaders to support es6 (stage-0) and JSX syntax.
14 |
The demo data used in the examples can be found here: demo.data.json
15 |
16 |
Simple Example
17 |
This is the simplest implementation of React-Object-List, displaying data in a table.
18 |
19 |
Loading ...
23 |
24 |
Table render
25 |
This example demonstrates:
26 |
27 | - Filters
28 | - Optional Columns
29 | - Pagination
30 |
31 |
32 |
Loading ...
36 |
37 |
List render
38 |
This example demonstrates:
39 |
42 |
43 |
Loading ...
47 |
48 |
--------------------------------------------------------------------------------
/demo-site/list-demo.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDom from 'react-dom'
3 | import ObjectList from 'react-object-list'
4 | import { List } from 'react-object-list/renderers'
5 |
6 | const mockData = require('./demo.data.json').slice(0, 3)
7 |
8 | ReactDom.render(
9 | ,
25 | document.querySelector('div.demo-mount-list')
26 | )
27 |
--------------------------------------------------------------------------------
/demo-site/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-object-list-demo-site",
3 | "version": "1.0.0",
4 | "description": "Demo site for react-object-list package",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "build": "webpack",
9 | "build:watch": "webpack --watch",
10 | "watch": "parallel --ungroup ::: \"yarn build:watch\" \"jekyll serve\""
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "react-object-list"
15 | },
16 | "author": "Uptick Pty Ltd",
17 | "license": "MIT",
18 | "dependencies": {
19 | "babel-regenerator-runtime": "^6.5.0",
20 | "classnames": "^2.2.5",
21 | "font-awesome": "^4.7.0",
22 | "moment": "^2.21.0",
23 | "prop-types": "^15.6.1",
24 | "react": "^16.2.0",
25 | "react-day-picker": "^7.1.4",
26 | "react-dom": "^16.2.0",
27 | "react-month-picker": "^1.3.5",
28 | "react-object-list": "^0.1.1",
29 | "react-select": "^2.0.0-beta.6",
30 | "uptick-demo-site": "latest"
31 | },
32 | "devDependencies": {
33 | "babel-cli": "^6.26.0",
34 | "babel-core": "^6.26.0",
35 | "babel-loader": "^7.1.4",
36 | "babel-preset-es2015": "^6.24.1",
37 | "babel-preset-react": "^6.24.1",
38 | "babel-preset-stage-0": "^6.24.1",
39 | "css-loader": "^0.28.11",
40 | "extract-text-webpack-plugin": "^4.0.0-beta.0",
41 | "node-sass": "^4.7.2",
42 | "resolve-url-loader": "^2.3.0",
43 | "sass-loader": "^6.0.7",
44 | "url-loader": "^1.0.1",
45 | "webpack": "^4.1.1"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/demo-site/simple-demo.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDom from 'react-dom'
3 | import ObjectList from 'react-object-list'
4 |
5 | const mockData = require('./demo.data.json').slice(0, 5)
6 |
7 | ReactDom.render(
8 | ,
23 | document.querySelector('div.demo-mount-simple')
24 | )
25 |
--------------------------------------------------------------------------------
/demo-site/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
4 | const extractSass = new ExtractTextPlugin({
5 | filename: 'demos.css',
6 | })
7 |
8 | module.exports = {
9 | entry: ['babel-regenerator-runtime', './demos.jsx'],
10 | mode: 'production',
11 | output: {
12 | path: path.resolve(__dirname, 'dist'),
13 | filename: 'demos.js',
14 | },
15 | module: {
16 | rules: [
17 | {
18 | test: /\.jsx?$/,
19 | exclude: /node_modules/,
20 | use: {
21 | loader: 'babel-loader',
22 | options: {
23 | babelrc: false,
24 | presets: [
25 | 'react',
26 | 'es2015',
27 | 'stage-0',
28 | ],
29 | },
30 | },
31 | }, {
32 | test: /\.(scss|sass)$/,
33 | exclude: /node_modules/,
34 | use: extractSass.extract({
35 | use: [{
36 | loader: 'css-loader', // translates CSS into CommonJS
37 | options: {
38 | minimize: true,
39 | },
40 | }, {
41 | loader: 'resolve-url-loader', // resolve relative urls inside the css files
42 | }, {
43 | loader: 'sass-loader?sourceMap', // compiles Sass to CSS
44 | }],
45 | }),
46 | }, {
47 | test: /\.(woff2?|eot|ttf|svg|otf)(\?.+)?$/i,
48 | use: [
49 | {
50 | loader: 'url-loader',
51 | options: {
52 | limit: 10000,
53 | name: '[name].[ext]',
54 | },
55 | },
56 | ],
57 | },
58 | ],
59 | },
60 | plugins: [
61 | extractSass,
62 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
63 | ],
64 | resolve: {
65 | modules: [
66 | path.resolve('node_modules'),
67 | ],
68 | alias: {
69 | mireco: path.resolve('..'),
70 | },
71 | },
72 | }
73 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | setupFiles: ['./.jest/jest.setup.js', './.jest/register-context.js'],
3 | snapshotSerializers: ['enzyme-to-json/serializer'],
4 | modulePaths: [
5 | '.',
6 | ],
7 | moduleNameMapper: {
8 | '\\.(css|less|sass)$': '/__mocks__/styleMock.js',
9 | },
10 | collectCoverageFrom: [
11 | 'src/**/*.js',
12 | '!src/**/*.stories.js',
13 | ],
14 | testURL: 'http://localhost/',
15 | }
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-object-list",
3 | "version": "0.2.10",
4 | "description": "React component to display an array of object data in a filterable view",
5 | "repository": {
6 | "url": "https://github.com/uptick/react-object-list",
7 | "type": "git"
8 | },
9 | "bugs": "https://github.com/uptick/react-object-list/issues",
10 | "files": [
11 | "main.js",
12 | "styles",
13 | "dist",
14 | "filters.js",
15 | "cells.js",
16 | "renderers.js",
17 | "utils.js",
18 | "icons.js",
19 | "src/resources/colours.sass",
20 | "src/resources/dark-table.sass",
21 | "src/resources/input-range.sass",
22 | "src/resources/main.sass"
23 | ],
24 | "main": "main.js",
25 | "scripts": {
26 | "publish-demo": "git branch -D gh-pages; git push origin --delete gh-pages; git checkout -b gh-pages; cd demo-site; yarn; npm run build; cd ..; git add .; git add -f demo-site/dist; git add -f demo-site/node_modules/uptick-demo-site/dist; git commit -m \"Demo site build\"; git push origin gh-pages; git checkout master; git push origin `git subtree split --prefix demo-site gh-pages`:gh-pages --force;",
27 | "build:js": "webpack --mode production",
28 | "build:css": "mkdir -p dist && npx node-sass src/resources/main.sass > dist/react-object-list.css",
29 | "build": "yarn build:js && yarn build:css",
30 | "dev": "webpack --mode development",
31 | "watch": "webpack --mode development --watch",
32 | "test": "jest",
33 | "storybook": "start-storybook -p 9001 -c .storybook",
34 | "prepare": "yarn build"
35 | },
36 | "author": "Uptick Pty Ltd (http://uptickhq.com)",
37 | "license": "MIT",
38 | "devDependencies": {
39 | "@fortawesome/fontawesome-free": "^5.4.1",
40 | "@storybook/addon-actions": "^4.0.0-alpha.8",
41 | "@storybook/addon-info": "^4.0.0-alpha.8",
42 | "@storybook/addon-knobs": "^4.0.0-alpha.8",
43 | "@storybook/addon-storyshots": "^4.0.0-alpha.8",
44 | "@storybook/addons": "^4.0.0-alpha.8",
45 | "@storybook/react": "^4.0.0-alpha.8",
46 | "babel-core": "^6.26.0",
47 | "babel-eslint": "^10.0.3",
48 | "babel-loader": "^7.1.4",
49 | "babel-plugin-require-context-hook": "^1.0.0",
50 | "babel-polyfill": "^6.26.0",
51 | "babel-preset-env": "^1.6.1",
52 | "babel-preset-react": "^6.24.1",
53 | "babel-preset-stage-0": "^6.24.1",
54 | "classnames": "^2.2.5",
55 | "codecov": "^3.0.0",
56 | "css-loader": "^0.28.10",
57 | "enzyme": "^3.3.0",
58 | "enzyme-adapter-react-16": "^1.1.1",
59 | "enzyme-to-json": "^3.3.1",
60 | "eslint": "^6.0.0",
61 | "eslint-config-standard": "^12.0.0",
62 | "eslint-config-standard-react": "^7.0.2",
63 | "eslint-plugin-babel": "^5.1.0",
64 | "eslint-plugin-import": "^2.14.0",
65 | "eslint-plugin-node": "^7.0.1",
66 | "eslint-plugin-promise": "^4.0.0",
67 | "eslint-plugin-react": "^7.11.1",
68 | "eslint-plugin-standard": "^4.0.0",
69 | "extract-text-webpack-plugin": "^4.0.0-beta.0",
70 | "file-loader": "^1.1.11",
71 | "jest": "^22.4.2",
72 | "moment": "^2.21.0",
73 | "node-sass": "^4.12.0",
74 | "peer-deps-externals-webpack-plugin": "^1.0.2",
75 | "prop-types": "^15.6.1",
76 | "raf": "^3.4.0",
77 | "react": "^16.2.0",
78 | "react-dom": "^16.2.0",
79 | "react-month-picker": "^1.3.5",
80 | "react-select": "^2.4.2",
81 | "react-test-renderer": "^16.2.0",
82 | "resolve-url-loader": "^2.3.0",
83 | "sass-loader": "^6.0.7",
84 | "style-loader": "^0.20.2",
85 | "url-loader": "^1.0.1",
86 | "webpack": "^4.41.2",
87 | "webpack-cli": "3.1.2"
88 | },
89 | "peerDependencies": {
90 | "classnames": "^2.2.5",
91 | "moment": "^2.21.0",
92 | "prop-types": "^15.6.1",
93 | "react": "15.x - 16.x",
94 | "react-dom": "15.x - 16.x",
95 | "react-month-picker": "^1.3.5",
96 | "react-select": "^2.0.0-beta.6"
97 | },
98 | "dependencies": {
99 | "date-fns": "^2.4.1",
100 | "mireco": "^0.0.20"
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/__tests__/ObjectList.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { shallow } from 'enzyme'
3 |
4 | import ObjectList from '../'
5 |
6 | describe('ObjectList', () => {
7 | describe('state', () => {
8 | it('sets itemSingleName/itemPluralName to "item/items" if unset', () => {
9 | const instance = shallow().instance()
10 | expect(instance.state.itemSingleName).toBe('item')
11 | expect(instance.state.itemPluralName).toBe('items')
12 | })
13 |
14 | it('sets itemSingleName/itemPluralName to "blah/blahs" if only itemSingleName set', () => {
15 | const instance = shallow().instance()
16 | expect(instance.state.itemSingleName).toBe('blah')
17 | expect(instance.state.itemPluralName).toBe('blahs')
18 | })
19 |
20 | it('sets itemSingleName/itemPluralName to "blah/blahies" if both names set', () => {
21 | const instance = shallow().instance()
22 | expect(instance.state.itemSingleName).toBe('blah')
23 | expect(instance.state.itemPluralName).toBe('blahies')
24 | })
25 | })
26 |
27 | describe('Functions', () => {
28 | const props = {
29 | selectItems: jest.fn(),
30 | }
31 |
32 | it('handles selectAll correctly', () => {
33 | spyOn(props, 'selectItems')
34 | const instance = shallow().instance()
35 | instance.selectAll()
36 | expect(props.selectItems).toHaveBeenCalledWith('all')
37 | })
38 |
39 | it('handles deselectAll correctly', () => {
40 | spyOn(props, 'selectItems')
41 | const instance = shallow().instance()
42 | instance.deselectAll()
43 | expect(props.selectItems).toHaveBeenCalledWith(null)
44 | })
45 | })
46 | })
47 |
--------------------------------------------------------------------------------
/src/actions-filters/Favourites.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 | import { withInfo } from '@storybook/addon-info'
4 | import { action } from '@storybook/addon-actions'
5 |
6 | import Favourites from './Favourites'
7 |
8 | const defaultFilterValues = {
9 | filters: {},
10 | activeSort: '-id',
11 | optionalFields: {},
12 | activeFilters: {},
13 | }
14 |
15 | const defaultProps = {
16 | favourites: [
17 | {
18 | name: 'Favourite 1',
19 | ...defaultFilterValues,
20 | },
21 | {
22 | name: 'Favourite 2',
23 | ...defaultFilterValues,
24 | optionalFields: {name: true},
25 | },
26 | ],
27 | handleDeleteFavourite: action('deleteFavourite'),
28 | handleAddFavourite: action('addFavourite'),
29 | }
30 |
31 | storiesOf('object-list/Favourites', module)
32 | .addDecorator((story, context) => withInfo(
33 | 'Favourites dropdown selector'
34 | )(story)(context))
35 | .add('default view', () => (
36 |
37 |
38 |
39 | )).add('interactive', () => {
40 | class FavouritesWrapper extends React.Component {
41 | state = {favourites: defaultProps.favourites}
42 | addFavourite = (favName) => this.setState(({favourites}) =>
43 | ({favourites: favourites.filter(({name}) => name !== favName).concat({name: favName, ...defaultFilterValues})}))
44 | deleteFavourite = (favName) => this.setState(({favourites}) =>
45 | ({favourites: favourites.filter(({name}) => name !== favName)}))
46 | render() {
47 | return (
48 | )
54 | }
55 | }
56 | return (
57 |
58 |
59 |
60 | )
61 | })
62 |
--------------------------------------------------------------------------------
/src/actions-filters/FavouritesItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import ClassNames from 'classnames'
4 |
5 | /**
6 | * Class representing one item in a favourites dropdown found in the api-list
7 | */
8 | export default class FavouritesItem extends React.Component {
9 | static propTypes = {
10 | /** callback function that sets this favourite filter as active in the api-list */
11 | loadFavourite: PropTypes.func,
12 | /** filters associated with a particular favourite e.g. { "status": "&status=ACTIVE%2CSETUP"} */
13 | filters: PropTypes.object,
14 | /** the name of the favourite */
15 | name: PropTypes.string,
16 | /** the kind of data being displayed by the api-list eg. "properties:property_list" */
17 | handleDelete: PropTypes.func,
18 | /** whether or not the particular filter is selected */
19 | isSelected: PropTypes.bool,
20 | /** icon to render to remove a favourite */
21 | RemoveFavouriteIcon: PropTypes.element,
22 | }
23 |
24 | static defaultProps = {
25 | RemoveFavouriteIcon: Delete,
26 | }
27 |
28 | /**
29 | * This function calls the callback function to set the current
30 | * favourite to active
31 | *
32 | * @param {MouseEvent} event - the click event
33 | */
34 | handleClick = event => {
35 | event.preventDefault()
36 | const {loadFavourite, name, filters: {activeFilters, optionalFields, activeSort}} = this.props
37 | loadFavourite(name, activeFilters, optionalFields, activeSort)
38 | }
39 |
40 | handleDelete = event => {
41 | event.preventDefault()
42 | this.props.handleDelete(this.props.name)
43 | }
44 |
45 | render() {
46 | const {RemoveFavouriteIcon, isSelected, name} = this.props
47 | return (
48 |
53 |
54 | {name}
55 |
56 | {RemoveFavouriteIcon && React.cloneElement(RemoveFavouriteIcon, {onClick: this.handleDelete})}
57 |
58 | )
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/actions-filters/FiltersContainer.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import {FILTER_BASE_TYPE} from '../utils/proptypes'
4 |
5 | class FiltersContainer extends Component {
6 | static propTypes = {
7 | /** array of potential filters that can be displayed inside the object-list */
8 | filters: PropTypes.arrayOf(PropTypes.shape({
9 | ...FILTER_BASE_TYPE,
10 | active: PropTypes.bool,
11 | })),
12 | /** callback containing {filterKey, comparison, value} for the individual filter */
13 | updateFilter: PropTypes.func,
14 | /** callback to remove a filter from the active list of filters */
15 | removeFilter: PropTypes.func,
16 | /** icons to use */
17 | icons: PropTypes.object,
18 | /** Object of custom react-select styles */
19 | selectStyles: PropTypes.object,
20 | }
21 | static defaultProps = {
22 | filters: [],
23 | activeFilters: [],
24 | icons: {},
25 | selectStyles: {},
26 | }
27 |
28 | renderFilter = (filter, i) => {
29 | const { Renderer, filterKey, loadOptions, value, ...props } = filter
30 | const { removeFilter, updateFilter, icons, selectStyles } = this.props
31 | return (
32 |
43 | )
44 | }
45 |
46 | render() {
47 | return (
48 |
49 | {this.props.filters.filter(filter => filter.active).map(this.renderFilter)}
50 |
51 | )
52 | }
53 | }
54 |
55 | export default FiltersContainer
56 |
--------------------------------------------------------------------------------
/src/actions-filters/FiltersContainer.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 | import { withInfo } from '@storybook/addon-info'
4 | import { action } from '@storybook/addon-actions'
5 |
6 | import FiltersContainer from './FiltersContainer'
7 | import * as importedFilters from '../filters'
8 |
9 | const filters = Object.entries(importedFilters).map(([name, filter]) => {
10 | const props = {}
11 | switch (name) {
12 | case 'RemoteChoiceFilter':
13 | case 'RemoteMultiChoiceFilter':
14 | props.loadOptions = action('loadOptions')
15 | break
16 | case 'DayFilter':
17 | props.name = null
18 | break
19 | }
20 |
21 | return {
22 | Renderer: filter,
23 | filterKey: name,
24 | active: true,
25 | name,
26 | ...props,
27 | }
28 | })
29 |
30 | storiesOf('object-list/FiltersContainer', module)
31 | .addDecorator((story, context) => withInfo(
32 | 'Container to render all the filters underneath eachother'
33 | )(story)(context))
34 | .add('default view', () => (
35 |
40 | ))
41 |
--------------------------------------------------------------------------------
/src/actions-filters/OptionalField.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | /**
5 | * Represents a single item in the "Visible Fields" drop-down
6 | */
7 | export default class OptionalField extends React.Component {
8 | static propTypes = {
9 | /** callback function to enable the optional field and save it to user preferences */
10 | onChange: PropTypes.func.isRequired,
11 | /** the api key used for this particular field */
12 | fieldKey: PropTypes.string,
13 | /** whether or not the field is enabled (displayed) */
14 | enabled: PropTypes.bool,
15 | /** the display text for the field */
16 | name: PropTypes.string,
17 | /** Icon to display when a field is enabled */
18 | CheckboxCheckedIcon: PropTypes.element,
19 | /** Icon to display when a field is not enabled */
20 | CheckboxUnCheckedIcon: PropTypes.element,
21 | }
22 |
23 | static defaultProps = {
24 | enabled: false,
25 | CheckboxCheckedIcon: ☑,
26 | CheckboxUnCheckedIcon: ☐,
27 | }
28 |
29 | /**
30 | * Toggles an optional field to be displayed by the objectlist
31 | *
32 | * @event {MouseEvent} event - event that fires when clicking on an OptionalField component
33 | */
34 | toggle = (event) => {
35 | event.preventDefault()
36 | this.props.onChange(this.props.fieldKey)
37 | }
38 |
39 | render() {
40 | const {enabled, CheckboxCheckedIcon, CheckboxUnCheckedIcon, name} = this.props
41 | const checked = enabled ? CheckboxCheckedIcon : CheckboxUnCheckedIcon
42 | return (
43 |
46 | )
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/actions-filters/OptionalFields.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 | import { withInfo } from '@storybook/addon-info'
4 | import { action } from '@storybook/addon-actions'
5 |
6 | import OptionalFields from './OptionalFields'
7 |
8 | const defaultProps = {
9 | extraColumns: ['a', 'b'],
10 | optionalFields: [
11 | {dataKey: 'a', header: 'Apple'},
12 | {dataKey: 'c', header: 'Cat'},
13 | {dataKey: 'd', header: 'Dragon'},
14 | {dataKey: 'e', header: 'Elephant'},
15 | {dataKey: 'f', header: 'Frappuccino'},
16 | {dataKey: 'g', header: 'Grizzly Bear'},
17 | {dataKey: 'h', header: 'Heaps Good Ay'},
18 | {dataKey: 'i', header: 'I\'m just waitin\' for a mate'},
19 | ],
20 | updateColumns: action('updateColumns'),
21 | }
22 |
23 | storiesOf('object-list/OptionalFields', module)
24 | .addDecorator((story, context) => withInfo(
25 | 'Optional fields dropdown selector'
26 | )(story)(context))
27 | .add('default view', () => (
28 |
29 |
30 |
31 |
32 |
33 | ))
34 |
--------------------------------------------------------------------------------
/src/actions-filters/SelectAllAction.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | class SelectAllAction extends Component {
5 | static propTypes = {
6 | /** the total number of items in the dataset */
7 | count: PropTypes.number,
8 | /** the amount of items displayed on the current page */
9 | itemCount: PropTypes.number,
10 | /** the name used to describe multiples of the items displayed */
11 | itemPluralName: PropTypes.string,
12 | /** the number of items currently selected */
13 | numSelected: PropTypes.number,
14 | /** callback function to handle selecting all items in the dataset. Set to null to not show it. */
15 | selectAll: PropTypes.func,
16 | /** callback function to handle deselecting all items */
17 | deselectAll: PropTypes.func,
18 | }
19 |
20 | handleSelectAllClick = (event) => {
21 | event.preventDefault()
22 | this.props.selectAll()
23 | }
24 |
25 | handleDeselectAllClick = event => {
26 | event.preventDefault()
27 | this.props.deselectAll()
28 | }
29 |
30 | render() {
31 | const {selectAll, count, numSelected, itemPluralName, itemCount, deselectAll} = this.props
32 | let selectAllLink
33 | if (selectAll && count > 0 && numSelected < count && numSelected >= itemCount) {
34 | selectAllLink = (
35 |
40 | Select all {count.toLocaleString()} {itemPluralName && itemPluralName.toLowerCase()}
41 |
42 | )
43 | }
44 | let deselectLink
45 | if (deselectAll && numSelected > 0) {
46 | deselectLink = (
47 |
52 | Clear selection
53 |
54 | )
55 | }
56 | if (selectAllLink || deselectLink) {
57 | return (
58 |
59 | {selectAllLink}{deselectLink}
60 |
61 | )
62 | } else {
63 | return null
64 | }
65 | }
66 | }
67 |
68 | export default SelectAllAction
69 |
--------------------------------------------------------------------------------
/src/actions-filters/SelectFilters.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import PropTypes from 'prop-types'
3 | import Select from '../utils/Select'
4 | import {FILTER_BASE_TYPE} from '../utils/proptypes'
5 | import {sortByName} from '../utils'
6 |
7 | class SelectFilters extends Component {
8 | static propTypes = {
9 | /** array of potential filters that can be displayed inside the object-list */
10 | filters: PropTypes.arrayOf(PropTypes.shape({
11 | ...FILTER_BASE_TYPE,
12 | active: PropTypes.bool,
13 | })),
14 | /** callback to add a filter to the list of active filters */
15 | addFilter: PropTypes.func,
16 | /** Object of custom react-select styles */
17 | selectStyles: PropTypes.object,
18 | }
19 | static defaultProps = {
20 | filters: [],
21 | selectStyles: {},
22 | }
23 | render() {
24 | const {filters, addFilter, selectStyles} = this.props
25 | if (filters.length === 0) { return null }
26 |
27 | let quickFilters = filters.filter(filter =>
28 | !filter.active && !filter.alwaysVisible
29 | )
30 | quickFilters = quickFilters.sort(sortByName)
31 |
32 | // There's something pretty odd happening with react-select v2 that
33 | // causes any option that itself contains an `options` field to use
34 | // that subfield instead of itself. WTF.
35 | // TODO: Check if this issue has been resolved in a later version.
36 | quickFilters = quickFilters.map(({options, ...rest}) => rest)
37 |
38 | return (
39 |
50 | )
51 | }
52 | }
53 |
54 | export default SelectFilters
55 |
--------------------------------------------------------------------------------
/src/actions-filters/SelectFilters.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 | import { withInfo } from '@storybook/addon-info'
4 | import { action } from '@storybook/addon-actions'
5 |
6 | import SelectFilters from './SelectFilters'
7 | import * as importedFilters from '../filters'
8 |
9 | const filters = Object.entries(importedFilters).map(([name, filter]) => {
10 | const props = {}
11 | switch (name) {
12 | case 'RemoteChoiceFilter':
13 | case 'RemoteMultiChoiceFilter':
14 | props.loadOptions = action('loadOptions')
15 | }
16 |
17 | return {
18 | Renderer: filter,
19 | filterKey: name,
20 | active: false,
21 | name,
22 | ...props,
23 | }
24 | })
25 |
26 | storiesOf('object-list/SelectFilters', module)
27 | .addDecorator((story, context) => withInfo(
28 | 'Filters dropdown selector'
29 | )(story)(context))
30 | .add('default view', () => (
31 |
35 | ))
36 |
--------------------------------------------------------------------------------
/src/actions-filters/__snapshots__/Favourites.stories.storyshot:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Storyshots object-list/Favourites default view 1`] = `
4 |
11 |
14 |
20 |
23 |
27 |
28 | Favourite 1
29 |
30 |
33 | Delete
34 |
35 |
36 |
40 |
41 | Favourite 2
42 |
43 |
46 | Delete
47 |
48 |
49 |
52 |
55 |
72 |
73 |
74 |
75 |
76 | `;
77 |
78 | exports[`Storyshots object-list/Favourites interactive 1`] = `
79 |
86 |
89 |
95 |
98 |
102 |
103 | Favourite 1
104 |
105 |
108 | Delete
109 |
110 |
111 |
115 |
116 | Favourite 2
117 |
118 |
121 | Delete
122 |
123 |
124 |
127 |
130 |
147 |
148 |
149 |
150 |
151 | `;
152 |
--------------------------------------------------------------------------------
/src/actions-filters/__snapshots__/OptionalFields.stories.storyshot:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Storyshots object-list/OptionalFields default view 1`] = `
4 |
12 |
19 |
22 |
27 |
30 |
37 |
44 |
51 |
58 |
65 |
72 |
79 |
86 |
87 |
88 |
89 |
90 | `;
91 |
--------------------------------------------------------------------------------
/src/actions-filters/__snapshots__/SelectFilters.stories.storyshot:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Storyshots object-list/SelectFilters default view 1`] = `
4 |
100 | `;
101 |
--------------------------------------------------------------------------------
/src/actions-filters/__tests__/FavouritesItem.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { shallow } from 'enzyme'
3 | import FavouritesItem from '../FavouritesItem'
4 |
5 | describe('FavouritesItem', () => {
6 | describe('Functions', () => {
7 | it('loads favourite on handleClick', () => {
8 | const props = {
9 | name: 'Springfield',
10 | filters: {
11 | activeFilters: 'AF',
12 | optionalFields: 'OF',
13 | activeSort: 'AS',
14 | },
15 | loadFavourite: jest.fn(),
16 | }
17 | spyOn(props, 'loadFavourite')
18 | const instance = shallow().instance()
19 |
20 | const mockEvent = {preventDefault: jasmine.createSpy()}
21 | instance.handleClick(mockEvent)
22 | expect(mockEvent.preventDefault).toHaveBeenCalled()
23 | expect(props.loadFavourite).toHaveBeenCalledWith('Springfield', 'AF', 'OF', 'AS')
24 | })
25 |
26 | it('calls back to delete favourite on handleDelete ', () => {
27 | const props = {
28 | name: 'Shelbyville',
29 | handleDelete: jest.fn(),
30 | }
31 | spyOn(props, 'handleDelete')
32 | const instance = shallow().instance()
33 |
34 | const mockEvent = {preventDefault: jasmine.createSpy()}
35 | instance.handleDelete(mockEvent)
36 | expect(mockEvent.preventDefault).toHaveBeenCalled()
37 | expect(props.handleDelete).toHaveBeenCalledWith('Shelbyville')
38 | })
39 | })
40 | })
41 |
--------------------------------------------------------------------------------
/src/actions-filters/__tests__/FiltersContainer.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { snapshotTest } from 'utils/tests'
3 | import FiltersContainer from '../FiltersContainer'
4 |
5 | describe('', () => {
6 | describe('Snapshot', () => {
7 | const Renderer = (props) => {props.filterKey}
8 | const defaultProps = {
9 | filters: [
10 | {filterKey: 'a', value: 'some value', active: true, Renderer},
11 | {filterKey: 'b', active: false, Renderer},
12 | {filterKey: 'c', value: 'some other value', active: false, Renderer},
13 | ],
14 | updateFilter: jest.fn(),
15 | }
16 | afterEach(() => {
17 | defaultProps.updateFilter.mockClear()
18 | })
19 |
20 | it('renders correctly', () => {
21 | snapshotTest()
22 | })
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/src/actions-filters/__tests__/OptionalField.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { shallow } from 'enzyme'
3 | import { snapshotTest } from 'utils/tests'
4 | import OptionalField from '../OptionalField'
5 |
6 | describe('OptionalField', () => {
7 | const defaultProps = {
8 | onChange: jest.fn(),
9 | fieldKey: 'a',
10 | name: 'Apple',
11 | }
12 | describe('Snapshot', () => {
13 | it('renders default', () => {
14 | snapshotTest()
15 | })
16 | it('renders enabled', () => {
17 | snapshotTest()
18 | })
19 | })
20 | describe('Functions', () => {
21 | it('toggles', () => {
22 | const mockEvent = {
23 | preventDefault: jasmine.createSpy(),
24 | }
25 | spyOn(defaultProps, 'onChange')
26 | const instance = shallow().instance()
27 | instance.toggle(mockEvent)
28 | expect(mockEvent.preventDefault).toHaveBeenCalled()
29 | expect(defaultProps.onChange).toHaveBeenCalledWith(defaultProps.fieldKey)
30 | })
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/src/actions-filters/__tests__/OptionalFields.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { shallow } from 'enzyme'
3 | import { snapshotTest } from 'utils/tests'
4 | import OptionalFields from '../OptionalFields'
5 |
6 | jest.mock('../OptionalField', () => 'OptionalField')
7 |
8 | describe('', () => {
9 | const defaultProps = {
10 | extraColumns: ['a', 'b'],
11 | optionalFields: [
12 | {dataKey: 'a', header: 'Apple'},
13 | {dataKey: 'c', header: 'Cat'},
14 | ],
15 | updateColumns: jest.fn(),
16 | }
17 | describe('Snapshot', () => {
18 | afterEach(() => {
19 | defaultProps.updateColumns.mockClear()
20 | })
21 |
22 | it('renders correctly', () => {
23 | snapshotTest()
24 | snapshotTest(, {optionalFieldsOpen: true})
25 | })
26 | })
27 | describe('Functions', () => {
28 | describe('handles dropdown', () => {
29 | let instance
30 | const mockEvent = {
31 | target: null,
32 | }
33 | beforeEach(() => {
34 | instance = shallow().instance()
35 | instance.setState({optionalFieldsOpen: true})
36 | instance.optionalFieldsDropdown = 'optionalFieldsDropdown'
37 | instance.optionalFieldsButton = 'buttonOF'
38 | })
39 | it('target is elsewhere', () => {
40 | instance.handleDropdown(mockEvent)
41 | expect(instance.state.optionalFieldsOpen).toBeFalsy()
42 | })
43 | it('target contains element', () => {
44 | instance.handleDropdown({target: {parentElement: {parentElement: instance.optionalFieldsDropdown}}})
45 | expect(instance.state.optionalFieldsOpen).toBeTruthy()
46 | })
47 | it('target is element', () => {
48 | mockEvent.target = instance.optionalFieldsButton
49 | instance.handleDropdown(mockEvent)
50 | expect(instance.state.optionalFieldsOpen).toBe(false)
51 | instance.handleDropdown(mockEvent)
52 | expect(instance.state.optionalFieldsOpen).toBe(true)
53 | })
54 | })
55 | })
56 | })
57 |
--------------------------------------------------------------------------------
/src/actions-filters/__tests__/SelectAllAction.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { shallow } from 'enzyme'
3 | import { snapshotTest } from 'utils/tests'
4 | import SelectAllAction from '../SelectAllAction'
5 |
6 | describe('', () => {
7 | describe('Snapshot', () => {
8 | it('does not render with no props', () => {
9 | snapshotTest()
10 | })
11 | it('shows both select all and deselect', () => {
12 | const props = {
13 | selectAll: jest.fn(),
14 | count: 5,
15 | numSelected: 3,
16 | itemCount: 3,
17 | deselectAll: jest.fn(),
18 | }
19 | snapshotTest()
20 | })
21 | it('shows deselect all when some of the items are selected', () => {
22 | const props = {
23 | count: 5,
24 | numSelected: 2,
25 | itemCount: 3,
26 | deselectAll: jest.fn(),
27 | }
28 | snapshotTest()
29 | })
30 | it('shows deselect all when all the items are selected', () => {
31 | const props = {
32 | count: 5,
33 | numSelected: 5,
34 | itemCount: 3,
35 | deselectAll: jest.fn(),
36 | }
37 | snapshotTest()
38 | })
39 | it('shows global select all when selected all on page', () => {
40 | const props = {
41 | selectAll: jest.fn(),
42 | count: 5,
43 | numSelected: 3,
44 | itemCount: 3,
45 | itemPluralName: 'kitties',
46 | deselectAll: jest.fn(),
47 | }
48 | snapshotTest()
49 | })
50 | })
51 | describe('Functions', () => {
52 | let instance
53 | const baseProps = {}
54 | const mockEvent = {}
55 | beforeEach(() => {
56 | mockEvent['preventDefault'] = jasmine.createSpy()
57 | baseProps['selectAll'] = jasmine.createSpy()
58 | baseProps['deselectAll'] = jasmine.createSpy()
59 | instance = shallow().instance()
60 | })
61 | it('selects all', () => {
62 | instance.handleSelectAllClick(mockEvent)
63 | expect(mockEvent.preventDefault).toHaveBeenCalled()
64 | expect(baseProps.selectAll).toHaveBeenCalled()
65 | })
66 | it('deselects all', () => {
67 | instance.handleDeselectAllClick(mockEvent)
68 | expect(mockEvent.preventDefault).toHaveBeenCalled()
69 | expect(baseProps.deselectAll).toHaveBeenCalled()
70 | })
71 | })
72 | })
73 |
--------------------------------------------------------------------------------
/src/actions-filters/__tests__/SelectFilters.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { snapshotTest } from 'utils/tests'
3 | import SelectFilters from '../SelectFilters'
4 |
5 | describe('', () => {
6 | describe('Snapshot', () => {
7 | const defaultProps = {
8 | filters: [
9 | {key: 'a', Renderer: () => 'Apple', props: {}, active: true, value: 'green'},
10 | {key: 'c', Renderer: () => 'Cat', props: {}},
11 | ],
12 | addFilter: jest.fn(),
13 | }
14 | afterEach(() => {
15 | defaultProps.addFilter.mockClear()
16 | })
17 |
18 | it('renders correctly', () => {
19 | snapshotTest()
20 | })
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/src/actions-filters/__tests__/__snapshots__/Favourites.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Favourites Snapshot renders correctly 1`] = `
4 |
7 |
13 |
16 |
26 |
40 |
43 |
46 |
63 |
64 |
65 |
66 | `;
67 |
--------------------------------------------------------------------------------
/src/actions-filters/__tests__/__snapshots__/FiltersContainer.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` Snapshot renders correctly 1`] = `
4 |
7 |
8 | a
9 |
10 |
11 | `;
12 |
--------------------------------------------------------------------------------
/src/actions-filters/__tests__/__snapshots__/OptionalField.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`OptionalField Snapshot renders default 1`] = `
4 |
11 | `;
12 |
13 | exports[`OptionalField Snapshot renders enabled 1`] = `
14 |
21 | `;
22 |
--------------------------------------------------------------------------------
/src/actions-filters/__tests__/__snapshots__/OptionalFields.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` Snapshot renders correctly 1`] = `
4 |
7 |
12 |
15 |
24 |
33 |
34 |
35 | `;
36 |
37 | exports[` Snapshot renders correctly 2`] = `
38 |
41 |
46 |
49 |
58 |
67 |
68 |
69 | `;
70 |
--------------------------------------------------------------------------------
/src/actions-filters/__tests__/__snapshots__/SelectAllAction.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` Snapshot does not render with no props 1`] = `null`;
4 |
5 | exports[` Snapshot shows both select all and deselect 1`] = `
6 |
26 | `;
27 |
28 | exports[` Snapshot shows deselect all when all the items are selected 1`] = `
29 |
40 | `;
41 |
42 | exports[` Snapshot shows deselect all when some of the items are selected 1`] = `
43 |
54 | `;
55 |
56 | exports[` Snapshot shows global select all when selected all on page 1`] = `
57 |
78 | `;
79 |
--------------------------------------------------------------------------------
/src/actions-filters/__tests__/__snapshots__/SelectFilters.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` Snapshot renders correctly 1`] = `
4 |
31 | `;
32 |
--------------------------------------------------------------------------------
/src/data-renderers/HeaderField.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | /**
5 | * Represents the text and sorting direction arrow of a TableHeader
6 | * one or more of these may exist within a single TableHeader
7 | */
8 | export default class HeaderField extends React.Component {
9 | static propTypes = {
10 | /** the sorting currently used for the data in the */
11 | activeSort: PropTypes.oneOf([true, false, null]),
12 | /** when a sortkey is passed, the field can be sorted by this key */
13 | sortKey: PropTypes.string,
14 | /** callback function passed down to determine the sorting of data loaded */
15 | updateSorting: PropTypes.func,
16 | /** class names that can be used for additional styling of the component */
17 | className: PropTypes.string,
18 | /** the text displayed within the component */
19 | header: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
20 | /** Icon to render to sort ascending */
21 | SortAscIcon: PropTypes.element,
22 | /** Icon to render to sort descending */
23 | SortDescIcon: PropTypes.element,
24 | /** Icon to render if unsorted column */
25 | UnsortedIcon: PropTypes.element,
26 | }
27 |
28 | static defaultProps = {
29 | activeSort: null,
30 | sortKey: null,
31 | SortAscIcon: ̭,
32 | SortDescIcon: ̬,
33 | UnsortedIcon: null,
34 | header: '',
35 | }
36 |
37 | /**
38 | * Determines whether to sort the particular column (Field) in
39 | * ascending or descending order, passing the sorting key back
40 | * to the objectlist
41 | *
42 | * @param {MouseEvent} event - event triggered from an onClick
43 | */
44 | handleClick = (event) => {
45 | if (this.props.sortKey) {
46 | if (this.props.activeSort) {
47 | this.props.updateSorting(this.props.sortKey, false)
48 | } else {
49 | this.props.updateSorting(this.props.sortKey, true)
50 | }
51 | }
52 | }
53 |
54 | _renderSortIcon = () => {
55 | const {SortAscIcon, SortDescIcon, UnsortedIcon, activeSort} = this.props
56 | switch (activeSort) {
57 | case true:
58 | return (
59 |
60 | {SortAscIcon}
61 |
62 | )
63 | case false:
64 | return (
65 |
66 | {SortDescIcon}
67 |
68 | )
69 | default:
70 | return (
71 |
72 | {UnsortedIcon}
73 |
74 | )
75 | }
76 | }
77 |
78 | _renderHeader = () => {
79 | switch (typeof this.props.header) {
80 | case 'function':
81 | return
82 | case 'string':
83 | return this.props.header
84 | default:
85 | return ''
86 | }
87 | }
88 |
89 | render() {
90 | if (this.props.sortKey !== null) {
91 | return (
92 |
97 | {this._renderHeader()}{this._renderSortIcon()}
98 |
99 | )
100 | } else {
101 | return {this._renderHeader()}
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/data-renderers/List.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import ClassNames from 'classnames'
4 |
5 | import ListCard from './ListCard'
6 | import Overlay from './Overlay'
7 | import { getVisibleColumns, handleRowClick } from '../utils/functions'
8 | import { STATUS_TYPE, STATUS_CHOICES, COLUMN_TYPE } from '../utils/proptypes'
9 |
10 | export default class ListRenderer extends Component {
11 | static propTypes = {
12 | columns: PropTypes.arrayOf(PropTypes.oneOfType([COLUMN_TYPE, PropTypes.arrayOf(COLUMN_TYPE)])),
13 | data: PropTypes.array,
14 | meta: PropTypes.object,
15 | // Custom component to be passed to render the list items
16 | Renderer: PropTypes.func,
17 | // Extra classes for the Renderer component to be rendered with
18 | extraClasses: PropTypes.string,
19 | /** loading status used if data is loaded asynchronously */
20 | status: STATUS_TYPE,
21 | /** Function called when table row is clicked */
22 | itemOnClick: PropTypes.func,
23 | }
24 |
25 | static defaultProps = {
26 | meta: {
27 | extraColumns: [],
28 | sortKeys: [],
29 | },
30 | columns: [],
31 | data: [],
32 | Renderer: ListCard,
33 | status: STATUS_CHOICES.done,
34 | }
35 |
36 | constructor(props) {
37 | super(props)
38 | this.state = {
39 | columns: getVisibleColumns(this.props.columns, this.props.meta.extraColumns),
40 | }
41 | }
42 |
43 | componentWillReceiveProps(nextProps) {
44 | this.setState(() => ({columns: getVisibleColumns(nextProps.columns, nextProps.meta.extraColumns)}))
45 | }
46 |
47 | renderListRows = () => {
48 | const {Renderer, data, itemOnClick} = this.props
49 | return data.map((row, idx) => {
50 | return (
51 | handleRowClick(event, row, itemOnClick) : null}
56 | />
57 | )
58 | })
59 | }
60 |
61 | render() {
62 | return (
63 |
64 |
65 |
66 | {this.renderListRows()}
67 |
68 |
69 | )
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/data-renderers/List.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 | import { withInfo } from '@storybook/addon-info'
4 | import { action } from '@storybook/addon-actions'
5 |
6 | import List from './List'
7 |
8 | const data = [
9 | {
10 | 'key': '1',
11 | 'user': 'Jane Doe',
12 | 'header': 'User information',
13 | 'item': 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
14 | },
15 | {
16 | 'key': '2',
17 | 'user': 'Random Account',
18 | 'header': 'User information',
19 | 'item': 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
20 | },
21 | {
22 | 'key': '3',
23 | 'user': 'James Smith',
24 | 'header': 'User information',
25 | 'item': 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
26 | },
27 | ]
28 |
29 | const customRenderer = (props) => {
30 | const paleGrey = '#B8BDBF'
31 | return (
32 |
33 |
34 | View
35 |
36 | {props.data.header}
37 | {props.data.user}
38 |
39 | {props.data.item !== null ? props.data.item : 'N/A'}
40 |
41 |
42 | )
43 | }
44 |
45 | storiesOf('object-list/Data Renderers/List Renderer', module)
46 | .addDecorator((story, context) => withInfo(
47 | 'Component used for displaying list items'
48 | )(story)(context))
49 | .add('default Renderer', () => {
50 | const columns = [
51 | {
52 | dataKey: '1',
53 | header: 'Item 1',
54 | sortable: false,
55 | optional: false,
56 | },
57 | {
58 | dataKey: '2',
59 | header: 'Item 2',
60 | sortable: false,
61 | optional: false,
62 | },
63 | {
64 | dataKey: '3',
65 | header: 'Item 3',
66 | sortable: false,
67 | optional: true,
68 | },
69 | ]
70 |
71 | const data = [
72 | {
73 | '1': 'Lorem Ipsum is simply dummy text',
74 | '2': 'Lorem Ipsum is simply dummy text',
75 | '3': 'Should not display',
76 | url: 'https://uptickhq.com',
77 | },
78 | {
79 | '1': 'Lorem Ipsum is simply dummy text (card 2)',
80 | '2': 'Lorem Ipsum is simply dummy text (card 2)',
81 | '3': 'Should not display',
82 | },
83 | ]
84 |
85 | return
86 | })
87 | .add('single item', () => {
88 | const singleItem = data.slice(0, 1)
89 | return
90 | })
91 | .add('loading', () => {
92 | return
93 | })
94 | .add('multiple items', () => {
95 | return
96 | })
97 |
--------------------------------------------------------------------------------
/src/data-renderers/ListCard.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { getValueFromAccessor } from './utils'
4 |
5 | /**
6 | * The component that is to be displayed within ListRenderer
7 | * if a custom Renderer is not provided
8 | */
9 | export default class ListCard extends Component {
10 | static propTypes = {
11 | columns: PropTypes.array,
12 | data: PropTypes.object,
13 | onClick: PropTypes.func,
14 | }
15 |
16 | static defaultProps = {
17 | columns: [],
18 | data: {},
19 | }
20 |
21 | renderItemRows = () => {
22 | const {columns, data} = this.props
23 | return columns.map((column, idx) => {
24 | const col = Array.isArray(column) ? column : [column]
25 | return (
26 |
27 |
{col.map(c => c.header).join(' ')}:
28 |
{col.map(c => getValueFromAccessor(data, c.dataKey)).join(' ')}
29 |
30 | )
31 | })
32 | }
33 |
34 | render() {
35 | const { onClick } = this.props
36 | const cardClasses = ['objectlist-list__item']
37 | if (onClick) cardClasses.push('objectlist-list__item--clickable')
38 | return (
39 |
40 | {this.props.data.url &&
41 | View
42 | }
43 | {this.renderItemRows()}
44 |
45 | )
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/data-renderers/Overlay.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import ClassNames from 'classnames'
3 | import { STATUS_TYPE, STATUS_CHOICES } from '../utils/proptypes'
4 |
5 | export default class Overlay extends Component {
6 | static propTypes = {
7 | /** loading status used if data is loaded asynchronously */
8 | status: STATUS_TYPE,
9 | }
10 |
11 | static defaultProps = {
12 | status: 'done',
13 | }
14 |
15 | render() {
16 | return (
17 |
20 | )
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/data-renderers/WidthHandle.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ClassNames from 'classnames'
3 | import PropTypes from 'prop-types'
4 |
5 | /**
6 | * An element inside TableHeader used to drag and resize
7 | * a column's width
8 | */
9 | export default class WidthHandle extends React.Component {
10 | static propTypes = {
11 | /** callback function when currently __dragging__ the handle */
12 | onChange: PropTypes.func,
13 | /** callback function when finished __dragging__ the handle to save selected width */
14 | onSave: PropTypes.func,
15 | }
16 |
17 | state = {
18 | enabled: false,
19 | x: null,
20 | }
21 |
22 | componentDidMount() {
23 | document.addEventListener('mousemove', this.handleGlobalMouseMove)
24 | document.addEventListener('mouseup', this.handleGlobalMouseUp)
25 | }
26 |
27 | componentWillUnmount() {
28 | document.removeEventListener('mousemove', this.handleGlobalMouseMove)
29 | document.removeEventListener('mouseup', this.handleGlobalMouseUp)
30 | }
31 |
32 | /**
33 | * Tracks the distance dragged left or right if currently dragging
34 | * the handle otherwise do nothing
35 | *
36 | * @param {MouseEvent} event - used to calculate the dx mouse movement
37 | */
38 | handleGlobalMouseMove = (event) => {
39 | if (!this.state.enabled) {
40 | return
41 | }
42 | if (this.state.x) { this.props.onChange(event.clientX - this.state.x) }
43 | this.setState({
44 | x: event.clientX,
45 | })
46 | }
47 |
48 | /**
49 | * Stop tracking changes and trigger the callback
50 | * function to save the width of the column
51 | * does nothing when the handle is not enabled
52 | *
53 | * @param {MouseEvent} event
54 | */
55 | handleGlobalMouseUp = (event) => {
56 | if (!this.state.enabled) {
57 | return
58 | }
59 | this.setDisabled()
60 | this.props.onSave()
61 | }
62 |
63 | /**
64 | * Set the handle to enabled ie. the column is being resized
65 | */
66 | setEnabled = () => {
67 | this.setState({
68 | enabled: true,
69 | })
70 | }
71 |
72 | /**
73 | * Set the handle to disabled. ie. the column is no longer being resized
74 | */
75 | setDisabled() {
76 | this.setState({
77 | enabled: false,
78 | x: null,
79 | })
80 | }
81 |
82 | /**
83 | * Change hover state to add hover styles for visual feedback
84 | */
85 | handleMouseOver = () => {
86 | this.setState(state => {
87 | state.hover = true
88 | return state
89 | })
90 | }
91 |
92 | /**
93 | * Revert to default styles when not hovering
94 | */
95 | handleMouseOut = () => {
96 | this.setState(state => {
97 | state.hover = false
98 | return state
99 | })
100 | }
101 |
102 | setWidth(width) {
103 | this.props.onChange(width)
104 | }
105 |
106 | render() {
107 | return (
108 |
114 | )
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/data-renderers/__tests__/HeaderField.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { shallow } from 'enzyme'
3 | import { snapshotTest } from 'utils/tests'
4 | import HeaderField from '../HeaderField'
5 |
6 | describe('HeaderField', () => {
7 | describe('Snapshots', () => {
8 | const props = {
9 | dataKey: 'header_sort_key',
10 | header: 'Header Text',
11 | }
12 | it('renders sortable', () => {
13 | snapshotTest()
14 | })
15 | it('renders not sortable', () => {
16 | snapshotTest()
17 | })
18 | it('renders sorted asc', () => {
19 | snapshotTest()
20 | })
21 | it('renders sorted desc', () => {
22 | snapshotTest()
23 | })
24 | it('renders with fa-sort icon', () => {
25 | snapshotTest()
26 | })
27 | it('renders with empty header', () => {
28 | snapshotTest()
29 | })
30 | })
31 | describe('Functions', () => {
32 | describe('handles click', () => {
33 | const baseProps = {
34 | dataKey: 'something',
35 | activeSort: true,
36 | updateSorting: jest.fn(),
37 | header: 'Thing',
38 | sortKey: '🎂',
39 | }
40 | beforeEach(() => {
41 | spyOn(baseProps, 'updateSorting')
42 | })
43 | it('has no sort key', () => {
44 | const props = {
45 | ...baseProps,
46 | sortKey: null,
47 | }
48 | const instance = shallow().instance()
49 | instance.handleClick()
50 | expect(baseProps.updateSorting).not.toHaveBeenCalled()
51 | })
52 | it('is not sorted', () => {
53 | const instance = shallow().instance()
54 | instance.handleClick()
55 | expect(baseProps.updateSorting).toHaveBeenCalledWith(baseProps.sortKey, true)
56 | })
57 | it('is sorted', () => {
58 | const instance = shallow().instance()
59 | instance.handleClick()
60 | expect(baseProps.updateSorting).toHaveBeenCalledWith(baseProps.sortKey, false)
61 | })
62 | })
63 | describe('renders header', () => {
64 | const baseProps = {
65 | dataKey: 'something',
66 | activeSort: true,
67 | updateSorting: jest.fn(),
68 | }
69 | it('is a function', () => {
70 | const headerFn = (props) => Hello
71 | const instance = shallow().instance()
72 | const header = instance._renderHeader()
73 | expect(header).toEqual(jasmine.any(Object))
74 | snapshotTest(header)
75 | })
76 | it('is a string', () => {
77 | const headerText = 'Yellow Brick Road'
78 | const instance = shallow().instance()
79 | expect(instance._renderHeader()).toBe(headerText)
80 | })
81 | it('is a string when nothing is passed in', () => {
82 | const instance = shallow().instance()
83 | expect(instance._renderHeader()).toBe('')
84 | })
85 | it('is something else', () => {
86 | spyOn(console, 'error')
87 | const headerThing = 99
88 | const instance = shallow().instance()
89 | expect(instance._renderHeader()).toBe('')
90 | expect(console.error).toHaveBeenCalled()
91 | expect(console.error.calls.mostRecent().args[0]).toContain('Failed prop type: Invalid prop `header` supplied to `HeaderField`.')
92 | })
93 | })
94 | })
95 | })
96 |
--------------------------------------------------------------------------------
/src/data-renderers/__tests__/List.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { shallow } from 'enzyme'
3 | import { snapshotTest } from 'utils/tests'
4 | import List from '../List'
5 |
6 | jest.mock('../../utils/functions', () => ({
7 | getVisibleColumns: (a, b) => [...a, ...b],
8 | setColumnLabels: input => input,
9 | }))
10 | jest.mock('../Overlay', () => 'Overlay')
11 |
12 | const props = {
13 | itemOnClick: jest.fn(),
14 | columns: [
15 | {
16 | dataKey: 'name',
17 | header: 'Name',
18 | item: ({row: {attributes: {name}}}) => name,
19 | optional: false,
20 | sortKey: '🎂',
21 | },
22 | [{
23 | dataKey: 'age',
24 | header: 'Age',
25 | item: ({row: {attributes: {age}}, key}) => ({age}),
26 | optional: false,
27 | sortKey: '🥧',
28 | },
29 | {
30 | dataKey: 'gender',
31 | header: 'Gender',
32 | item: ({row: {attributes: {gender}}}) => gender,
33 | optional: false,
34 | sortKey: '🍩',
35 | }],
36 | ],
37 | data: [
38 | {id: 1, type: 'Person', attributes: {name: 'Sam', age: '25', gender: 'M'}},
39 | {id: 2, type: 'Person', attributes: {name: 'Mary', age: '12', gender: 'F'}},
40 | {id: 3, type: 'Person', attributes: {name: 'Q', age: '3', gender: '?'}},
41 | {id: 4, type: 'Person', attributes: {name: 'Peter', age: '111', gender: 'FM'}},
42 | {id: 5, type: 'Person', attributes: {name: 'Amy', age: '32', gender: 'Confused'}},
43 | ],
44 | }
45 |
46 | describe('List', () => {
47 | describe('Lifecycle', () => {
48 | it('componentWillReceiveProps', () => {
49 | const newProps = {
50 | columns: [{dataKey: 'a'}, {dataKey: 'b'}],
51 | meta: {
52 | extraColumns: [{dataKey: 'c'}, {dataKey: 'd'}],
53 | },
54 | }
55 | const instance = shallow(
)
56 | instance.instance().setState({columns: []})
57 | instance.setProps(newProps)
58 | expect(instance.instance().state.columns).toEqual([...newProps.columns, ...newProps.meta.extraColumns])
59 | })
60 | })
61 | describe('Snapshots', () => {
62 | it('can click on items', () => {
63 | snapshotTest(
)
64 | })
65 | })
66 | })
67 |
--------------------------------------------------------------------------------
/src/data-renderers/__tests__/Overlay.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { snapshotTest } from 'utils/tests'
3 | import Overlay from '../Overlay'
4 |
5 | describe('Overlay', () => {
6 | describe('Snapshots', () => {
7 | it('not loading', () => {
8 | snapshotTest()
9 | })
10 | it('loading', () => {
11 | snapshotTest()
12 | })
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/src/data-renderers/__tests__/TableHeader.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { shallow } from 'enzyme'
3 | import { snapshotTest } from 'utils/tests'
4 | import TableHeader from '../TableHeader'
5 |
6 | jest.mock('../HeaderField', () => 'HeaderField')
7 |
8 | describe('Table Header', () => {
9 | const headerItem = {
10 | dataKey: 'header_sort_key',
11 | header: 'Header Text',
12 | item: jest.fn(),
13 | sortKey: '🍬',
14 | optional: false,
15 | }
16 | describe('Snapshots', () => {
17 | it('renders correctly', () => {
18 | const props = {
19 | headerItems: {...headerItem},
20 | setSort: jest.fn(),
21 | }
22 | snapshotTest()
23 | })
24 | it('renders correctly with multiple header items', () => {
25 | const props = {
26 | headerItems: [{...headerItem}, {...headerItem, header: 'Header Two Text', dataKey: 'header_two_key'}],
27 | setSort: jest.fn(),
28 | }
29 | snapshotTest()
30 | })
31 | })
32 |
33 | describe('Functions', () => {
34 | it('will recieve props as array', () => {
35 | const props = {
36 | headerItems: headerItem,
37 | }
38 | const nextProps = {
39 | headerItems: [{...headerItem, dataKey: 'somethingelse'}],
40 | }
41 | const instance = shallow().instance()
42 | instance.componentWillReceiveProps(nextProps)
43 | expect(instance.state.headerItems).toEqual([{...headerItem, dataKey: 'somethingelse'}])
44 | })
45 | it('will recieve props as non-array', () => {
46 | const props = {
47 | headerItems: headerItem,
48 | }
49 | const nextProps = {
50 | headerItems: {...headerItem, dataKey: 'somethingelse'},
51 | }
52 | const instance = shallow().instance()
53 | instance.componentWillReceiveProps(nextProps)
54 | expect(instance.state.headerItems).toEqual([{...headerItem, dataKey: 'somethingelse'}])
55 | })
56 | it('will recieve same props', () => {
57 | const props = {
58 | headerItems: headerItem,
59 | }
60 | const nextProps = {
61 | headerItems: headerItem,
62 | }
63 | const instance = shallow().instance()
64 | instance.componentWillReceiveProps(nextProps)
65 | expect(instance.state.headerItems).toEqual([headerItem])
66 | })
67 | it('saves width', () => {
68 | const props = {
69 | saveWidth: jasmine.createSpy(),
70 | label: 'something',
71 | }
72 | const instance = shallow().instance()
73 | instance.setState({width: 10})
74 | instance.saveWidth()
75 | expect(props.saveWidth).toHaveBeenCalledWith(props.label, 10)
76 | })
77 | })
78 |
79 | describe('sets width', () => {
80 | let instance
81 | beforeEach(() => {
82 | instance = shallow().instance()
83 | instance.setState({width: 50})
84 | })
85 | it('width > 20', () => {
86 | instance.setWidth(50)
87 | expect(instance.state.width).toBe(100)
88 | })
89 | it('width < 20', () => {
90 | instance.setWidth('-40')
91 | expect(instance.state.width).toBe(50)
92 | })
93 | })
94 | })
95 |
--------------------------------------------------------------------------------
/src/data-renderers/__tests__/__snapshots__/HeaderField.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`HeaderField Functions renders header is a function 1`] = `
4 |
5 | Hello
6 |
7 | `;
8 |
9 | exports[`HeaderField Snapshots renders not sortable 1`] = `
10 |
11 | Header Text
12 |
13 | `;
14 |
15 | exports[`HeaderField Snapshots renders sortable 1`] = `
16 |
21 | Header Text
22 |
25 |
26 |
27 |
28 | `;
29 |
30 | exports[`HeaderField Snapshots renders sorted asc 1`] = `
31 |
36 | Header Text
37 |
40 |
41 | ̭
42 |
43 |
44 | `;
45 |
46 | exports[`HeaderField Snapshots renders sorted desc 1`] = `
47 |
52 | Header Text
53 |
56 |
57 | ̬
58 |
59 |
60 | `;
61 |
62 | exports[`HeaderField Snapshots renders with empty header 1`] = `
63 |
68 |
69 |
72 |
73 |
74 |
75 | `;
76 |
77 | exports[`HeaderField Snapshots renders with fa-sort icon 1`] = `
78 |
83 | Header Text
84 |
87 |
88 |
89 |
90 | `;
91 |
--------------------------------------------------------------------------------
/src/data-renderers/__tests__/__snapshots__/Overlay.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Overlay Snapshots loading 1`] = `
4 |
7 | `;
8 |
9 | exports[`Overlay Snapshots not loading 1`] = `
10 |
13 | `;
14 |
--------------------------------------------------------------------------------
/src/data-renderers/__tests__/__snapshots__/TableHeader.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Table Header Snapshots renders correctly 1`] = `
4 |
10 |
22 | |
23 | `;
24 |
25 | exports[`Table Header Snapshots renders correctly with multiple header items 1`] = `
26 |
32 |
44 |
56 | |
57 | `;
58 |
--------------------------------------------------------------------------------
/src/data-renderers/__tests__/__snapshots__/WidthHandle.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`WidthHandle Snapshots default 1`] = `
4 |
10 | `;
11 |
--------------------------------------------------------------------------------
/src/data-renderers/index.js:
--------------------------------------------------------------------------------
1 | export Table from './Table'
2 | export List from './List'
3 |
--------------------------------------------------------------------------------
/src/data-renderers/utils/__tests__/functions.test.js:
--------------------------------------------------------------------------------
1 | import { getValueFromAccessor, handleRowClick } from '../functions'
2 |
3 | describe('getValueFromAccessor', () => {
4 | it('returns value for a simple key/row', () => {
5 | expect(getValueFromAccessor({a: 'b'}, ['a'])).toBe('b')
6 | })
7 | it('returns value for a nested row/mutiple keys', () => {
8 | expect(getValueFromAccessor({a: {b: 'c'}}, ['a', 'b'])).toBe('c')
9 | })
10 | it('returns value for a nested row/dotted key', () => {
11 | expect(getValueFromAccessor({a: {b: 'c'}}, 'a.b')).toBe('c')
12 | })
13 | it('returns null for incorrect key', () => {
14 | expect(getValueFromAccessor({a: {b: 'c'}}, 'a.c')).toBe(null)
15 | })
16 | })
17 |
18 | describe('handle row click', () => {
19 | const currentTarget = {}
20 | const testTarget = (target, shouldTrigger = false) => {
21 | const itemOnClick = jasmine.createSpy()
22 | const row = {}
23 | const mockEvent = {
24 | currentTarget,
25 | target,
26 | persist: jasmine.createSpy(),
27 | preventDefault: jasmine.createSpy(),
28 | stopPropagation: jasmine.createSpy(),
29 | }
30 | handleRowClick(mockEvent, row, itemOnClick)
31 | expect(mockEvent.persist).toHaveBeenCalled()
32 | if (shouldTrigger) {
33 | expect(mockEvent.preventDefault).toHaveBeenCalled()
34 | expect(mockEvent.stopPropagation).toHaveBeenCalled()
35 | expect(itemOnClick).toHaveBeenCalledWith(row)
36 | } else {
37 | expect(mockEvent.preventDefault).not.toHaveBeenCalled()
38 | expect(mockEvent.stopPropagation).not.toHaveBeenCalled()
39 | expect(itemOnClick).not.toHaveBeenCalled()
40 | }
41 | }
42 | it('handles clicking on row', () => {
43 | const target = {
44 | parentElement: currentTarget,
45 | tagName: 'TD',
46 | }
47 | testTarget(target, true)
48 | })
49 | it('handles clicking on a link in a row', () => {
50 | const target = {
51 | parentElement: currentTarget,
52 | tagName: 'A',
53 | }
54 | testTarget(target)
55 | })
56 | it('handles clicking on a button in a row', () => {
57 | const target = {
58 | parentElement: currentTarget,
59 | tagName: 'BUTTON',
60 | }
61 | testTarget(target)
62 | })
63 | it('handles clicking on an input in a row', () => {
64 | const target = {
65 | parentElement: currentTarget,
66 | tagName: 'INPUT',
67 | }
68 | testTarget(target)
69 | })
70 | it('handles clicking on a textarea in a row', () => {
71 | const target = {
72 | parentElement: currentTarget,
73 | tagName: 'TEXTAREA',
74 | }
75 | testTarget(target)
76 | })
77 | })
78 |
--------------------------------------------------------------------------------
/src/data-renderers/utils/functions.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Gets the value of the cell from the row and keys provided
3 | * @param {Object} row Row supplied
4 | * @param {Array|string} keys List of keys used to access the value
5 | * @return {(Object|string|number)} Value from row, or row itself if no keys
6 | */
7 | const getValueFromAccessor = (row, keys) => {
8 | if (!Array.isArray(keys)) keys = keys.split('.')
9 | let value = row
10 | const cloneKeys = [...keys]
11 | while (value && cloneKeys.length) {
12 | const thisKey = cloneKeys.shift()
13 | if (thisKey in value) {
14 | value = value[thisKey]
15 | } else {
16 | return null
17 | }
18 | }
19 | return value
20 | }
21 |
22 | const handleRowClick = (event, row, itemOnClick) => {
23 | event.persist()
24 | let target = event.target
25 | while (target !== event.currentTarget) {
26 | if (['a', 'button', 'input', 'textarea'].includes(target.tagName.toLowerCase())) {
27 | return
28 | }
29 | target = target.parentElement
30 | }
31 | event.preventDefault()
32 | event.stopPropagation()
33 | itemOnClick(row)
34 | }
35 |
36 | export {
37 | getValueFromAccessor,
38 | handleRowClick,
39 | }
40 |
--------------------------------------------------------------------------------
/src/data-renderers/utils/index.js:
--------------------------------------------------------------------------------
1 | export {
2 | getValueFromAccessor,
3 | handleRowClick,
4 | } from './functions'
5 |
--------------------------------------------------------------------------------
/src/filters/ChoiceFilter.js:
--------------------------------------------------------------------------------
1 | import {Choice} from './types'
2 | import {makeFilter} from './utils'
3 |
4 | /**
5 | * Filter used to select from a list of options
6 | */
7 | const ChoiceFilter = makeFilter(Choice)
8 | ChoiceFilter.defaultProps = {
9 | comparisonOptions: [
10 | {value: 'is', label: 'Is'},
11 | {value: 'is_not', label: 'Is not'},
12 | ],
13 | comparison: 'is',
14 | multi: false,
15 | }
16 |
17 | export default ChoiceFilter
18 |
--------------------------------------------------------------------------------
/src/filters/CurrencyFilter.js:
--------------------------------------------------------------------------------
1 | import {Currency} from './types'
2 | import {makeFilter} from './utils'
3 |
4 | /**
5 | * Filter used to filter by a monetary value
6 | */
7 | const CurrencyFilter = makeFilter(Currency)
8 | CurrencyFilter.defaultProps = {
9 | comparisonOptions: [
10 | {value: 'exact', label: 'Exact'},
11 | {value: 'gt', label: 'Greater than'},
12 | {value: 'lt', label: 'Less than'},
13 | {value: 'gte', label: 'Greater than or equal'},
14 | {value: 'lte', label: 'Less than or equal'},
15 | ],
16 | }
17 |
18 | export default CurrencyFilter
19 |
--------------------------------------------------------------------------------
/src/filters/DateFilter.js:
--------------------------------------------------------------------------------
1 | import {Date} from './types'
2 | import {makeFilter} from './utils'
3 |
4 | const fixedComparison = {value: 'fixed', label: 'Fixed Date'}
5 | const relativeComparison = {value: 'relative', label: 'Relative to Now'}
6 |
7 | /**
8 | * Filter used to select a date
9 | */
10 | const DateFilter = makeFilter(Date)
11 | DateFilter.defaultProps = {
12 | fixedComparison: fixedComparison,
13 | relativeComparison: relativeComparison,
14 | comparisonOptions: [fixedComparison, relativeComparison],
15 | comparison: relativeComparison.value,
16 | }
17 |
18 | export default DateFilter
19 |
--------------------------------------------------------------------------------
/src/filters/DayFilter.js:
--------------------------------------------------------------------------------
1 | import {makeFilter} from './utils'
2 | import {Day} from './types'
3 |
4 | /**
5 | * Filter used to move between days
6 | */
7 | const DayFilter = makeFilter(Day)
8 | DayFilter.defaultProps = {
9 | comparisonOptions: [],
10 | permanent: true,
11 | name: null,
12 | }
13 |
14 | export default DayFilter
15 |
--------------------------------------------------------------------------------
/src/filters/MultiChoiceFilter.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ChoiceFilter from './ChoiceFilter'
3 |
4 | /**
5 | * Variation of ChoiceFilter with multiple options select enabled
6 | */
7 | class MultiChoiceFilter extends React.Component {
8 | static defaultProps = {
9 | ...ChoiceFilter.defaultProps,
10 | multi: true,
11 | options: [],
12 | }
13 | render() {
14 | return ()
15 | }
16 | }
17 | export default MultiChoiceFilter
18 |
--------------------------------------------------------------------------------
/src/filters/NumberSliderFilter.js:
--------------------------------------------------------------------------------
1 | import {NumberSlider} from './types'
2 | import {makeFilter} from './utils'
3 |
4 | /**
5 | * Filter used to select a range of numbers
6 | */
7 | const NumberSliderFilter = makeFilter(NumberSlider)
8 | NumberSliderFilter.defaultProps = {
9 | comparisonOptions: [
10 | {value: 'exact', label: 'Exact'},
11 | {value: 'gt', label: 'Greater than'},
12 | {value: 'lt', label: 'Less than'},
13 | {value: 'gte', label: 'Greater than or equal'},
14 | {value: 'lte', label: 'Less than or equal'},
15 | ],
16 | comparison: 'exact',
17 | }
18 |
19 | export default NumberSliderFilter
20 |
--------------------------------------------------------------------------------
/src/filters/RemoteChoiceFilter.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 |
3 | import {Choice} from './types'
4 | import {makeFilter} from './utils'
5 |
6 | const RemoteChoiceFilter = makeFilter(Choice)
7 | RemoteChoiceFilter.defaultProps = {
8 | ...RemoteChoiceFilter.defaultProps,
9 | remote: true,
10 | autoload: true,
11 | options: undefined,
12 | }
13 | RemoteChoiceFilter.propTypes = {
14 | ...RemoteChoiceFilter.propTypes,
15 | loadOptions: PropTypes.func.isRequired,
16 | }
17 |
18 | export default RemoteChoiceFilter
19 |
--------------------------------------------------------------------------------
/src/filters/RemoteMultiChoiceFilter.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import RemoteChoiceFilter from './RemoteChoiceFilter'
3 |
4 | /**
5 | * A variation on RemoteChoiceFilter with multiple choices enabled
6 | * Allows lookup through the API of choices
7 | */
8 | export default class RemoteMultiChoiceFilter extends React.Component {
9 | static defaultProps = {
10 | ...RemoteMultiChoiceFilter.defaultProps,
11 | multi: true,
12 | comparisonOptions: [
13 | {value: 'is', label: 'Is'},
14 | {value: 'is_not', label: 'Is not'},
15 | ],
16 | }
17 | render() {
18 | return (
19 | )
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/filters/SearchFilter.js:
--------------------------------------------------------------------------------
1 | import {makeFilter} from './utils'
2 | import {Search} from './types'
3 |
4 | /**
5 | * Filter used to search text
6 | */
7 | const SearchFilter = makeFilter(Search)
8 | SearchFilter.defaultProps = {
9 | comparisonOptions: [],
10 | permanent: true,
11 | name: null,
12 | }
13 |
14 | export default SearchFilter
15 |
--------------------------------------------------------------------------------
/src/filters/TextContainsFilter.js:
--------------------------------------------------------------------------------
1 | import {makeFilter} from './utils'
2 | import {Search} from './types'
3 |
4 | /**
5 | * Filter used to search text containing
6 | */
7 | const TextContainsFilter = makeFilter(Search)
8 | TextContainsFilter.defaultProps = {
9 | comparisonOptions: [
10 | {value: 'contains', label: 'Contains'},
11 | {value: 'not_contains', label: 'Does Not Contain'},
12 | ],
13 | comparison: 'contains',
14 | }
15 |
16 | export default TextContainsFilter
17 |
--------------------------------------------------------------------------------
/src/filters/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | Boolean,
3 | Month,
4 | } from './types'
5 | import {makeFilter} from './utils'
6 |
7 | const BooleanFilter = makeFilter(Boolean)
8 | const MonthFilter = makeFilter(Month)
9 |
10 | export { BooleanFilter, MonthFilter }
11 | export CurrencyFilter from './CurrencyFilter'
12 | export ChoiceFilter from './ChoiceFilter'
13 | export DateFilter from './DateFilter'
14 | export DayFilter from './DayFilter'
15 | export MultiChoiceFilter from './MultiChoiceFilter'
16 | export NumberSliderFilter from './NumberSliderFilter'
17 | export SearchFilter from './SearchFilter'
18 | export TextContainsFilter from './TextContainsFilter'
19 |
20 | export RemoteChoiceFilter from './RemoteChoiceFilter'
21 | export RemoteMultiChoiceFilter from './RemoteMultiChoiceFilter'
22 |
--------------------------------------------------------------------------------
/src/filters/types/Boolean.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Choice from './Choice'
4 |
5 | /**
6 | * Filter input used to pass boolean values
7 | */
8 | export default class Boolean extends React.Component {
9 | static propTypes = {
10 | /** Label used for true option */
11 | trueLabel: PropTypes.string,
12 | /** Label used for false option */
13 | falseLabel: PropTypes.string,
14 | /** Function called when value changed */
15 | onChange: PropTypes.func,
16 | /** Current value of filter */
17 | value: PropTypes.string,
18 | /** Object of custom react-select styles */
19 | selectStyles: PropTypes.object,
20 | }
21 |
22 | static defaultProps = {
23 | trueLabel: 'Yes',
24 | falseLabel: 'No',
25 | selectStyles: {},
26 | }
27 |
28 | onValueChange = newValue => {
29 | this.props.onChange(newValue.value)
30 | }
31 |
32 | render() {
33 | const {trueLabel, falseLabel, value, selectStyles} = this.props
34 | const trueOption = {value: 'True', label: trueLabel}
35 | const falseOption = {value: 'False', label: falseLabel}
36 | // TODO: The next line is here to be sure no matter what we pass in we use
37 | // the correct value. However the real issue is probably elsewhere i.e. we
38 | // should pass in the object, not True or False.
39 | const mappedValue = (value === 'True') ? trueOption : ((value === 'False') ? falseOption : null)
40 | return (
41 |
47 | )
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/filters/types/Currency.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | /**
5 | * Filter input used to pass currency values
6 | */
7 | class Currency extends React.Component {
8 | static propTypes = {
9 | /** Current value of filter */
10 | value: PropTypes.string,
11 | /** Currency symbol to be displayed */
12 | currencySymbol: PropTypes.string,
13 | /** Function called when value changed */
14 | onChange: PropTypes.func,
15 | }
16 |
17 | static defaultProps = {
18 | currencySymbol: '$',
19 | }
20 |
21 | /**
22 | * Handles value change and calls onChange with new value
23 | */
24 | handleValueChange = () => {
25 | this.props.onChange(this.refs.input.value)
26 | }
27 |
28 | render() {
29 | const {value, currencySymbol} = this.props
30 | return (
31 |
32 | {currencySymbol}
33 |
40 | .00
41 |
42 | )
43 | }
44 | }
45 |
46 | export default Currency
47 |
--------------------------------------------------------------------------------
/src/filters/types/Date.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { format } from 'date-fns'
4 |
5 | import Select from '../../utils/Select'
6 | import { Date as DateInput } from 'mireco'
7 | import { ISO_8601_DATE_FORMAT } from 'mireco/constants'
8 |
9 | /**
10 | * Filter input used to pass date values either fixed
11 | * or relative to the current date
12 | */
13 | export default class DateComponent extends React.Component {
14 | static propTypes = {
15 | /** Function to be called when value changes */
16 | onChange: PropTypes.func,
17 | /** Current filter value */
18 | value: PropTypes.oneOfType([
19 | PropTypes.string,
20 | PropTypes.shape({value: PropTypes.string, label: PropTypes.string}),
21 | ]),
22 | /** Selected comparison */
23 | comparison: PropTypes.string.isRequired,
24 | /** Comparison used for fixed date */
25 | fixedComparison: PropTypes.shape({
26 | /** Value passed back */
27 | value: PropTypes.any,
28 | /** Value shown to user */
29 | label: PropTypes.string,
30 | }),
31 | /** Valid options for relative dates */
32 | relativeDateOptions: PropTypes.arrayOf(PropTypes.shape({
33 | /** Value passed back */
34 | value: PropTypes.string,
35 | /** Value shown to user */
36 | label: PropTypes.string,
37 | })),
38 | /** Object of custom react-select styles */
39 | selectStyles: PropTypes.object,
40 | }
41 |
42 | static defaultProps = {
43 | relativeDateOptions: [
44 | {value: 'today', label: 'Today'},
45 | {value: 'week_start', label: 'Beginning of this week'},
46 | {value: 'month_start', label: 'Beginning of this month'},
47 | {value: 'year_start', label: 'Beginning of this year'},
48 | ],
49 | selectStyles: {},
50 | }
51 |
52 | /**
53 | * Handles relative date value changes.
54 | * Calls onChange with new value if valid
55 | * @param {Object} newValue Relative date option to change to
56 | */
57 | handleDateRelativeChange = (newValue) => {
58 | if (Array.isArray(newValue)) { return }
59 | this.props.onChange(newValue)
60 | }
61 |
62 | render() {
63 | const { value, relativeDateOptions, comparison, fixedComparison, selectStyles } = this.props
64 | let date
65 | if (value === null) {
66 | date = null
67 | } else if (value instanceof Date && value.isValid()) {
68 | date = format(value, ISO_8601_DATE_FORMAT)
69 | } else if (typeof value === 'string') {
70 | date = value
71 | }
72 | let dateChoice
73 | if (comparison === fixedComparison.value) {
74 | dateChoice = (
75 |
82 | )
83 | } else {
84 | dateChoice = (
85 |
93 | )
94 | }
95 | return dateChoice
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/filters/types/Day.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Moment from 'moment'
4 |
5 | /**
6 | * Filter input used to pass specific dates
7 | */
8 | class Day extends React.Component {
9 | static propTypes = {
10 | /** Format used to send/read the date from the API */
11 | format: PropTypes.string,
12 | /** Function called when value changed */
13 | onChange: PropTypes.func,
14 | /** Current filter value */
15 | value: PropTypes.instanceOf(Moment),
16 | }
17 |
18 | static defaultProps = {
19 | format: 'DD-MM-YYYY',
20 | value: Moment(),
21 | }
22 |
23 | /**
24 | * Handles clicking on previous day button.
25 | * Calls onChange with the previous day.
26 | * @param {MouseEvent} event onClick event
27 | */
28 | handlePreviousClick = (event) => {
29 | event.preventDefault()
30 | const newValue = this.props.value.clone().subtract(1, 'days')
31 | this.props.onChange(newValue)
32 | }
33 |
34 | /**
35 | * Handles clicking on next day button.
36 | * Calls onChange with the next day.
37 | * @param {MouseEvent} event onClick event
38 | */
39 | handleNextClick = (event) => {
40 | event.preventDefault()
41 | const newValue = this.props.value.clone().add(1, 'days')
42 | this.props.onChange(newValue)
43 | }
44 |
45 | render() {
46 | return (
47 |
48 |
51 |
{this.props.value.format('dddd MMMM Do YYYY')}
52 |
55 |
56 | )
57 | }
58 | }
59 |
60 | export default Day
61 |
--------------------------------------------------------------------------------
/src/filters/types/Search.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | /**
5 | * Filter input used to pass text values,
6 | * customised for a single search field
7 | */
8 | class Search extends React.Component {
9 | static propTypes = {
10 | /** Current value of filter */
11 | value: PropTypes.string,
12 | /** Function to call on change */
13 | onChange: PropTypes.func,
14 | /** ms to wait after typing stops to send request */
15 | updateDelay: PropTypes.number,
16 | }
17 |
18 | static defaultProps = {
19 | updateDelay: 400,
20 | value: '',
21 | }
22 |
23 | state = {
24 | currentValue: this.props.value || '',
25 | updateScheduled: null,
26 | }
27 |
28 | componentWillReceiveProps(nextProps, nextState) {
29 | if (nextProps.value !== this.props.value) {
30 | this.setState(() => ({currentValue: nextProps.value}))
31 | }
32 | }
33 |
34 | /**
35 | * Debounce call to get result until user has stopped typing
36 | */
37 | scheduleUpdate() {
38 | if (this.state.updateScheduled) {
39 | clearTimeout(this.state.updateScheduled)
40 | }
41 | this.setState(() => ({
42 | updateScheduled: setTimeout(
43 | // eslint-disable-next-line
44 | () => this.props.onChange(this.state.currentValue), this.props.updateDelay
45 | ),
46 | }))
47 | }
48 |
49 | /**
50 | * Handles value change and calls scheduleUpdate
51 | */
52 | handleValueChange = (event) => {
53 | const newValue = event.target.value
54 | this.setState(() => ({
55 | currentValue: newValue,
56 | }), this.scheduleUpdate())
57 | }
58 |
59 | render() {
60 | return (
61 |
68 | )
69 | }
70 | }
71 |
72 | export default Search
73 |
--------------------------------------------------------------------------------
/src/filters/types/__tests__/Boolean.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { snapshotTest } from 'utils/tests'
3 | import { shallow } from 'enzyme'
4 | import Boolean from '../Boolean'
5 |
6 | jest.mock('../Choice', () => 'Choice')
7 |
8 | describe('Boolean', () => {
9 | const baseProps = {
10 | onChange: jest.fn(),
11 | value: 'True',
12 | }
13 | describe('Snapshots', () => {
14 | it('renders true', () => {
15 | snapshotTest()
16 | })
17 | it('renders false', () => {
18 | snapshotTest()
19 | })
20 | it('has custom labels', () => {
21 | snapshotTest()
22 | })
23 | })
24 | describe('Functions', () => {
25 | let instance
26 | beforeEach(() => {
27 | spyOn(baseProps, 'onChange')
28 | instance = shallow().instance()
29 | })
30 | it('handles value change', () => {
31 | const mockValue = {value: 'False', key: '1'}
32 | instance.onValueChange(mockValue)
33 | expect(baseProps.onChange).lastCalledWith('False')
34 | })
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/src/filters/types/__tests__/Currency.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { shallow } from 'enzyme'
3 | import { snapshotTest } from 'utils/tests'
4 | import Currency from '../Currency'
5 |
6 | describe('Currency', () => {
7 | const baseProps = {
8 | onChange: jest.fn(),
9 | value: '25.08',
10 | }
11 | describe('Snapshots', () => {
12 | it('renders default', () => {
13 | snapshotTest()
14 | })
15 | it('renders with custom currency symbol', () => {
16 | snapshotTest()
17 | })
18 | })
19 | describe('Functions', () => {
20 | it('handles value changing', () => {
21 | spyOn(baseProps, 'onChange')
22 | const instance = shallow().instance()
23 | instance.refs = {
24 | input: {
25 | value: '145.98',
26 | },
27 | }
28 | instance.handleValueChange()
29 | expect(baseProps.onChange).toHaveBeenCalledWith('145.98')
30 | })
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/src/filters/types/__tests__/Date.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { shallow } from 'enzyme'
3 | import { snapshotTest } from 'utils/tests'
4 | import DateComponent from '../Date'
5 |
6 | describe('Date', () => {
7 | const baseProps = {
8 | onChange: jest.fn(),
9 | fixedComparison: {value: 'fixed', label: 'Exact'},
10 | comparison: 'fixed',
11 | }
12 | describe('Snapshots', () => {
13 | it('renders default', () => {
14 | snapshotTest()
15 | })
16 | it('renders with relative comparison', () => {
17 | snapshotTest()
18 | })
19 | it('has custom input format', () => {
20 | snapshotTest()
21 | })
22 | it('has custom relative date options', () => {
23 | snapshotTest()
28 | })
29 | it('has null value', () => {
30 | snapshotTest()
31 | })
32 | })
33 | describe('Functions', () => {
34 | describe('handles fixed date changing', () => {
35 | let instance
36 | beforeEach(() => {
37 | spyOn(baseProps, 'onChange')
38 | instance = shallow().instance()
39 | })
40 | it('handles valid date', () => {
41 | const newValue = Date()
42 | instance.props.onChange(newValue)
43 | expect(baseProps.onChange).toHaveBeenCalledWith(newValue)
44 | })
45 | })
46 | describe('handles relative date changing', () => {
47 | let instance
48 | beforeEach(() => {
49 | spyOn(baseProps, 'onChange')
50 | instance = shallow().instance()
51 | })
52 | it('handles array passed', () => {
53 | const newValue = ['now']
54 | instance.handleDateRelativeChange(newValue)
55 | expect(baseProps.onChange).not.toHaveBeenCalled()
56 | })
57 | it('handles single value', () => {
58 | const newValue = 'now'
59 | instance.handleDateRelativeChange(newValue)
60 | expect(baseProps.onChange).toHaveBeenCalledWith(newValue)
61 | })
62 | })
63 | })
64 | })
65 |
--------------------------------------------------------------------------------
/src/filters/types/__tests__/Day.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Moment from 'moment'
3 | import { shallow } from 'enzyme'
4 | import { snapshotTest } from 'utils/tests'
5 | import Day from '../Day'
6 |
7 | describe('Day', () => {
8 | const baseProps = {
9 | onChange: jest.fn(),
10 | value: Moment(),
11 | }
12 | describe('Snapshots', () => {
13 | it('renders default', () => {
14 | snapshotTest()
15 | })
16 | })
17 | describe('Functions', () => {
18 | let instance
19 | let mockEvent
20 | beforeEach(() => {
21 | spyOn(baseProps, 'onChange')
22 | instance = shallow().instance()
23 | mockEvent = {
24 | preventDefault: jasmine.createSpy(),
25 | }
26 | })
27 | it('handles previous click', () => {
28 | instance.handlePreviousClick(mockEvent)
29 | expect(mockEvent.preventDefault).toHaveBeenCalled()
30 | const expectedValue = instance.props.value.clone().subtract(1, 'days')
31 | expect(baseProps.onChange).toHaveBeenCalledWith(expectedValue)
32 | })
33 | it('handles next click', () => {
34 | instance.handleNextClick(mockEvent)
35 | expect(mockEvent.preventDefault).toHaveBeenCalled()
36 | const expectedValue = instance.props.value.clone().add(1, 'days')
37 | expect(baseProps.onChange).toHaveBeenCalledWith(expectedValue)
38 | })
39 | })
40 | })
41 |
--------------------------------------------------------------------------------
/src/filters/types/__tests__/Month.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Moment from 'moment'
3 | import { shallow } from 'enzyme'
4 | import { snapshotTest } from 'utils/tests'
5 | import Month from '../Month'
6 |
7 | jest.mock('react-month-picker', () => 'MonthPicker')
8 |
9 | describe('Month', () => {
10 | const baseProps = {
11 | onChange: jest.fn(),
12 | value: Moment(),
13 | minYear: 2000,
14 | maxYear: 2050,
15 | }
16 | describe('Snapshots', () => {
17 | it('renders default', () => {
18 | snapshotTest()
19 | })
20 | it('has format', () => {
21 | snapshotTest()
22 | })
23 | it('has minYear', () => {
24 | snapshotTest()
25 | })
26 | it('has maxYear', () => {
27 | snapshotTest()
28 | })
29 | it('has null value', () => {
30 | snapshotTest()
31 | })
32 | it('has undefined value', () => {
33 | snapshotTest()
34 | })
35 | })
36 | describe('Lifecycle', () => {
37 | it('componentWillReceiveProps', () => {
38 | const newProps = {
39 | value: Moment('2018-02-01'),
40 | }
41 | const wrapper = shallow()
42 | wrapper.setProps(newProps)
43 | expect(wrapper.instance().state.textValue).toEqual(newProps.value.format(Month.defaultProps.format))
44 | })
45 | })
46 | describe('Functions', () => {
47 | let instance
48 | beforeEach(() => {
49 | spyOn(baseProps, 'onChange')
50 | instance = shallow().instance()
51 | })
52 | describe('handleTextChange', () => {
53 | it('handles valid month', () => {
54 | instance.refs = {
55 | textInput: {
56 | value: 'Jan 2016',
57 | },
58 | }
59 | const monthValue = Moment(instance.refs.textInput.value, instance.props.format, true) // strict=true
60 | instance.handleTextChange()
61 | expect(instance.state.textValue).toBe(instance.refs.textInput.value)
62 | expect(baseProps.onChange).toHaveBeenCalledWith(monthValue)
63 | })
64 | it('handles invalid month', () => {
65 | instance.refs = {
66 | textInput: {
67 | value: 'Jemima puddle duck',
68 | },
69 | }
70 | instance.handleTextChange()
71 | expect(instance.state.textValue).toBe(instance.refs.textInput.value)
72 | expect(baseProps.onChange).toHaveBeenCalledWith(null)
73 | })
74 | })
75 | it('handles click', () => {
76 | instance = shallow().instance()
77 | instance.refs = {
78 | monthPicker: {
79 | show: jest.fn(),
80 | },
81 | }
82 | spyOn(instance.refs.monthPicker, 'show')
83 | instance.handleClick()
84 | expect(instance.refs.monthPicker.show).toHaveBeenCalled()
85 | })
86 | it('handles value change', () => {
87 | const mockYear = Math.floor(Math.random() * 50) + 2000
88 | const mockMonth = Math.floor(Math.random() * 12)
89 | const date = Moment({year: mockYear, month: mockMonth})
90 | instance.handleValueChange(mockYear, mockMonth + 1)
91 | expect(instance.state.textValue).toBe(date.format(instance.props.format))
92 | const arg = baseProps.onChange.calls.mostRecent().args
93 | expect(arg.toString()).toEqual(date.toString())
94 | })
95 | })
96 | })
97 |
--------------------------------------------------------------------------------
/src/filters/types/__tests__/NumberSlider.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { shallow } from 'enzyme'
3 | import { snapshotTest } from 'utils/tests'
4 | import NumberSlider from '../NumberSlider'
5 |
6 | describe('NumberSlider', () => {
7 | const baseProps = {
8 | onChange: jest.fn(),
9 | value: 50,
10 | precision: 1,
11 | }
12 | describe('Snapshots', () => {
13 | it('renders default', () => {
14 | snapshotTest()
15 | })
16 | it('custom min', () => {
17 | snapshotTest()
18 | })
19 | it('custom max', () => {
20 | snapshotTest()
21 | })
22 | })
23 | describe('Lifecycle', () => {
24 | it('componentWillReceiveProps', () => {
25 | const newProps = {
26 | value: 3128907234589789,
27 | }
28 | const wrapper = shallow()
29 | wrapper.setProps(newProps)
30 | expect(wrapper.instance().state.currentValue).toEqual(newProps.value)
31 | })
32 | })
33 | describe('Functions', () => {
34 | let instance
35 | beforeEach(() => {
36 | spyOn(baseProps, 'onChange')
37 | instance = shallow().instance()
38 | })
39 | it('handles slider value change', () => {
40 | const mockValue = Math.floor(Math.random() * 100) + 1
41 | const mockEvent = {
42 | target: {
43 | value: mockValue,
44 | },
45 | }
46 | instance.handleSliderValueChange(mockEvent)
47 | expect(instance.state.currentValue).toBe(mockValue)
48 | expect(baseProps.onChange).not.toHaveBeenCalled()
49 | })
50 | it('handles value change', () => {
51 | const mockValue = Math.floor(Math.random() * 100) + 1
52 | const mockEvent = {
53 | target: {
54 | value: mockValue,
55 | },
56 | }
57 | instance.handleValueChange(mockEvent)
58 | expect(instance.state.currentValue).toBe(mockValue)
59 | expect(baseProps.onChange).toHaveBeenCalledWith(mockValue.toFixed(1))
60 | })
61 | it('handles slider value finalise', () => {
62 | const mockValue = Math.floor(Math.random() * 100) + 1
63 | const mockEvent = {
64 | target: {
65 | value: mockValue,
66 | },
67 | }
68 | instance.handleSliderValueFinalise(mockEvent)
69 | expect(instance.state.currentValue).toBe(mockValue)
70 | expect(baseProps.onChange).toHaveBeenCalledWith(mockValue.toFixed(1))
71 | })
72 | it('handles logarithmic translation', () => {
73 | const mockValue = Math.floor(Math.random() * 100) + 1
74 | instance = shallow().instance()
75 | const actualValue = instance.getValueFromSlider(mockValue)
76 | expect(instance.getValueForSlider(actualValue)).toBeCloseTo(mockValue, 10)
77 | })
78 | })
79 | })
80 |
--------------------------------------------------------------------------------
/src/filters/types/__tests__/Search.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { shallow } from 'enzyme'
3 | import { snapshotTest } from 'utils/tests'
4 | import Search from '../Search'
5 |
6 | jest.useFakeTimers()
7 |
8 | describe('Search', () => {
9 | const baseProps = {
10 | onChange: jest.fn(),
11 | value: 'Kittens',
12 | updateDelay: 1234,
13 | }
14 | describe('Snapshots', () => {
15 | it('renders default', () => {
16 | snapshotTest()
17 | })
18 | it('has new value', () => {
19 | snapshotTest(, {currentValue: 21})
20 | })
21 | })
22 | describe('Lifecycle', () => {
23 | it('componentWillReceiveProps', () => {
24 | const newProps = {
25 | value: 'Hello my fluffies 🐈🐈',
26 | }
27 | const wrapper = shallow()
28 | wrapper.setProps(newProps)
29 | expect(wrapper.instance().state.currentValue).toEqual(newProps.value)
30 | })
31 | })
32 | describe('Functions', () => {
33 | let instance
34 | beforeEach(() => {
35 | spyOn(baseProps, 'onChange')
36 | instance = shallow().instance()
37 | })
38 | it('handles value change', () => {
39 | const mockValue = 'Some search term'
40 | const mockEvent = {
41 | target: {
42 | value: mockValue,
43 | },
44 | }
45 | spyOn(instance, 'scheduleUpdate')
46 | instance.handleValueChange(mockEvent)
47 | expect(instance.state.currentValue).toBe(mockValue)
48 | expect(instance.scheduleUpdate).toHaveBeenCalled()
49 | expect(baseProps.onChange).not.toHaveBeenCalled()
50 | })
51 | it('schedules update', () => {
52 | instance.setState({currentValue: 'Bob', updateScheduled: null})
53 | instance.scheduleUpdate()
54 | expect(setTimeout).toHaveBeenCalledTimes(2)
55 | expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), baseProps.updateDelay)
56 | expect(typeof instance.state.updateScheduled).toBe('number')
57 | expect(instance.state.updateScheduled % 1).toBe(0)
58 | instance.scheduleUpdate()
59 | expect(clearTimeout).toHaveBeenCalledTimes(1)
60 | instance.setState({currentValue: 'Not Bob'})
61 | jest.runOnlyPendingTimers()
62 | expect(baseProps.onChange).toHaveBeenCalledWith('Not Bob')
63 | })
64 | })
65 | })
66 |
--------------------------------------------------------------------------------
/src/filters/types/__tests__/__snapshots__/Boolean.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Boolean Snapshots has custom labels 1`] = `
4 |
26 | `;
27 |
28 | exports[`Boolean Snapshots renders false 1`] = `
29 |
51 | `;
52 |
53 | exports[`Boolean Snapshots renders true 1`] = `
54 |
76 | `;
77 |
--------------------------------------------------------------------------------
/src/filters/types/__tests__/__snapshots__/Currency.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Currency Snapshots renders default 1`] = `
4 |
7 |
10 | $
11 |
12 |
18 |
21 | .00
22 |
23 |
24 | `;
25 |
26 | exports[`Currency Snapshots renders with custom currency symbol 1`] = `
27 |
30 |
33 | %
34 |
35 |
41 |
44 | .00
45 |
46 |
47 | `;
48 |
--------------------------------------------------------------------------------
/src/filters/types/__tests__/__snapshots__/Day.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Day Snapshots renders default 1`] = `
4 |
7 |
13 |
16 | Saturday July 15th 2017
17 |
18 |
24 |
25 | `;
26 |
--------------------------------------------------------------------------------
/src/filters/types/__tests__/__snapshots__/NumberSlider.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`NumberSlider Snapshots custom max 1`] = `
4 |
7 |
16 |
26 |
27 | `;
28 |
29 | exports[`NumberSlider Snapshots custom min 1`] = `
30 |
33 |
42 |
52 |
53 | `;
54 |
55 | exports[`NumberSlider Snapshots renders default 1`] = `
56 |
59 |
68 |
78 |
79 | `;
80 |
--------------------------------------------------------------------------------
/src/filters/types/__tests__/__snapshots__/Search.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Search Snapshots has new value 1`] = `
4 |
11 | `;
12 |
13 | exports[`Search Snapshots renders default 1`] = `
14 |
21 | `;
22 |
--------------------------------------------------------------------------------
/src/filters/types/index.js:
--------------------------------------------------------------------------------
1 | export Boolean from './Boolean'
2 | export Choice from './Choice'
3 | export Currency from './Currency'
4 | export Date from './Date'
5 | export Day from './Day'
6 | export Month from './Month'
7 | export NumberSlider from './NumberSlider'
8 | export Search from './Search'
9 |
--------------------------------------------------------------------------------
/src/filters/utils/FilterComparison.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Select from '../../utils/Select'
4 |
5 | class FilterComparison extends React.Component {
6 | static propTypes = {
7 | options: PropTypes.arrayOf(PropTypes.shape({
8 | label: PropTypes.string,
9 | value: Select.propTypes.value,
10 | })),
11 | value: Select.propTypes.value,
12 | onChange: PropTypes.func.isRequired,
13 | /** Object of custom react-select styles */
14 | selectStyles: PropTypes.object,
15 | }
16 |
17 | static defaultProps = {
18 | selectStyles: {},
19 | }
20 |
21 | handleChange = newValue => this.props.onChange(newValue.value)
22 |
23 | render() {
24 | const {options, value, selectStyles} = this.props
25 | return (
26 |
27 |
35 | )
36 | }
37 | }
38 |
39 | export default FilterComparison
40 |
--------------------------------------------------------------------------------
/src/filters/utils/FilterLabel.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | /**
5 | * Component to render label for filters
6 | */
7 | class FilterLabel extends React.Component {
8 | static propTypes = {
9 | /** Text to render in the label */
10 | label: PropTypes.string.isRequired,
11 | }
12 | render() {
13 | return (
14 |
15 | )
16 | }
17 | }
18 |
19 | export default FilterLabel
20 |
--------------------------------------------------------------------------------
/src/filters/utils/RemoveFilter.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | /**
5 | * Component to render button used to remove a filter
6 | */
7 | class RemoveFilter extends React.Component {
8 | static propTypes = {
9 | /** Function to be called to remove filter */
10 | onClick: PropTypes.func.isRequired,
11 | /** Icon to be rendered in button */
12 | Icon: PropTypes.element,
13 | }
14 |
15 | static defaultProps = {
16 | Icon: ⊖,
17 | }
18 |
19 | render() {
20 | const {Icon, onClick} = this.props
21 | return (
22 |
29 | )
30 | }
31 | }
32 |
33 | export default RemoveFilter
34 |
--------------------------------------------------------------------------------
/src/filters/utils/__mocks__/FilterComparison.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const MockFilterComparison = props => React.createElement('FilterComparison', props)
4 | const RealFilterComparison = require.requireActual('../FilterComparison').default
5 | /* eslint-disable-next-line */
6 | MockFilterComparison.propTypes = RealFilterComparison.propTypes
7 |
8 | export default MockFilterComparison
9 |
--------------------------------------------------------------------------------
/src/filters/utils/__tests__/FilterComparison.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { shallow } from 'enzyme'
3 | import { snapshotTest } from 'utils/tests'
4 | import FilterComparison from '../FilterComparison'
5 |
6 | describe('FilterComparison', () => {
7 | const baseProps = {
8 | onChange: jest.fn(),
9 | value: 'test',
10 | options: [{value: 'test', label: 'Test This'}],
11 | }
12 | describe('Snapshots', () => {
13 | it('renders default', () => {
14 | snapshotTest()
15 | })
16 | })
17 | describe('Functions', () => {
18 | it('handles change', () => {
19 | spyOn(baseProps, 'onChange')
20 | const instance = shallow().instance()
21 | instance.handleChange({value: 42})
22 | expect(baseProps.onChange).toHaveBeenCalledWith(42)
23 | })
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/src/filters/utils/__tests__/FilterLabel.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { snapshotTest } from 'utils/tests'
3 | import FilterLabel from '../FilterLabel'
4 |
5 | describe('FilterLabel', () => {
6 | const baseProps = {
7 | label: 'test',
8 | }
9 | describe('Snapshots', () => {
10 | it('renders default', () => {
11 | snapshotTest()
12 | })
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/src/filters/utils/__tests__/RemoveFilter.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { snapshotTest } from 'utils/tests'
3 | import RemoveFilter from '../RemoveFilter'
4 |
5 | describe('RemoveFilter', () => {
6 | const baseProps = {
7 | onClick: jest.fn(),
8 | }
9 | describe('Snapshots', () => {
10 | it('renders default', () => {
11 | snapshotTest()
12 | })
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/src/filters/utils/__tests__/__snapshots__/FilterComparison.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`FilterComparison Snapshots renders default 1`] = `
4 |
7 |
34 |
35 | `;
36 |
--------------------------------------------------------------------------------
/src/filters/utils/__tests__/__snapshots__/FilterLabel.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`FilterLabel Snapshots renders default 1`] = `
4 |
10 | `;
11 |
--------------------------------------------------------------------------------
/src/filters/utils/__tests__/__snapshots__/RemoveFilter.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`RemoveFilter Snapshots renders default 1`] = `
4 |
11 | `;
12 |
--------------------------------------------------------------------------------
/src/filters/utils/__tests__/__snapshots__/makeFilter.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`makeFilter Snapshots has a name 1`] = `
4 |
7 |
10 |
11 | 44 tests
12 |
13 |
17 |
18 | `;
19 |
20 | exports[`makeFilter Snapshots renders default 1`] = `
21 |
24 |
25 | 44 tests
26 |
27 |
31 |
32 | `;
33 |
34 | exports[`makeFilter Snapshots renders with comparison null 1`] = `
35 |
38 |
60 |
61 | 44 tests
62 |
63 |
67 |
68 | `;
69 |
70 | exports[`makeFilter Snapshots renders with comparison options 1`] = `
71 |
74 |
96 |
97 | 44 tests
98 |
99 |
103 |
104 | `;
105 |
106 | exports[`makeFilter Snapshots renders with permanent true 1`] = `
107 |
110 |
111 | 44 tests
112 |
113 |
114 | `;
115 |
--------------------------------------------------------------------------------
/src/filters/utils/index.js:
--------------------------------------------------------------------------------
1 | export FilterComparison from './FilterComparison'
2 | export FilterLabel from './FilterLabel'
3 | export RemoveFilter from './RemoveFilter'
4 | export makeFilter from './makeFilter'
5 | export * from './utils'
6 |
--------------------------------------------------------------------------------
/src/filters/utils/utils.js:
--------------------------------------------------------------------------------
1 | const possibleQueryStringLookupExpressions = ['exact', 'lt', 'gt', 'gte', 'lte']
2 |
3 | export {
4 | possibleQueryStringLookupExpressions,
5 | }
6 |
--------------------------------------------------------------------------------
/src/icons/FontAwesome.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const FontAwesomeIcons = (majorVersion = 5) => {
4 | switch (majorVersion) {
5 | case 5:
6 | return {
7 | OptionalFields: ,
8 | Favourites: ,
9 | RemoveFilter: ,
10 | RemoveFavourite: ,
11 | DropdownOpen: ,
12 | DropdownClose: ,
13 | SortAsc: ,
14 | SortDesc: ,
15 | Unsorted: ,
16 | Loading: ,
17 | CheckboxChecked: ,
18 | CheckboxUnchecked: ,
19 | }
20 | default:
21 | console.warn(
22 | `Could not find config for version ${majorVersion}`,
23 | 'Accepted versions are: 5',
24 | 'Please make an issue in `react-object-list` to fix this.'
25 | )
26 | }
27 | }
28 |
29 | export default FontAwesomeIcons
30 |
--------------------------------------------------------------------------------
/src/icons/index.js:
--------------------------------------------------------------------------------
1 | export FontAwesome from './FontAwesome'
2 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import ObjectList from './ObjectList'
2 | import ActionsFilterContainer from './actions-filters/ActionsFiltersContainer'
3 | export { COLUMN_TYPE } from './ObjectList'
4 | export { FILTER_BASE_TYPE, META_TYPE, STATUS_TYPE, STATUS_CHOICES, SELECTION_TYPE } from './utils/proptypes'
5 | export default ObjectList
6 |
7 | const FilterContainer = ActionsFilterContainer
8 | export { FilterContainer }
9 |
--------------------------------------------------------------------------------
/src/pagination/Page.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import ClassNames from 'classnames'
4 |
5 | /**
6 | * Component used to display one page number
7 | */
8 | export default class Page extends React.Component {
9 | static propTypes = {
10 | // This page's number
11 | page: PropTypes.number.isRequired,
12 | // True this page is active
13 | active: PropTypes.bool,
14 | // True this page is disabled
15 | disabled: PropTypes.bool,
16 | // Function to call to change page
17 | goToPage: PropTypes.func.isRequired,
18 | // Label to display page with
19 | label: PropTypes.string,
20 | }
21 |
22 | /**
23 | * Handles click event and calls function
24 | * to go to that page
25 | * @param {MouseEvent} event onClick event
26 | */
27 | handlePageClick = event => {
28 | event.preventDefault()
29 | const {goToPage, page} = this.props
30 | goToPage(page)
31 | }
32 |
33 | render() {
34 | const {page, active, disabled, label} = this.props
35 | return (
36 |
44 |
45 | {label || page.toLocaleString()}
46 |
47 |
48 | )
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/pagination/Pagination.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Page from './Page'
4 |
5 | /**
6 | * Component used to display/control pagination
7 | */
8 | export default class Pagination extends React.Component {
9 | static propTypes = {
10 | // Initial page of results
11 | page: PropTypes.number,
12 | // Number of items to display per page
13 | perPage: PropTypes.number,
14 | // Maximum number of pages to display
15 | maxPages: PropTypes.number,
16 | // Total number of results
17 | count: PropTypes.number,
18 | // Function to call to change page
19 | goToPage: PropTypes.func.isRequired,
20 | // True if results are loading
21 | loading: PropTypes.bool,
22 | // Icon to display if results are loading
23 | LoadingIcon: PropTypes.element,
24 | // plural name for items displayed in the list
25 | itemPluralName: PropTypes.string.isRequired,
26 | }
27 |
28 | static defaultProps = {
29 | page: 1,
30 | perPage: 50,
31 | maxPages: 10,
32 | }
33 |
34 | render() {
35 | const {page, maxPages, count, perPage, loading, LoadingIcon, goToPage, itemPluralName} = this.props
36 | const pages = []
37 | let totalPages = 0
38 | const minPage = Math.min(
39 | Math.ceil(page - (maxPages / 2)),
40 | Math.ceil(count / perPage) - maxPages + 1
41 | )
42 | for (let position = 0; position < count; position += perPage) {
43 | const pageIndex = Math.floor(position / perPage) + 1
44 | totalPages++
45 | if (pageIndex < minPage) { continue }
46 | if (pages.length >= maxPages) { continue }
47 | pages.push(
48 |
54 | )
55 | }
56 | if (totalPages === 1) return null
57 | const prevPage = Math.max(Math.min(totalPages, page - 1), 1)
58 | const prevLink = (
59 |
67 | )
68 |
69 | const nextPage = Math.max(Math.min(totalPages, page + 1), 1)
70 | const nextLink = (
71 |
79 | )
80 | let totals
81 | if (!loading) {
82 | totals = (
83 |
84 |
85 | {`Showing page ${page} of ${Math.max(totalPages, 1)} out of ${count} total ${itemPluralName}.`}
86 |
87 |
88 | )
89 | } else {
90 | totals = (
91 |
92 |
93 | {LoadingIcon} Loading {itemPluralName} ...
94 |
95 |
96 | )
97 | }
98 | return (
99 |
100 |
101 | {prevLink}
102 | {pages}
103 | {nextLink}
104 |
105 | {totals}
106 |
107 | )
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/pagination/Pagination.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 | import { withInfo } from '@storybook/addon-info'
4 | import { action } from '@storybook/addon-actions'
5 | import { withKnobs, boolean, number } from '@storybook/addon-knobs/react'
6 |
7 | import Pagination from './Pagination'
8 |
9 | const props = {
10 | page: 3,
11 | perPage: 5,
12 | maxPages: 3,
13 | count: 42,
14 | loading: false,
15 | itemPluralName: 'buildings',
16 | LoadingIcon: ,
17 | }
18 |
19 | storiesOf('object-list/Pagination', module)
20 | .addDecorator((story, context) => withInfo(
21 | 'Pagination used at the bottom of the ObjectList'
22 | )(story)(context))
23 | .addDecorator(withKnobs)
24 | .add('default view', () => (
25 |
32 | ))
33 | .add('loading', () => (
34 |
39 | ))
40 | .add('interactive', () => {
41 | class PaginationWrapper extends React.Component {
42 | state = {...props}
43 | goToPage = (page) => {
44 | this.setState(() => ({page}))
45 | action('Setting page to')(page)
46 | }
47 | render() {
48 | return
49 | }
50 | }
51 | return ()
52 | })
53 |
--------------------------------------------------------------------------------
/src/pagination/__tests__/Page.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { shallow } from 'enzyme'
3 | import {
4 | snapshotTest,
5 | } from 'utils/tests'
6 | import Page from '../Page.js'
7 |
8 | const mockEvent = {
9 | preventDefault: jest.fn(),
10 | }
11 |
12 | describe('Page', () => {
13 | const defaultProps = {
14 | goToPage: jest.fn(),
15 | page: 4,
16 | }
17 | describe('Snapshots', () => {
18 | it('has page number', () => {
19 | snapshotTest()
20 | })
21 | it('has label', () => {
22 | snapshotTest()
23 | })
24 | it('is active', () => {
25 | snapshotTest()
26 | })
27 | it('is disabled', () => {
28 | snapshotTest()
29 | })
30 | it('is active and disabled', () => {
31 | snapshotTest()
32 | })
33 | })
34 | describe('Functions', () => {
35 | it('handles page click', () => {
36 | spyOn(mockEvent, 'preventDefault')
37 | spyOn(defaultProps, 'goToPage')
38 | const instance = shallow().instance()
39 | instance.handlePageClick(mockEvent)
40 | expect(mockEvent.preventDefault).toHaveBeenCalled()
41 | expect(defaultProps.goToPage).toHaveBeenCalledWith(99)
42 | })
43 | })
44 | })
45 |
--------------------------------------------------------------------------------
/src/pagination/__tests__/Pagination.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { snapshotTest } from 'utils/tests'
3 | import Pagination from '../Pagination.js'
4 |
5 | jest.mock('../Page', () => 'Page')
6 |
7 | describe('Pagination', () => {
8 | const dummyLoader = Loading
9 | const defaultProps = {
10 | goToPage: jest.fn(),
11 | page: 1,
12 | perPage: 10,
13 | maxPages: 10,
14 | count: 100,
15 | itemPluralName: 'snakes',
16 | }
17 | describe('Snapshots', () => {
18 | it('has one page', () => {
19 | snapshotTest()
20 | })
21 | it('is on first page', () => {
22 | snapshotTest()
23 | })
24 | it('is on last page', () => {
25 | snapshotTest()
26 | })
27 | it('is loading', () => {
28 | snapshotTest()
29 | })
30 | it('has loading icon', () => {
31 | snapshotTest()
32 | })
33 | it('has more than maxPages', () => {
34 | snapshotTest()
35 | })
36 | })
37 | })
38 |
--------------------------------------------------------------------------------
/src/pagination/__tests__/__snapshots__/Page.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Page Snapshots has label 1`] = `
4 |
8 |
12 | New page please
13 |
14 |
15 | `;
16 |
17 | exports[`Page Snapshots has page number 1`] = `
18 |
22 |
26 | 4
27 |
28 |
29 | `;
30 |
31 | exports[`Page Snapshots is active 1`] = `
32 |
36 |
40 | 4
41 |
42 |
43 | `;
44 |
45 | exports[`Page Snapshots is active and disabled 1`] = `
46 |
50 |
54 | 4
55 |
56 |
57 | `;
58 |
59 | exports[`Page Snapshots is disabled 1`] = `
60 |
64 |
68 | 4
69 |
70 |
71 | `;
72 |
--------------------------------------------------------------------------------
/src/pagination/index.js:
--------------------------------------------------------------------------------
1 | export Pagination from './Pagination'
2 |
--------------------------------------------------------------------------------
/src/readme.md:
--------------------------------------------------------------------------------
1 | # Instructions for porting over object-list from workforce
2 |
3 | ## Snapshot testing
4 | Insetad of
5 | ```javascript
6 | import { snapshotTest } from 'utils/tests'
7 |
8 |
9 | it('snapshots!', () => {
10 | snapshotTest(This works!
)
11 | })
12 | ```
13 | Use
14 | ```javascript
15 | import { snapshotTest } from 'utils/tests'
16 | it('snapshots!', () => {
17 | snapshotTest(This works!
)
18 | })
19 | ```
20 |
21 | ## Generate CSS styling
22 | Recommend to import css styles into the `index.js` file
23 | ```javascript
24 | import './api-list.sass'
25 | ```
26 | This will generate the complete styles in the `dist` folder when running `yarn build`
27 |
--------------------------------------------------------------------------------
/src/resources/colours.sass:
--------------------------------------------------------------------------------
1 | $link-color: blue
2 | $primary-button-color: #337ab7
3 | $global-background-color: white
4 | $button-text-color: rgb(70, 70, 70)
5 | $placeholder-color: #aaa
6 | $dropdown-hover-color: #f5f5f5
7 | $dropdown-selected-color: #f5f5f5
8 | $text-muted-color: #999
9 | $icon-color: black
10 | $slider-shadow-dark-color: #555
11 | $slider-shadow-light-color: #333
12 | $active-colour: #337ab7
13 | $nav-link-colour: #337ab7
14 | $nav-active-colour: #6da5d6
15 | $nav-color-disabled: rgb(164, 164, 164)
16 | $table-row-background: rgba(0, 0, 0, 0.02)
17 | $table-row-active-color: rgba(0, 0 , 0, 0.05)
18 | $border-color: rgb(204, 204, 204)
19 | $table-border-color: rgb(236,238,239)
20 | $border-color-hover: rgb(234, 234, 234)
21 | $error_message_color: rgb(181, 28, 28)
22 | $error_message_background_color: $global-background-color
23 |
--------------------------------------------------------------------------------
/src/resources/dark-table.sass:
--------------------------------------------------------------------------------
1 | $roundedEdgeRadius: 0.25rem
2 | $darkBackgroundColour: #4A4A4A
3 | $componentBackgroundColour: #F2F2F2
4 |
5 | .objectlist-table
6 | box-shadow: 0 0.05rem 0.2rem 0.05rem rgba(0,0,0,0.3)
7 | border-radius: $roundedEdgeRadius
8 | margin-bottom: 4px
9 |
10 | .objectlist-table__th
11 | background-color: $darkBackgroundColour
12 | color: white
13 | border-bottom: none
14 | font-weight: normal
15 |
16 | .objectlist-table__th:first-child
17 | border-top-left-radius: $roundedEdgeRadius
18 |
19 | .objectlist-table__th:last-child
20 | border-top-right-radius: $roundedEdgeRadius
21 |
22 | .objectlist-table__row:nth-of-type(even)
23 | background-color: white
24 |
25 | .objectlist-table__row:nth-of-type(even)
26 | background-color: $componentBackgroundColour
27 |
28 | .objectlist-table__td, .objectlist-table__th
29 | border-top: none
30 | text-align: center
31 |
--------------------------------------------------------------------------------
/src/resources/input-range.sass:
--------------------------------------------------------------------------------
1 | @import 'colours'
2 |
3 | input[type=range]
4 | -webkit-appearance: none
5 |
6 | input[type=range]:focus
7 | outline: none
8 |
9 | input[type=range]::-webkit-slider-runnable-track
10 | height: 8.4px
11 | cursor: pointer
12 | box-shadow: 0px 0px 1px $slider-shadow-dark-color, 0px 0px 0px $slider-shadow-light-color
13 | background: $global-background-color
14 | border-radius: 1.3px
15 | border: 0px solid #010101
16 |
17 | input[type=range]::-webkit-slider-thumb
18 | box-shadow: 0px 0px 1px $slider-shadow-dark-color, 0px 0px 0px $slider-shadow-light-color
19 | border: 1px solid rgba(0, 0, 0, 0)
20 | height: 16px
21 | width: 9px
22 | border-radius: 2px
23 | background: $global-background-color
24 | cursor: pointer
25 | -webkit-appearance: none
26 | margin-top: -3.8px
27 |
28 | input[type=range]:focus::-webkit-slider-runnable-track
29 | background: $global-background-color
30 |
31 | input[type=range]::-moz-range-track
32 | height: 8.4px
33 | cursor: pointer
34 | box-shadow: 0px 0px 1px $slider-shadow-dark-color, 0px 0px 0px $slider-shadow-light-color
35 | background: $global-background-color
36 | border-radius: 1.3px
37 | border: 0px solid #010101
38 |
39 | input[type=range]::-moz-range-thumb
40 | box-shadow: 0px 0px 1px $slider-shadow-dark-color, 0px 0px 0px $slider-shadow-light-color
41 | border: 1px solid rgba(0, 0, 0, 0)
42 | height: 16px
43 | width: 9px
44 | border-radius: 2px
45 | background: #ffffff
46 | cursor: pointer
47 |
48 | input[type=range]::-ms-track
49 | height: 8.4px
50 | cursor: pointer
51 | background: transparent
52 | border-color: transparent
53 | color: transparent
54 |
55 | input[type=range]::-ms-fill-lower
56 | background: $global-background-color
57 | border: 0px solid #010101
58 | border-radius: 2.6px
59 | box-shadow: 0px 0px 1px $slider-shadow-dark-color, 0px 0px 0px $slider-shadow-light-color
60 |
61 | input[type=range]::-ms-fill-upper
62 | background: $global-background-color
63 | border: 0px solid #010101
64 | border-radius: 2.6px
65 | box-shadow: 0px 0px 1px $slider-shadow-dark-color, 0px 0px 0px $slider-shadow-light-color
66 |
67 | input[type=range]::-ms-thumb
68 | box-shadow: 0px 0px 1px $slider-shadow-dark-color, 0px 0px 0px $slider-shadow-light-color
69 | border: 1px solid rgba(0, 0, 0, 0)
70 | height: 16px
71 | width: 9px
72 | border-radius: 2px
73 | background: #ffffff
74 | cursor: pointer
75 | height: 8.4px
76 |
77 | input[type=range]:focus::-ms-fill-lower
78 | background: $global-background-color
79 |
80 | input[type=range]:focus::-ms-fill-upper
81 | background: $global-background-color
82 |
--------------------------------------------------------------------------------
/src/types/AllSelector.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | /**
5 | * Checkbox with a callback function for selecting all elements in an
6 | * api list
7 | */
8 | class AllSelector extends React.Component {
9 | static propTypes = {
10 | /** number of items in the list selected */
11 | numSelected: PropTypes.number,
12 | /** number of items in the list */
13 | total: PropTypes.number,
14 | /** callback function passed down from the objectlist to select all items in a list */
15 | selectAll: PropTypes.func,
16 | /** callback function passed down from the objectlist to deselect all items in a list */
17 | deselectAll: PropTypes.func,
18 | }
19 |
20 | static defaultProps = {
21 | numSelected: 0,
22 | total: 0,
23 | }
24 |
25 | /**
26 | * Callback function executed by the api list
27 | * to select every element in the table
28 | */
29 | handleChange = () => {
30 | const {numSelected, selectAll, deselectAll} = this.props
31 | if (numSelected > 0) {
32 | deselectAll()
33 | } else {
34 | selectAll()
35 | }
36 | }
37 |
38 | __setIndeterminate = (numSelected, total) => {
39 | this.checkbox.indeterminate = (numSelected > 0 && numSelected < total)
40 | }
41 |
42 | componentDidMount() {
43 | const {numSelected, total} = this.props
44 | this.__setIndeterminate(numSelected, total)
45 | }
46 |
47 | componentDidUpdate() {
48 | const {numSelected, total} = this.props
49 | this.__setIndeterminate(numSelected, total)
50 | }
51 |
52 | render() {
53 | const {numSelected, total} = this.props
54 | return (
55 | 0 && numSelected >= total}
59 | onChange={this.handleChange}
60 | ref={elem => { this.checkbox = elem }}
61 | />
62 | )
63 | }
64 | }
65 |
66 | export default AllSelector
67 |
--------------------------------------------------------------------------------
/src/types/BooleanType.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import ClassNames from 'classnames'
4 |
5 | /**
6 | * Display a green checkmark or red cross depending on whether
7 | * true or false is passed to the component
8 | */
9 | class BooleanType extends React.Component {
10 | static propTypes = {
11 | /** boolean condition to visually present on the screen */
12 | condition: PropTypes.bool,
13 | }
14 |
15 | static defaultProps = {
16 | condition: false,
17 | }
18 |
19 | render() {
20 | return (
21 |
22 | )
23 | }
24 | }
25 |
26 | export default BooleanType
27 |
--------------------------------------------------------------------------------
/src/types/Currency.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Empty from './Empty'
4 | import { currency } from './utils'
5 |
6 | /**
7 | * Display a number provided in a currency format
8 | */
9 | class Currency extends React.Component {
10 | static propTypes = {
11 | /** the value as a string or number to format as a currency */
12 | value: PropTypes.oneOfType([
13 | PropTypes.string,
14 | PropTypes.number,
15 | ]),
16 | }
17 |
18 | render() {
19 | if (typeof this.props.value === 'undefined' || this.props.value === null) {
20 | return ()
21 | }
22 | return ({currency(this.props.value)})
23 | }
24 | }
25 |
26 | export default Currency
27 |
--------------------------------------------------------------------------------
/src/types/DatePart.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import DateTime from './DateTime'
3 |
4 | import {
5 | DATE_FORMAT,
6 | } from '../utils'
7 |
8 | /**
9 | * Presents date elements or timestamps in human readable format
10 | */
11 | class DatePart extends React.Component {
12 | static defaultProps = {
13 | outputFormat: DATE_FORMAT,
14 | }
15 | render() {
16 | return (
17 |
20 | )
21 | }
22 | }
23 |
24 | export default DatePart
25 |
--------------------------------------------------------------------------------
/src/types/DateTime.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Moment from 'moment'
3 | import PropTypes from 'prop-types'
4 |
5 | import {
6 | DATETIME_FORMAT,
7 | } from '../utils'
8 |
9 | /**
10 | * Presents timestamps in a human readable format
11 | * where by default the date and time components are shown
12 | */
13 | class DateTime extends React.Component {
14 | static propTypes = {
15 | outputFormat: PropTypes.string,
16 | value: PropTypes.oneOfType([
17 | PropTypes.array, // array of dates, each formatted as a string
18 | PropTypes.object, // an object containing "start" and "end" dates as properties of the object. This is the same as providing two dates in array format
19 | PropTypes.string, // a single date formatted as a string
20 | ]),
21 | empty: PropTypes.string,
22 | /** the separator used when multiple dates are provided */
23 | seperator: PropTypes.string,
24 | }
25 | static defaultProps = {
26 | outputFormat: DATETIME_FORMAT,
27 | empty: '-',
28 | seperator: '-',
29 | value: null,
30 | dateOnly: false,
31 | }
32 | /**
33 | * Return date in specififed format if valid
34 | * @param {Object} date Moment date object to be formatted
35 | * @return {string} Formatted date string or empty
36 | */
37 | formatDateTime(date) {
38 | date = Moment.utc(date)
39 | if (date.isValid()) {
40 | return date.local().format(this.props.outputFormat)
41 | }
42 | return this.props.empty
43 | }
44 | /**
45 | * Return date range with dates formatted as specified
46 | * @param {Array} dates List of dates to be formatted
47 | * @return {string} Formatted date string
48 | */
49 | formatRange(dates) {
50 | let retString = ''
51 | dates.map((date, index) => {
52 | const dateString = `${this.formatDateTime(date)}`
53 | if (retString.search(dateString) === -1) {
54 | if (index > 0) {
55 | retString += ` ${this.props.seperator} `
56 | }
57 | retString += dateString
58 | }
59 | })
60 | return retString
61 | }
62 | render() {
63 | if (this.props.value) {
64 | let inner
65 | switch (typeof this.props.value) {
66 | case 'object':
67 | if (Array.isArray(this.props.value)) {
68 | inner = this.formatRange(this.props.value)
69 | } else if ('start' in this.props.value && 'end' in this.props.value) {
70 | inner = this.formatRange([this.props.value.start, this.props.value.end])
71 | }
72 | break
73 | case 'string':
74 | inner = `${this.formatDateTime(this.props.value)}`
75 | break
76 | }
77 | if (inner) {
78 | return ({inner}
)
79 | }
80 | }
81 | return {this.props.empty}
82 | }
83 | }
84 |
85 | export default DateTime
86 |
--------------------------------------------------------------------------------
/src/types/Empty.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | /**
5 | * Returns a span tag representing an empty element/content on the page
6 | */
7 | class Empty extends React.Component {
8 | static defaultProps = {
9 | emptyText: '-',
10 | }
11 | static propTypes = {
12 | emptyText: PropTypes.string,
13 | }
14 | render() {
15 | return ({this.props.emptyText})
16 | }
17 | }
18 |
19 | export default Empty
20 |
--------------------------------------------------------------------------------
/src/types/Links.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | /**
5 | * Provided links to edit/view the current item (usually inside an objectlist)
6 | * generate the links required
7 | */
8 | class Links extends React.Component {
9 | static propTypes = {
10 | viewUrl: PropTypes.string,
11 | editUrl: PropTypes.string,
12 | extra: PropTypes.arrayOf(PropTypes.shape({
13 | url: PropTypes.string,
14 | text: PropTypes.string,
15 | })),
16 | }
17 |
18 | static defaultProps = {
19 | extra: [],
20 | }
21 |
22 | render() {
23 | const links = []
24 | if (this.props.viewUrl) {
25 | links.push(
26 |
31 | View
32 |
33 | )
34 | }
35 | if (this.props.editUrl) {
36 | if (links.length) {
37 | links.push( )
38 | }
39 | links.push(
40 |
45 | Edit
46 |
47 | )
48 | }
49 | this.props.extra.forEach(({url, text}, i) => {
50 | links.push(
51 |
56 | {text}
57 |
58 | )
59 | })
60 | return (
61 |
64 | {links}
65 |
66 | )
67 | }
68 | }
69 |
70 | export default Links
71 |
--------------------------------------------------------------------------------
/src/types/RelativeDate.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Moment from 'moment'
4 | import DatePart from './DatePart'
5 | import Empty from './Empty'
6 | import {SHORTDATE_FORMAT} from '../utils'
7 |
8 | /**
9 | * Displays a date relative to the current time eg. "4 days ago" or "in 3 days"
10 | * alongside the date passed in
11 | */
12 | class RelativeDate extends React.Component {
13 | static propTypes = {
14 | value: PropTypes.string,
15 | /** display the date passed in as well as the relative time period */
16 | showDate: PropTypes.bool,
17 | }
18 |
19 | static defaultProps = {
20 | showDate: true,
21 | }
22 |
23 | render() {
24 | const date = Moment(this.props.value)
25 | const isValid = date.isValid()
26 | if (isValid) {
27 | return (
28 |
29 |
30 | {Moment(this.props.value).fromNow()}
31 |
32 | {this.props.showDate && (
33 |
34 |
35 |
36 | )}
37 |
38 | )
39 | }
40 | return ()
41 | }
42 | }
43 | export default RelativeDate
44 |
--------------------------------------------------------------------------------
/src/types/RelativeDateTime.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Moment from 'moment'
4 | import DatePart from './DatePart'
5 | import Empty from './Empty'
6 | import {SHORTDATETIME_FORMAT} from '../utils'
7 |
8 | /**
9 | * Displays a datetime relative to the current time eg. "4 days ago" or "in 3 hours"
10 | * alongside the datetime passed in
11 | */
12 | class RelativeDateTime extends React.Component {
13 | static propTypes = {
14 | value: PropTypes.string,
15 | }
16 |
17 | render() {
18 | const date = Moment(this.props.value)
19 | const isValid = date.isValid()
20 | if (isValid) {
21 | return (
22 |
23 |
24 | {Moment(this.props.value).fromNow()}
25 |
26 |
27 |
28 |
29 |
30 | )
31 | }
32 | return ()
33 | }
34 | }
35 | export default RelativeDateTime
36 |
--------------------------------------------------------------------------------
/src/types/Selector.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | /**
5 | * Renders an individual checkbox for a row inside an objectlist
6 | */
7 | class Selector extends React.Component {
8 | static propTypes = {
9 | /** callback function passed down from the objectlist to select/unselect an individual item */
10 | toggleSelect: PropTypes.func,
11 | id: PropTypes.oneOfType([
12 | PropTypes.string,
13 | PropTypes.number,
14 | ]),
15 | selected: PropTypes.bool,
16 | }
17 |
18 | static defaultProps = {
19 | selected: false,
20 | }
21 |
22 | /**
23 | * Calls the callback function with the id for the item
24 | */
25 | handleChange = () => {
26 | this.props.toggleSelect(this.props.id)
27 | }
28 |
29 | render() {
30 | return (
31 |
37 | )
38 | }
39 | }
40 |
41 | export default Selector
42 |
--------------------------------------------------------------------------------
/src/types/TextAttr.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Empty from './Empty'
4 |
5 | /**
6 | * Extracts and displays data from a JSON structure
7 | */
8 | class TextAttr extends React.Component {
9 | static propTypes = {
10 | /** object to display eg. { data: { id: 1234, type: "Property", attributes: {name: "name of property", ref: "refcode"}} } */
11 | value: PropTypes.object,
12 | /** which of the attributes or data values to use as displaytext */
13 | display: PropTypes.string,
14 | identifier: PropTypes.string,
15 | subtext: PropTypes.string,
16 | }
17 | static defaultProps = {
18 | display: '__str__',
19 | }
20 | render() {
21 | if (!(this.props.value && 'data' in this.props.value && this.props.value.data)) {
22 | return
23 | }
24 | const data = this.props.value.data
25 | let text = `${data.type} ${data.id}`
26 | const attrs = data.attributes || {}
27 | if (this.props.display) {
28 | if (this.props.display in attrs) {
29 | text = attrs[this.props.display]
30 | } else if (this.props.display in data) {
31 | text = data[this.props.display]
32 | }
33 | }
34 | let identifier
35 | if (this.props.identifier) {
36 | if (this.props.identifier in attrs) {
37 | identifier = attrs[this.props.identifier]
38 | } else if (this.props.identifier in data) {
39 | identifier = data[this.props.identifier]
40 | }
41 | }
42 | let subtext
43 | if (this.props.subtext) {
44 | if (this.props.subtext in attrs) {
45 | subtext = attrs[this.props.subtext]
46 | } else if (this.props.subtext in data) {
47 | subtext = data[this.props.subtext]
48 | }
49 | }
50 | return (
51 |
52 |
53 | {identifier && {identifier} }
54 | {(text && text.length) ? text : }
55 |
56 | {subtext &&
{subtext}}
57 |
58 | )
59 | }
60 | }
61 |
62 | export default TextAttr
63 |
--------------------------------------------------------------------------------
/src/types/__tests__/AllSelector.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { shallow, mount } from 'enzyme'
3 | import {
4 | snapshotTest,
5 | } from 'utils/tests'
6 | import { AllSelector } from '../'
7 |
8 | describe('Selector type', () => {
9 | beforeAll(() => {
10 | spyOn(AllSelector.prototype, 'componentDidMount')
11 | })
12 | describe('Snapshots', () => {
13 | it('renders default', () => {
14 | snapshotTest()
15 | })
16 | it('renders all selected', () => {
17 | snapshotTest()
18 | })
19 | })
20 | describe('Lifecycle', () => {
21 | it('updates interdeterminateness when props changed', () => {
22 | const wrapper = shallow()
23 | const instance = wrapper.instance()
24 | spyOn(instance, '__setIndeterminate')
25 | wrapper.setProps({numSelected: 4, total: 10})
26 | expect(instance.__setIndeterminate).toHaveBeenCalledWith(4, 10)
27 | })
28 | })
29 | describe('Events', () => {
30 | it('calls select when none selected', () => {
31 | const callback = jest.fn()
32 | const wrapper = mount()
33 | expect(callback).not.toBeCalled()
34 | wrapper.find('input').simulate('change')
35 | expect(callback).toBeCalled()
36 | })
37 | it('calls deselect when some selected', () => {
38 | const callback = jest.fn()
39 | const wrapper = mount()
40 | expect(callback).not.toBeCalled()
41 | wrapper.find('input').simulate('change')
42 | expect(callback).toBeCalled()
43 | })
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/src/types/__tests__/BooleanType.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | snapshotTest,
4 | } from 'utils/tests'
5 | import { BooleanType } from '../'
6 |
7 | describe('BooleanType', () => {
8 | it('snapshot test', () => {
9 | const testValues = [
10 | {},
11 | {condition: true},
12 | {condition: false},
13 | ]
14 | testValues.forEach(testValue => {
15 | snapshotTest()
16 | })
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/src/types/__tests__/Currency.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { snapshotTest } from 'utils/tests'
3 | import { Currency } from '../'
4 |
5 | describe('Currency type', () => {
6 | it('snapshot test', () => {
7 | const testValues = [
8 | {},
9 | {value: '254789043'},
10 | {value: 4672389},
11 | ]
12 | testValues.forEach(testValue => {
13 | snapshotTest()
14 | })
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/src/types/__tests__/DatePart.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { shallow } from 'enzyme'
3 | import { DATE_FORMAT } from '../../utils'
4 | import DatePart from '../DatePart'
5 | import DateTime from '../DateTime'
6 |
7 | describe('DatePart type', () => {
8 | test('contained check', () => {
9 | const datepart = shallow()
10 | expect(datepart.equals()).toBe(true)
11 | })
12 | })
13 |
--------------------------------------------------------------------------------
/src/types/__tests__/DateTime.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Moment from 'moment'
3 | import { shallow } from 'enzyme'
4 | import { snapshotTest } from 'utils/tests'
5 | import { SHORTDATE_FORMAT } from '../../utils'
6 | import { DateTime } from '../'
7 |
8 | describe('DateTime', () => {
9 | describe('Snapshots', () => {
10 | it('parses dates', () => {
11 | const date = Moment.utc('2017-01-01')
12 | const laterDate = Moment.utc('2017-02-01')
13 | const testValues = [
14 | {},
15 | {empty: 'No content'},
16 | {value: date},
17 | {value: date, outputFormat: SHORTDATE_FORMAT},
18 | {value: '2018-02-07', outputFormat: SHORTDATE_FORMAT},
19 | {value: '2018-02-07 14:00:11', outputFormat: SHORTDATE_FORMAT},
20 | {value: [date, laterDate]},
21 | {value: {start: date, end: laterDate}},
22 | {value: {start: date, end: laterDate}, seperator: 'to'},
23 | ]
24 | testValues.forEach(testValue => {
25 | snapshotTest()
26 | })
27 | })
28 | })
29 | describe('Functions', () => {
30 | describe('Format date time', () => {
31 | it('returns empty if date invalid', () => {
32 | spyOn(console, 'warn')
33 | const dateString = 'abcdef'
34 | const instance = shallow().instance()
35 | const result = instance.formatDateTime(dateString)
36 | expect(result).toBe('No date')
37 | expect(console.warn.calls.count()).toEqual(1)
38 | })
39 | })
40 | describe('Format range', () => {
41 | let instance
42 | beforeEach(() => {
43 | instance = shallow().instance()
44 | spyOn(instance, 'formatDateTime').and.callFake(input => Moment(input).format('DD/MM'))
45 | })
46 | it('builds date string', () => {
47 | const dates = [
48 | '2009-01-23',
49 | '2018-03-21',
50 | '2019-10-04',
51 | ]
52 | const result = instance.formatRange(dates)
53 | expect(result).toBe('23/01 to 21/03 to 04/10')
54 | })
55 | it('ignores duplicates', () => {
56 | const dates = [
57 | '2009-01-23',
58 | '2009-01-23',
59 | '2018-03-21',
60 | '2019-10-04',
61 | ]
62 | const result = instance.formatRange(dates)
63 | expect(result).toBe('23/01 to 21/03 to 04/10')
64 | })
65 | })
66 | })
67 | })
68 |
--------------------------------------------------------------------------------
/src/types/__tests__/Empty.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | snapshotTest,
4 | } from 'utils/tests'
5 | import { Empty } from '../'
6 |
7 | describe('Empty type', () => {
8 | it('snapshot test', () => {
9 | const testValues = [
10 | {},
11 | {emptyText: 'test'},
12 | ]
13 | testValues.forEach(testValue => {
14 | snapshotTest()
15 | })
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/src/types/__tests__/Links.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | snapshotTest,
4 | } from 'utils/tests'
5 | import { Links } from '../'
6 |
7 | describe('Links type', () => {
8 | it('snapshot test', () => {
9 | const testValues = [
10 | {},
11 | {viewUrl: '/view/'},
12 | {editUrl: '/edit/'},
13 | {viewUrl: '/view/', editUrl: '/edit/'},
14 | ]
15 | testValues.forEach(testValue => {
16 | snapshotTest()
17 | })
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/src/types/__tests__/RelativeDate.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { snapshotTest } from 'utils/tests'
3 |
4 | import { RelativeDate } from '../'
5 |
6 | jest.mock('../DatePart', () => 'DatePart')
7 | jest.mock('../Empty', () => 'Empty')
8 |
9 | describe('RelativeDate type', () => {
10 | it('snapshot tests', () => {
11 | // Expecting a warning since one of our test values is not a valid date
12 | spyOn(console, 'warn')
13 | const testValues = [
14 | {},
15 | {value: '2018-02-07'},
16 | {value: '2018-02-07 14:00:11', showDate: false},
17 | {value: '2018-02-07 14:00:11', showDate: true},
18 | {value: 'abcdef'},
19 | ]
20 | testValues.forEach(testValue => {
21 | snapshotTest()
22 | })
23 | expect(console.warn.calls.mostRecent().args[0]).toContain('value provided is not in a recognized RFC2822 or ISO format')
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/src/types/__tests__/Selector.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { shallow } from 'enzyme'
3 | import {
4 | snapshotTest,
5 | } from 'utils/tests'
6 | import { Selector } from '../'
7 |
8 | describe('Selector type', () => {
9 | it('snapshot test', () => {
10 | snapshotTest()
11 | snapshotTest()
12 | })
13 | it('makes a call to the callback function when changed', () => {
14 | const callback = jest.fn()
15 | const id = 534
16 | const wrapper = shallow()
17 | expect(callback).not.toBeCalled()
18 | wrapper.find('input').simulate('change')
19 | expect(callback).lastCalledWith(id)
20 | })
21 | })
22 |
--------------------------------------------------------------------------------
/src/types/__tests__/TextAttr.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { snapshotTest } from 'utils/tests'
3 | import { TextAttr } from '../'
4 |
5 | describe('TextAttr type', () => {
6 | it('snapshot tests', () => {
7 | const input = {data: null}
8 | const noAttrs = {data: {id: 1, type: 'TestModel'}}
9 | const allAttrs = {data: {
10 | id: 1,
11 | type: 'TestModel',
12 | attributes: {
13 | __str__: 'default',
14 | name: 'abc',
15 | ref: '123',
16 | detail: 'some important detail',
17 | empty: '',
18 | },
19 | }}
20 | const testValues = [
21 | {},
22 | {value: input},
23 | {value: noAttrs},
24 | {value: allAttrs},
25 | {value: allAttrs, display: null},
26 | {value: allAttrs, display: 'name'},
27 | {value: allAttrs, display: 'name', identifier: 'ref'},
28 | {value: allAttrs, display: 'name', identifier: 'ref', subtext: 'detail'},
29 | {value: allAttrs, display: 'name', identifier: 'wrongref', subtext: 'wrongdetail'},
30 | {value: allAttrs, display: 'type', identifier: 'type', subtext: 'type'},
31 | {value: allAttrs, display: 'empty'},
32 | ]
33 | testValues.forEach(testValue => {
34 | snapshotTest()
35 | })
36 | })
37 | })
38 |
--------------------------------------------------------------------------------
/src/types/__tests__/__snapshots__/AllSelector.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Selector type Snapshots renders all selected 1`] = `
4 |
10 | `;
11 |
12 | exports[`Selector type Snapshots renders default 1`] = `
13 |
19 | `;
20 |
--------------------------------------------------------------------------------
/src/types/__tests__/__snapshots__/BooleanType.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`BooleanType snapshot test 1`] = `
4 |
7 | `;
8 |
9 | exports[`BooleanType snapshot test 2`] = `
10 |
13 | `;
14 |
15 | exports[`BooleanType snapshot test 3`] = `
16 |
19 | `;
20 |
--------------------------------------------------------------------------------
/src/types/__tests__/__snapshots__/Currency.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Currency type snapshot test 1`] = `
4 |
7 | -
8 |
9 | `;
10 |
11 | exports[`Currency type snapshot test 2`] = `
12 |
13 | $254,789,043.00
14 |
15 | `;
16 |
17 | exports[`Currency type snapshot test 3`] = `
18 |
19 | $4,672,389.00
20 |
21 | `;
22 |
--------------------------------------------------------------------------------
/src/types/__tests__/__snapshots__/DateTime.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`DateTime Snapshots parses dates 1`] = `
4 |
7 | -
8 |
9 | `;
10 |
11 | exports[`DateTime Snapshots parses dates 2`] = `
12 |
15 | No content
16 |
17 | `;
18 |
19 | exports[`DateTime Snapshots parses dates 3`] = `
20 |
23 | -
24 |
25 | `;
26 |
27 | exports[`DateTime Snapshots parses dates 4`] = `
28 |
31 | -
32 |
33 | `;
34 |
35 | exports[`DateTime Snapshots parses dates 5`] = `
36 |
37 | 07/02/2018
38 |
39 | `;
40 |
41 | exports[`DateTime Snapshots parses dates 6`] = `
42 |
43 | 08/02/2018
44 |
45 | `;
46 |
47 | exports[`DateTime Snapshots parses dates 7`] = `
48 |
49 | 1st Jan 2017 10:00am - 1st Feb 2017 10:00am
50 |
51 | `;
52 |
53 | exports[`DateTime Snapshots parses dates 8`] = `
54 |
55 | 1st Jan 2017 10:00am - 1st Feb 2017 10:00am
56 |
57 | `;
58 |
59 | exports[`DateTime Snapshots parses dates 9`] = `
60 |
61 | 1st Jan 2017 10:00am to 1st Feb 2017 10:00am
62 |
63 | `;
64 |
--------------------------------------------------------------------------------
/src/types/__tests__/__snapshots__/Empty.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Empty type snapshot test 1`] = `
4 |
7 | -
8 |
9 | `;
10 |
11 | exports[`Empty type snapshot test 2`] = `
12 |
15 | test
16 |
17 | `;
18 |
--------------------------------------------------------------------------------
/src/types/__tests__/__snapshots__/Links.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Links type snapshot test 1`] = `
4 |
7 | `;
8 |
9 | exports[`Links type snapshot test 2`] = `
10 |
20 | `;
21 |
22 | exports[`Links type snapshot test 3`] = `
23 |
33 | `;
34 |
35 | exports[`Links type snapshot test 4`] = `
36 |
55 | `;
56 |
--------------------------------------------------------------------------------
/src/types/__tests__/__snapshots__/RelativeDate.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`RelativeDate type snapshot tests 1`] = `
4 |
5 |
6 | a few seconds ago
7 |
8 |
11 |
15 |
16 |
17 | `;
18 |
19 | exports[`RelativeDate type snapshot tests 2`] = `
20 |
21 |
22 | in 7 months
23 |
24 |
27 |
31 |
32 |
33 | `;
34 |
35 | exports[`RelativeDate type snapshot tests 3`] = `
36 |
37 |
38 | in 7 months
39 |
40 |
41 | `;
42 |
43 | exports[`RelativeDate type snapshot tests 4`] = `
44 |
45 |
46 | in 7 months
47 |
48 |
51 |
55 |
56 |
57 | `;
58 |
59 | exports[`RelativeDate type snapshot tests 5`] = ``;
60 |
--------------------------------------------------------------------------------
/src/types/__tests__/__snapshots__/Selector.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Selector type snapshot test 1`] = `
4 |
10 | `;
11 |
12 | exports[`Selector type snapshot test 2`] = `
13 |
19 | `;
20 |
--------------------------------------------------------------------------------
/src/types/__tests__/__snapshots__/TextAttr.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`TextAttr type snapshot tests 1`] = `
4 |
7 | -
8 |
9 | `;
10 |
11 | exports[`TextAttr type snapshot tests 2`] = `
12 |
15 | -
16 |
17 | `;
18 |
19 | exports[`TextAttr type snapshot tests 3`] = `
20 |
21 |
22 | TestModel 1
23 |
24 |
25 | `;
26 |
27 | exports[`TextAttr type snapshot tests 4`] = `
28 |
29 |
30 | default
31 |
32 |
33 | `;
34 |
35 | exports[`TextAttr type snapshot tests 5`] = `
36 |
37 |
38 | TestModel 1
39 |
40 |
41 | `;
42 |
43 | exports[`TextAttr type snapshot tests 6`] = `
44 |
49 | `;
50 |
51 | exports[`TextAttr type snapshot tests 7`] = `
52 |
53 |
54 |
57 | 123
58 |
59 |
60 | abc
61 |
62 |
63 | `;
64 |
65 | exports[`TextAttr type snapshot tests 8`] = `
66 |
67 |
68 |
71 | 123
72 |
73 |
74 | abc
75 |
76 |
79 | some important detail
80 |
81 |
82 | `;
83 |
84 | exports[`TextAttr type snapshot tests 9`] = `
85 |
90 | `;
91 |
92 | exports[`TextAttr type snapshot tests 10`] = `
93 |
94 |
95 |
98 | TestModel
99 |
100 |
101 | TestModel
102 |
103 |
106 | TestModel
107 |
108 |
109 | `;
110 |
111 | exports[`TextAttr type snapshot tests 11`] = `
112 |
113 |
114 |
117 | -
118 |
119 |
120 |
121 | `;
122 |
--------------------------------------------------------------------------------
/src/types/__tests__/utils.test.js:
--------------------------------------------------------------------------------
1 | import { currency } from '../utils'
2 |
3 | describe('utils', () => {
4 | describe('currency', () => {
5 | it('handles floats with appropriate truncation', () => {
6 | const result = currency(1.2251)
7 | expect(result).toBe('$1.23')
8 | })
9 |
10 | it('handles negative floats', () => {
11 | const result = currency(-12.555)
12 | expect(result).toBe('-$12.56')
13 | })
14 |
15 | it('handles null', () => {
16 | const result = currency(null)
17 | expect(result).toBe('$0.00')
18 | })
19 |
20 | it('handles nan', () => {
21 | const result = currency(NaN)
22 | expect(result).toBe('$0.00')
23 | })
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/src/types/index.js:
--------------------------------------------------------------------------------
1 | export Empty from './Empty'
2 | export Selector from './Selector'
3 | export AllSelector from './AllSelector'
4 | export Links from './Links'
5 | export TextAttr from './TextAttr'
6 | export DatePart from './DatePart'
7 | export DateTime from './DateTime'
8 | export BooleanType from './BooleanType'
9 | export RelativeDate from './RelativeDate'
10 | export RelativeDateTime from './RelativeDateTime'
11 | export Currency from './Currency'
12 |
--------------------------------------------------------------------------------
/src/types/utils.js:
--------------------------------------------------------------------------------
1 | function floatPrecision(floatValue, precision) {
2 | floatValue = parseFloat(floatValue)
3 | if (isNaN(floatValue)) { return parseFloat('0').toFixed(precision) } else {
4 | const power = Math.pow(10, precision)
5 | floatValue = (Math.round(floatValue * power) / power).toFixed(precision)
6 | return floatValue.toString()
7 | }
8 | }
9 |
10 | function commaNumber(numberString) {
11 | const parts = numberString.toString().split('.')
12 | parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
13 | return parts.join('.')
14 | }
15 | /**
16 | * Converts a float to stringed currency representation
17 | * @param {number} floatNumber Amount to convert
18 | * @return {string} Dollar string
19 | */
20 | export function currency(floatNumber) {
21 | return `${floatNumber < 0 ? '-' : ''}$${commaNumber(floatPrecision(Math.abs(floatNumber), 2))}`
22 | }
23 |
--------------------------------------------------------------------------------
/src/utils/ErrorMessage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | class ErrorMessage extends React.Component {
5 | static propTypes = {error: PropTypes.objectOf(Error)}
6 | render() {
7 | const {error} = this.props
8 | if (error && error.message) {
9 | return (
10 |
11 |
12 | {error.message}
13 |
14 | )
15 | } else {
16 | return null
17 | }
18 | }
19 | }
20 |
21 | export default ErrorMessage
22 |
--------------------------------------------------------------------------------
/src/utils/Select.stories.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { storiesOf } from '@storybook/react'
3 | import { withInfo } from '@storybook/addon-info'
4 |
5 | import Select from './Select'
6 |
7 | storiesOf('object-list/Utils/Select', module)
8 | .addDecorator((story, context) => withInfo(
9 | 'Wrapper for react-select to ensure consistency'
10 | )(story)(context))
11 | .add('has custom menu style', () => (
12 |
13 | ))
14 | .add('has custom menu container style', () => (
15 |
16 | ))
17 |
--------------------------------------------------------------------------------
/src/utils/__snapshots__/Select.stories.storyshot:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Storyshots object-list/Utils/Select has custom menu container style 1`] = `
4 |
16 | `;
17 |
18 | exports[`Storyshots object-list/Utils/Select has custom menu style 1`] = `
19 |
31 | `;
32 |
--------------------------------------------------------------------------------
/src/utils/__snapshots__/loading.stories.storyshot:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Storyshots object-list/Utils/Loading Icon loading 1`] = `
4 |
5 |
6 |
7 | red
8 | :
9 |
10 |
11 |
19 |
20 |
21 |
22 |
23 | blue
24 | :
25 |
26 |
27 |
35 |
36 |
37 |
38 |
39 | yellow
40 | :
41 |
42 |
43 |
51 |
52 |
53 |
54 |
55 | green
56 | :
57 |
58 |
59 |
67 |
68 |
69 |
70 |
71 | purple
72 | :
73 |
74 |
75 |
83 |
84 |
85 |
86 |
87 | grey
88 | :
89 |
90 |
91 |
99 |
100 |
101 |
102 |
103 | :
104 |
105 |
106 |
110 |
111 |
112 |
113 | `;
114 |
--------------------------------------------------------------------------------
/src/utils/__tests__/ErrorMessage.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | snapshotTest,
4 | } from 'utils/tests'
5 | import { ErrorMessage } from '../'
6 |
7 | describe('Error Message', () => {
8 | it('default', () => {
9 | snapshotTest()
10 | })
11 | it('has error', () => {
12 | snapshotTest()
13 | })
14 | })
15 |
--------------------------------------------------------------------------------
/src/utils/__tests__/__snapshots__/ErrorMessage.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Error Message default 1`] = `null`;
4 |
5 | exports[`Error Message has error 1`] = `
6 |
9 |
12 |
13 | Testing
14 |
15 |
16 | `;
17 |
--------------------------------------------------------------------------------
/src/utils/__tests__/proptypes.test.js:
--------------------------------------------------------------------------------
1 | import {SELECTION_TYPE} from '../proptypes'
2 |
3 | describe('custom proptypes', () => {
4 | it('correctly detects selection proptype', () => {
5 | const selection = {
6 | 1: true,
7 | 2: true,
8 | 3: false,
9 | }
10 |
11 | expect(SELECTION_TYPE({selection}, 'selection', 'ComponentName')).toBe(undefined)
12 | })
13 | it('handles invalid selection props', () => {
14 | const selection = [1, 2, 3]
15 | expect(SELECTION_TYPE({selection}, 'selection', 'ComponentName')).toEqual(expect.any(Error))
16 | })
17 | it('hanldes a missing prop when required', () => {
18 | expect(SELECTION_TYPE.isRequired({}, 'selection', 'ComponentName')).toEqual(expect.any(Error))
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/src/utils/constants.js:
--------------------------------------------------------------------------------
1 | const DATE_FORMAT = 'Do MMM YYYY'
2 | const DATETIME_FORMAT = `${DATE_FORMAT} h:mma`
3 | const SHORTDATE_FORMAT = 'DD/MM/YYYY'
4 | const SHORTDATETIME_FORMAT = `${SHORTDATE_FORMAT} h:mma`
5 | const API_DATE_FORMAT = 'YYYY-MM-DD'
6 | const allowedLookupExpressions = ['lt', 'gt', 'lte', 'gte']
7 |
8 | export {
9 | DATE_FORMAT,
10 | DATETIME_FORMAT,
11 | SHORTDATE_FORMAT,
12 | SHORTDATETIME_FORMAT,
13 | API_DATE_FORMAT,
14 | allowedLookupExpressions,
15 | }
16 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | export ErrorMessage from './ErrorMessage'
2 |
3 | export {
4 | DATE_FORMAT,
5 | DATETIME_FORMAT,
6 | SHORTDATE_FORMAT,
7 | SHORTDATETIME_FORMAT,
8 | API_DATE_FORMAT,
9 | allowedLookupExpressions,
10 | } from './constants'
11 | export {
12 | COLUMN_TYPE,
13 | COLUMN_BASE_TYPE,
14 | FILTER_BASE_TYPE,
15 | META_TYPE,
16 | STATUS_TYPE,
17 | STATUS_CHOICES,
18 | ALL_SELECTED,
19 | SELECTION_TYPE,
20 | } from './proptypes'
21 | export {
22 | getVisibleColumns,
23 | setColumnLabels,
24 | sortByName,
25 | getLeafColumns,
26 | makeSelectStyles,
27 | } from './functions'
28 |
--------------------------------------------------------------------------------
/src/utils/proptypes.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 |
3 | const STATUS_CHOICES = {
4 | loading: 'loading',
5 | error: 'error',
6 | done: 'done',
7 | }
8 | const STATUS_TYPE = PropTypes.oneOf(Object.values(STATUS_CHOICES))
9 |
10 | const ALL_SELECTED = 'all'
11 | function selectionTypeValidator(isRequired) {
12 | return function(props, propName, componentName) {
13 | const value = props[propName]
14 | if (isRequired && (value === null || value === undefined)) {
15 | return new Error(`Missing prop \`${propName}\`
16 | supplied to \`${componentName}\` expected one of ['${ALL_SELECTED}', obj]`)
17 | }
18 | let validObject = true
19 | if (typeof value === 'object') {
20 | validObject = Object.entries(value).every(
21 | ([key, value]) => (typeof key === 'number' || 'string') && typeof value === 'boolean')
22 | } else {
23 | validObject = false
24 | }
25 |
26 | if (value !== ALL_SELECTED && !validObject) {
27 | return new Error(`Invalid prop \`${propName}\` of \`${value}\`
28 | supplied to \`${componentName}\` expected one of ['${ALL_SELECTED}', obj]`)
29 | }
30 | }
31 | }
32 | const SELECTION_TYPE = selectionTypeValidator(false)
33 | SELECTION_TYPE.isRequired = selectionTypeValidator(true)
34 |
35 | const COLUMN_BASE_TYPE = {
36 | fieldKey: PropTypes.string, // used to uniquely identify this column
37 | dataKey: PropTypes.string, // used to access the data inside the data object
38 | header: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
39 | item: PropTypes.oneOfType([PropTypes.func, PropTypes.element]),
40 | sortKey: PropTypes.string,
41 | optional: PropTypes.bool,
42 | /* string or function of a data row returning a string to add to row classes */
43 | rowClass: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
44 | }
45 |
46 | const COLUMN_TYPE = PropTypes.shape(COLUMN_BASE_TYPE)
47 |
48 | const FILTER_BASE_TYPE = {
49 | /** Name of filter, used to render filter */
50 | name: PropTypes.string,
51 | /** Identifier key for filter */
52 | filterKey: PropTypes.string,
53 | /** If true, indicates the filter cannot be removed */
54 | permanent: PropTypes.bool,
55 | }
56 |
57 | const META_TYPE = PropTypes.shape({
58 | /** the current sortkeys applied using {key: asc_desc_bool } eg. {name: true} for ascending */
59 | sortKeys: PropTypes.arrayOf(PropTypes.object),
60 | /** current page highlighted on the pagination */
61 | currentPage: PropTypes.number,
62 | /** the maximum number of items shown on a page */
63 | perPage: PropTypes.number,
64 | /** the keys of the optional columns that are currently being displayed */
65 | extraColumns: PropTypes.arrayOf(PropTypes.string),
66 | /** the total number of items in the dataset if only displaying a subset */
67 | totalCount: PropTypes.number,
68 | /** the list of items currently selected */
69 | selection: PropTypes.array,
70 | })
71 |
72 | export {
73 | COLUMN_TYPE,
74 | COLUMN_BASE_TYPE,
75 | FILTER_BASE_TYPE,
76 | META_TYPE,
77 | STATUS_TYPE,
78 | STATUS_CHOICES,
79 | SELECTION_TYPE,
80 | ALL_SELECTED,
81 | }
82 |
--------------------------------------------------------------------------------
/utils/tests.js:
--------------------------------------------------------------------------------
1 | import renderer from 'react-test-renderer'
2 |
3 | /**
4 | * Helper function to run a snapshot test on an element
5 | * @param {Object} elem React Component with props i.e.
6 | * @param {Object} [newState=null] (Optional) State to set component to for snapshot
7 | */
8 | export function snapshotTest(elem, newState = null) {
9 | const rendered = renderer.create(elem)
10 | if (newState) {
11 | const instance = rendered.getInstance()
12 | instance.setState({
13 | ...instance.state,
14 | ...newState,
15 | })
16 | }
17 | const tree = rendered.toJSON()
18 | expect(tree).toMatchSnapshot()
19 | }
20 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
4 | const PeerDepsExternalsPlugin = require('peer-deps-externals-webpack-plugin')
5 |
6 | const extractSass = new ExtractTextPlugin({
7 | filename: '[name].[contenthash].css',
8 | })
9 |
10 | module.exports = {
11 | entry: {
12 | main: path.resolve('src', 'index.js'),
13 | filters: path.resolve('src', 'filters', 'index.js'),
14 | renderers: path.resolve('src', 'data-renderers', 'index.js'),
15 | cells: path.resolve('src', 'types', 'index.js'),
16 | utils: path.resolve('src', 'utils', 'index.js'),
17 | icons: path.resolve('src', 'icons', 'index.js'),
18 | },
19 | output: {
20 | path: path.resolve(__dirname),
21 | filename: '[name].js',
22 | libraryTarget: 'umd',
23 | library: 'react-object-list',
24 | },
25 | module: {
26 | rules: [{
27 | test: /\.js$/,
28 | exclude: /node_modules/,
29 | use: {
30 | loader: 'babel-loader',
31 | },
32 | }, {
33 | test: /\.(scss|sass)$/,
34 | exclude: /node_modules/,
35 | use: extractSass.extract({
36 | use: [{
37 | loader: 'css-loader', // translates CSS into CommonJS
38 | options: {
39 | minimize: true,
40 | },
41 | }, {
42 | loader: 'resolve-url-loader', // resolve relative urls inside the css files
43 | }, {
44 | loader: 'sass-loader?sourceMap', // compiles Sass to CSS
45 | }],
46 | }),
47 | }, {
48 | test: /\.(woff2?|eot|ttf|svg|otf)(\?.+)?$/i,
49 | use: [
50 | {
51 | loader: 'url-loader',
52 | options: {
53 | limit: 10000,
54 | name: '[name].[hash].[ext]',
55 | },
56 | },
57 | ],
58 | }],
59 | },
60 | plugins: [
61 | extractSass,
62 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
63 | new PeerDepsExternalsPlugin(),
64 | ],
65 | }
66 |
--------------------------------------------------------------------------------