├── .prettierignore ├── __mocks__ └── copy-to-clipboard.js ├── .babelrc ├── src ├── redux │ ├── actions │ │ ├── textActions.js │ │ ├── customComponentsActions.js │ │ ├── notifierActions.js │ │ └── datatableActions.js │ └── reducers │ │ ├── reducers.js │ │ ├── customComponentsReducer.js │ │ ├── notifierReducer.js │ │ └── textReducer.js ├── components │ ├── DatatableHeader │ │ └── Widgets │ │ │ ├── Transition.js │ │ │ ├── AdditionalIcons.js │ │ │ ├── Filter.js │ │ │ ├── SelectionIcons.js │ │ │ └── Search.js │ ├── DatatableCore │ │ ├── InputTypes │ │ │ ├── BooleanWrapper.js │ │ │ ├── PickersFunction.js │ │ │ ├── SelectWrapper.js │ │ │ ├── DatePickerWrapper.js │ │ │ ├── TimePickerWrapper.js │ │ │ ├── DateTimePickerWrapper.js │ │ │ ├── TextFieldWrapper.js │ │ │ └── CreateInput.js │ │ ├── Header │ │ │ ├── Header.js │ │ │ ├── HeaderColumnsFilterBar.js │ │ │ └── HeaderActionsCell.js │ │ ├── CellTypes.js │ │ └── Body │ │ │ └── BodyCell.js │ ├── MuiTheme.js │ ├── Loader.js │ ├── Notifier.js │ ├── DatatableFooter │ │ └── DatatableFooter.js │ └── DatatableContainer.js ├── moment.config.js └── index.js ├── test ├── enzyme.conf.js ├── reduxTest │ ├── actionsTest │ │ ├── customComponentsActions.test.js │ │ └── notifierActions.test.js │ └── reducersTest │ │ ├── notifierReducer.test.js │ │ └── customComponentsReducer.test.js ├── componentsTest │ ├── DatatableHeaderTest │ │ ├── Widgets │ │ │ ├── Filter.test.js │ │ │ ├── AdditionalIcons.test.js │ │ │ ├── SelectionIcons.test.js │ │ │ └── CreatePreset.test.js │ │ └── DatatableHeader.test.js │ ├── DatatableCoreTest │ │ ├── InputTypesTest │ │ │ ├── BooleanWrapper.test.js │ │ │ ├── CreateInput.test.js │ │ │ ├── SelectWrapper.test.js │ │ │ └── PickersFunction.test.js │ │ └── HeaderTest │ │ │ ├── Header.test.js │ │ │ ├── HeaderColumnsFilterBar.test.js │ │ │ ├── HeaderActionsCell.test.js │ │ │ └── HeaderRow.test.js │ ├── Loader.test.js │ ├── DatatableContainer.test.js │ └── DatatableInitializer.test.js └── index.test.js ├── .npmignore ├── .eslintignore ├── .storybook ├── addons.js └── config.js ├── .prettierrc ├── data ├── minimumOptionsSample.js ├── customDataTypesSample.js ├── simpleOptionsNoDataSample.js ├── storeNoCustomComponentsSample.js ├── simpleOptionsSample.js ├── storeCustomTableBodyRowComponentSample.js ├── storeCustomTableHeaderCellComponentSample.js ├── storeCustomTableBodyCellComponentSample.js ├── storeCustomTableHeaderRowComponentSample.js ├── storyOptionsNoActionSample.js ├── storeSample.js ├── storeSampleWithPages.js ├── mergedPageSample.js ├── customTableHeaderCellSample.js ├── storeNoDataSample.js ├── storeNoRowsDataSample.js ├── customTableHeaderRowSample.js ├── customTableBodyCellSample.js ├── maximumOptionsSample.js ├── mergedSimpleOptionsSample.js ├── mergedSimpleOptionsSampleCustomSize.js ├── textReducer.js ├── mergedSimpleOptionsSampleHeightResize.js ├── mergedSetRowsPerPageSample.js ├── mergedSimpleOptionsSampleWidthResize.js ├── mergedSimpleOptionsSampleWidthHeightResize.js ├── mergedDatableReducerRowsEdited.js ├── customTableBodyRowSample.js ├── defaultOptionsSample.js ├── mergedMinimumOptionsSample.js ├── storyOptionsSample.js ├── storyOptionsSample2.js ├── mergedMaximumOptionsSample.js └── samples.js ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── PULL_REQUEST_TEMPLATE.md ├── webpack.config.js ├── .travis.yml ├── stories ├── Basics │ ├── defaultStory.js │ └── noDataStory.js ├── Override │ ├── customDataTypesStory.js │ ├── customTableBodyCellStory.js │ ├── customTableHeaderRowStory.js │ ├── customTableHeaderCellStory.js │ └── customTableBodyRowStory.js └── index.stories.js ├── .eslintrc ├── LICENSE ├── examples └── override │ ├── headerCell.md │ ├── datatypes.md │ ├── bodyCell.md │ ├── headerRow.md │ └── bodyRow.md └── package.json /.prettierignore: -------------------------------------------------------------------------------- 1 | .storybook/ 2 | node_modules/ 3 | storybook-static/ 4 | index.js -------------------------------------------------------------------------------- /__mocks__/copy-to-clipboard.js: -------------------------------------------------------------------------------- 1 | const copy = jest.fn(); 2 | export default copy; 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": ["@babel/plugin-proposal-class-properties"] 4 | } -------------------------------------------------------------------------------- /src/redux/actions/textActions.js: -------------------------------------------------------------------------------- 1 | const initText = payload => ({ 2 | type: "INIT_TEXT", 3 | payload 4 | }); 5 | 6 | export default initText; 7 | -------------------------------------------------------------------------------- /test/enzyme.conf.js: -------------------------------------------------------------------------------- 1 | import { configure } from "enzyme"; 2 | import Adapter from "enzyme-adapter-react-16"; 3 | 4 | configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | test/ 3 | stories/ 4 | .storybook/ 5 | .eslintignore 6 | .eslintrc 7 | storybook-static/ 8 | __mocks__/ 9 | examples/ 10 | data/ 11 | coverage/ 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | dist/* 3 | example/node_modules/* 4 | *.prettierrc 5 | storybook-static/ 6 | webpack.config.js 7 | index.js 8 | config/* 9 | data/storyOptionsSample.js -------------------------------------------------------------------------------- /src/redux/actions/customComponentsActions.js: -------------------------------------------------------------------------------- 1 | const initializeCustomComponents = payload => ({ 2 | type: "INITIALIZE_CUSTOM_COMPONENTS", 3 | payload 4 | }); 5 | 6 | export default initializeCustomComponents; 7 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import "@storybook/addon-knobs/register"; 2 | import "@storybook/addon-actions/register"; 3 | import "@storybook/addon-links/register"; 4 | import "@storybook/addon-notes/register"; 5 | import "@dump247/storybook-state"; 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "printWidth": 80, 4 | "tabWidth": 2, 5 | "singleQuote": false, 6 | "trailingComma": "none", 7 | "jsxBracketSameLine": false, 8 | "parser": "babel", 9 | "noSemi": true, 10 | "rcVerbose": true 11 | } -------------------------------------------------------------------------------- /src/components/DatatableHeader/Widgets/Transition.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Slide } from "@material-ui/core"; 3 | 4 | const Transition = React.forwardRef(function Transition(props, ref) { 5 | return ; 6 | }); 7 | 8 | export default Transition; 9 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from "@storybook/react"; 2 | 3 | // automatically import all files ending in *.stories.js 4 | const req = require.context("../stories", true, /.stories.js$/); 5 | function loadStories() { 6 | req.keys().forEach(filename => req(filename)); 7 | } 8 | 9 | configure(loadStories, module); 10 | -------------------------------------------------------------------------------- /data/minimumOptionsSample.js: -------------------------------------------------------------------------------- 1 | import { keyColumn, data } from "./optionsObjectSample"; 2 | 3 | const minimumOptionsSample = { 4 | // Only here to avoid error in reducer 5 | dimensions: { 6 | datatable: { 7 | width: "100vw", 8 | height: "100vh" 9 | } 10 | }, 11 | keyColumn, 12 | data 13 | }; 14 | 15 | export default minimumOptionsSample; 16 | -------------------------------------------------------------------------------- /src/redux/reducers/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import customComponentsReducer from "./customComponentsReducer"; 3 | import datatableReducer from "./datatableReducer"; 4 | import notifierReducer from "./notifierReducer"; 5 | import textReducer from "./textReducer"; 6 | 7 | export default combineReducers({ 8 | datatableReducer, 9 | customComponentsReducer, 10 | notifierReducer, 11 | textReducer 12 | }); 13 | -------------------------------------------------------------------------------- /src/moment.config.js: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | 3 | export const locale = 4 | window.navigator.userLanguage || window.navigator.language; 5 | moment.locale(locale); 6 | export const localeData = moment.localeData(); 7 | export const dateFormatUser = localeData.longDateFormat("L"); 8 | export const timeFormatUser = localeData.longDateFormat("LT"); 9 | export const dateTimeFormatUser = localeData.longDateFormat("lll"); 10 | export { moment }; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | /dist 9 | 10 | # example 11 | /example/node_modules 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | storybook-static/ 25 | coverage/ -------------------------------------------------------------------------------- /src/redux/actions/notifierActions.js: -------------------------------------------------------------------------------- 1 | export const enqueueSnackbar = payload => { 2 | const key = payload.options && payload.options.key; 3 | 4 | return { 5 | type: "ENQUEUE_SNACKBAR", 6 | payload: { 7 | ...payload, 8 | key 9 | } 10 | }; 11 | }; 12 | 13 | export const closeSnackbar = payload => ({ 14 | type: "CLOSE_SNACKBAR", 15 | payload 16 | }); 17 | 18 | export const removeSnackbar = payload => ({ 19 | type: "REMOVE_SNACKBAR", 20 | payload 21 | }); 22 | -------------------------------------------------------------------------------- /src/redux/reducers/customComponentsReducer.js: -------------------------------------------------------------------------------- 1 | const defaultState = { 2 | customProps: null, 3 | CustomTableBodyCell: null, 4 | CustomTableBodyRow: null, 5 | CustomTableHeaderCell: null, 6 | CustomTableHeaderRow: null, 7 | customDataTypes: [] 8 | }; 9 | 10 | const customComponentsReducer = (state = defaultState, action) => { 11 | switch (action.type) { 12 | case "INITIALIZE_CUSTOM_COMPONENTS": 13 | return action.payload; 14 | default: 15 | return state; 16 | } 17 | }; 18 | 19 | export default customComponentsReducer; 20 | -------------------------------------------------------------------------------- /data/customDataTypesSample.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const customDataTypesSample = [ 4 | { 5 | dataType: "number", 6 | component: cellVal => ( 7 |
{cellVal}
8 | ) 9 | }, 10 | { 11 | dataType: "text", 12 | component: cellVal =>
{cellVal}
13 | }, 14 | { 15 | dataType: "iban", 16 | component: cellVal =>
{cellVal}
17 | } 18 | ]; 19 | 20 | export default customDataTypesSample; 21 | -------------------------------------------------------------------------------- /data/simpleOptionsNoDataSample.js: -------------------------------------------------------------------------------- 1 | import { 2 | title, 3 | keyColumn, 4 | columns, 5 | selectionIcons 6 | } from "./optionsObjectSample"; 7 | 8 | const simpleOptionsNoDataSample = { 9 | title, 10 | dimensions: { 11 | datatable: { 12 | width: "90vw", 13 | height: "40vh" 14 | } 15 | }, 16 | keyColumn, 17 | data: { 18 | columns, 19 | rows: [] 20 | }, 21 | features: { 22 | canEdit: true, 23 | canPrint: true, 24 | canDownload: true, 25 | selectionIcons 26 | } 27 | }; 28 | export default simpleOptionsNoDataSample; 29 | -------------------------------------------------------------------------------- /data/storeNoCustomComponentsSample.js: -------------------------------------------------------------------------------- 1 | import mergedSimpleOptionsSample from "./mergedSimpleOptionsSample"; 2 | import textReducer from "./textReducer"; 3 | 4 | const storeNoCustomComponentsSample = { 5 | datatableReducer: mergedSimpleOptionsSample, 6 | customComponentsReducer: { 7 | CustomTableBodyCell: null, 8 | CustomTableBodyRow: null, 9 | CustomTableHeaderCell: null, 10 | CustomTableHeaderRow: null, 11 | customDataTypes: [] 12 | }, 13 | notifierReducer: { notifications: [] }, 14 | textReducer 15 | }; 16 | 17 | export default storeNoCustomComponentsSample; 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE REQUEST]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Summary 11 | 12 | One paragraph explanation of the feature. 13 | 14 | ## Motivation 15 | 16 | Why are we doing this? What use cases does it support? What is the expected outcome? 17 | 18 | ## Describe alternatives you've considered 19 | 20 | A clear and concise description of the alternative solutions you've considered. 21 | 22 | ## Additional context 23 | 24 | Add any other context or screenshots about the feature request here. 25 | -------------------------------------------------------------------------------- /data/simpleOptionsSample.js: -------------------------------------------------------------------------------- 1 | import { title, keyColumn, data, selectionIcons, currentScreen } from "./optionsObjectSample"; 2 | 3 | const simpleOptionsSample = { 4 | title, 5 | currentScreen, 6 | dimensions: { 7 | datatable: { 8 | width: "90vw", 9 | height: "40vh" 10 | } 11 | }, 12 | keyColumn, 13 | data, 14 | features: { 15 | canEdit: true, 16 | canPrint: true, 17 | canCreatePreset: true, 18 | canDownload: true, 19 | canDelete: true, 20 | canSelectRow: true, 21 | canSearch: null, 22 | canFilter: true, 23 | canSaveUserConfiguration: undefined, 24 | selectionIcons 25 | } 26 | }; 27 | 28 | export default simpleOptionsSample; 29 | -------------------------------------------------------------------------------- /test/reduxTest/actionsTest/customComponentsActions.test.js: -------------------------------------------------------------------------------- 1 | import initializeCustomComponents from "../../../src/redux/actions/customComponentsActions"; 2 | 3 | describe("Component actions", () => { 4 | it("should create an action to initialize custom components", () => { 5 | const payload = { 6 | CustomTableBodyCell: null, 7 | CustomTableBodyRow: null, 8 | CustomTableHeaderCell: null, 9 | CustomTableHeaderRow: null, 10 | customDataTypes: [] 11 | }; 12 | const expectedAction = { 13 | type: "INITIALIZE_CUSTOM_COMPONENTS", 14 | payload 15 | }; 16 | expect(initializeCustomComponents(payload)).toEqual(expectedAction); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /data/storeCustomTableBodyRowComponentSample.js: -------------------------------------------------------------------------------- 1 | import mergedSimpleOptionsSample from "./mergedSimpleOptionsSample"; 2 | import customTableBodyRowSample from "./customTableBodyRowSample"; 3 | import textReducer from "./textReducer"; 4 | 5 | const storeCustomTableBodyRowComponentSample = { 6 | datatableReducer: mergedSimpleOptionsSample, 7 | customComponentsReducer: { 8 | CustomTableBodyCell: null, 9 | CustomTableBodyRow: customTableBodyRowSample, 10 | CustomTableHeaderCell: null, 11 | CustomTableHeaderRow: null, 12 | customDataTypes: [] 13 | }, 14 | notifierReducer: { notifications: [] }, 15 | textReducer 16 | }; 17 | 18 | export default storeCustomTableBodyRowComponentSample; 19 | -------------------------------------------------------------------------------- /data/storeCustomTableHeaderCellComponentSample.js: -------------------------------------------------------------------------------- 1 | import mergedSimpleOptionsSample from "./mergedSimpleOptionsSample"; 2 | import customTableHeaderCellSample from "./customTableHeaderCellSample"; 3 | import textReducer from "./textReducer"; 4 | 5 | const storeCustomTableHeaderCellComponentSample = { 6 | datatableReducer: mergedSimpleOptionsSample, 7 | customComponentsReducer: { 8 | CustomTableBodyCell: null, 9 | CustomTableBodyRow: null, 10 | CustomTableHeaderCell: customTableHeaderCellSample, 11 | CustomTableHeaderRow: null, 12 | customDataTypes: [] 13 | }, 14 | notifierReducer: [], 15 | textReducer 16 | }; 17 | 18 | export default storeCustomTableHeaderCellComponentSample; 19 | -------------------------------------------------------------------------------- /data/storeCustomTableBodyCellComponentSample.js: -------------------------------------------------------------------------------- 1 | import mergedSimpleOptionsSample from "./mergedSimpleOptionsSample"; 2 | import customTableBodyCellSample from "./customTableBodyCellSample"; 3 | import textReducer from "./textReducer"; 4 | 5 | const storeCustomTableBodyCellComponentSample = { 6 | datatableReducer: mergedSimpleOptionsSample, 7 | customComponentsReducer: { 8 | CustomTableBodyCell: customTableBodyCellSample, 9 | CustomTableBodyRow: null, 10 | CustomTableHeaderCell: null, 11 | CustomTableHeaderRow: null, 12 | customDataTypes: [] 13 | }, 14 | notifierReducer: { notifications: [] }, 15 | textReducer 16 | }; 17 | 18 | export default storeCustomTableBodyCellComponentSample; 19 | -------------------------------------------------------------------------------- /data/storeCustomTableHeaderRowComponentSample.js: -------------------------------------------------------------------------------- 1 | import mergedSimpleOptionsSample from "./mergedSimpleOptionsSample"; 2 | import customTableHeaderRowSample from "./customTableHeaderRowSample"; 3 | import textReducer from "./textReducer"; 4 | 5 | const storeCustomTableHeaderRowComponentSample = { 6 | datatableReducer: mergedSimpleOptionsSample, 7 | customComponentsReducer: { 8 | CustomTableBodyCell: null, 9 | CustomTableBodyRow: null, 10 | CustomTableHeaderCell: null, 11 | CustomTableHeaderRow: customTableHeaderRowSample, 12 | customDataTypes: [] 13 | }, 14 | notifierReducer: { notifications: [] }, 15 | textReducer 16 | }; 17 | 18 | export default storeCustomTableHeaderRowComponentSample; 19 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const nodeExternals = require("webpack-node-externals"); 3 | 4 | module.exports = { 5 | entry: path.resolve(__dirname, "src/index.js"), 6 | output: { 7 | path: path.resolve(__dirname, "./"), 8 | filename: "index.js", 9 | library: "", 10 | libraryTarget: "commonjs" 11 | }, 12 | externals: [nodeExternals()], 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.js$/, 17 | exclude: /(node_modules|bower_components)/, 18 | loader: "babel-loader", 19 | options: { 20 | presets: ["@babel/preset-env", "@babel/react"] 21 | } 22 | }, 23 | { 24 | test: /\.css$/, 25 | use: ["style-loader", "css-loader"] 26 | } 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "stable" 4 | cache: 5 | directories: 6 | - node_modules 7 | install: 8 | - npm i --legacy-peer-deps 9 | - npm install -g codecov 10 | script: 11 | - npm run lint 12 | - npm run build 13 | - npm test 14 | after_success: 15 | - if [ "$TRAVIS_BRANCH" == "master" ]; then 16 | codecov; 17 | fi 18 | before_deploy: 19 | - npm run build-storybook 20 | - if [ "$TRAVIS_BRANCH" == "master" ]; then 21 | npm run build; 22 | fi 23 | deploy: 24 | provider: pages 25 | skip_cleanup: true 26 | github_token: $GH_TOKEN 27 | local_dir: storybook-static/ 28 | on: 29 | branch: master 30 | deploy: 31 | provider: npm 32 | email: mailmorgandubois@gmail.com 33 | api_key: $NPM_TOKEN 34 | on: 35 | branch: master 36 | -------------------------------------------------------------------------------- /stories/Basics/defaultStory.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { chunk } from "lodash"; 3 | import { Datatable } from "../../src/index"; 4 | import { storyOptionsSample } from "../../data/samples"; 5 | 6 | const refreshRows = () => { 7 | const { rows } = storyOptionsSample.data; 8 | const randomTime = Math.floor(Math.random() * 4000) + 1000; 9 | const randomResolve = Math.floor(Math.random() * 10) + 1; 10 | return new Promise((resolve, reject) => { 11 | setTimeout(() => { 12 | if (randomResolve > 3) { 13 | resolve(chunk(rows, rows.length)[0]); 14 | } 15 | reject(new Error("err")); 16 | }, randomTime); 17 | }); 18 | }; 19 | 20 | const defaultStory = () => { 21 | return ( 22 | 27 | ); 28 | }; 29 | 30 | export default defaultStory; 31 | -------------------------------------------------------------------------------- /data/storyOptionsNoActionSample.js: -------------------------------------------------------------------------------- 1 | import { title, keyColumn, data } from "./optionsObjectSample"; 2 | import rows from "./rows"; 3 | 4 | const storyOptionsNoActionSample = { 5 | title, 6 | 7 | dimensions: { 8 | datatable: { 9 | width: "100%", 10 | height: "70vh" 11 | } 12 | }, 13 | keyColumn, 14 | data: { 15 | ...data, 16 | rows 17 | }, 18 | features: { 19 | canPrint: true, 20 | canDownload: true, 21 | canSearch: true, 22 | canFilter: true, 23 | canRefreshRows: true, 24 | canOrderColumns: true, 25 | canSaveUserConfiguration: true, 26 | userConfiguration: { 27 | columnsOrder: [ 28 | "id", 29 | "name", 30 | "age", 31 | "adult", 32 | "birthDate", 33 | "eyeColor", 34 | "iban" 35 | ], 36 | copyToClipboard: true 37 | } 38 | } 39 | }; 40 | 41 | export default storyOptionsNoActionSample; 42 | -------------------------------------------------------------------------------- /data/storeSample.js: -------------------------------------------------------------------------------- 1 | import mergedSimpleOptionsSample from "./mergedSimpleOptionsSample"; 2 | import customTableBodyCellSample from "./customTableBodyCellSample"; 3 | import customTableBodyRowSample from "./customTableBodyRowSample"; 4 | import customTableHeaderCellSample from "./customTableHeaderCellSample"; 5 | import customTableHeaderRowSample from "./customTableHeaderRowSample"; 6 | import customDataTypesSample from "./customDataTypesSample"; 7 | import textReducer from "./textReducer"; 8 | 9 | const storeSample = { 10 | datatableReducer: mergedSimpleOptionsSample, 11 | customComponentsReducer: { 12 | CustomTableBodyCell: customTableBodyCellSample, 13 | CustomTableBodyRow: customTableBodyRowSample, 14 | CustomTableHeaderCell: customTableHeaderCellSample, 15 | CustomTableHeaderRow: customTableHeaderRowSample, 16 | customDataTypes: customDataTypesSample 17 | }, 18 | notifierReducer: { notifications: [] }, 19 | textReducer 20 | }; 21 | 22 | export default storeSample; 23 | -------------------------------------------------------------------------------- /data/storeSampleWithPages.js: -------------------------------------------------------------------------------- 1 | import mergedPageSample from "./mergedPageSample"; 2 | import customTableBodyCellSample from "./customTableBodyCellSample"; 3 | import customTableBodyRowSample from "./customTableBodyRowSample"; 4 | import customTableHeaderCellSample from "./customTableHeaderCellSample"; 5 | import customTableHeaderRowSample from "./customTableHeaderRowSample"; 6 | import customDataTypesSample from "./customDataTypesSample"; 7 | import textReducer from "./textReducer"; 8 | 9 | const storeSampleWithPages = { 10 | datatableReducer: mergedPageSample, 11 | customComponentsReducer: { 12 | CustomTableBodyCell: customTableBodyCellSample, 13 | CustomTableBodyRow: customTableBodyRowSample, 14 | CustomTableHeaderCell: customTableHeaderCellSample, 15 | CustomTableHeaderRow: customTableHeaderRowSample, 16 | customDataTypes: customDataTypesSample 17 | }, 18 | notifierReducer: { notifications: [] }, 19 | textReducer 20 | }; 21 | 22 | export default storeSampleWithPages; 23 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb", "prettier", "prettier/react"], 3 | "plugins": ["react", "prettier"], 4 | "parser": "babel-eslint", 5 | "rules": { 6 | "react/prefer-stateless-function": 0, 7 | "prefer-spread": 0, 8 | "react/require-default-props": 0, 9 | "import/no-extraneous-dependencies": 0, 10 | "radix": 0, 11 | "react/no-find-dom-node": 0, 12 | "import/no-named-as-default": 0, 13 | "react/no-multi-comp": 0, 14 | "react/no-unescaped-entities": 0, 15 | "react/jsx-filename-extension": [ 16 | 1, 17 | { 18 | "extensions": [".js", "jsx"] 19 | } 20 | ], 21 | "prettier/prettier": "error", 22 | "max-len": 0 23 | }, 24 | "env": { 25 | "browser": true, 26 | "es6": true, 27 | "jest": true 28 | }, 29 | "settings": { 30 | "import/ignore": "node_modules", 31 | "import/resolver": { 32 | "webpack": { 33 | "config": "./config/webpack-common-config.js" 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/DatatableCore/InputTypes/BooleanWrapper.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Checkbox } from "@material-ui/core"; 3 | import { 4 | cellValPropType, 5 | rowIdPropType, 6 | columnIdPropType, 7 | setRowEditedPropType, 8 | requiredPropType 9 | } from "../../../proptypes"; 10 | 11 | const BooleanWrapper = ({ 12 | cellVal, 13 | rowId, 14 | columnId, 15 | setRowEdited, 16 | required 17 | }) => { 18 | return ( 19 | 25 | setRowEdited({ rowId, columnId, newValue: checked }) 26 | } 27 | /> 28 | ); 29 | }; 30 | 31 | BooleanWrapper.propTypes = { 32 | required: requiredPropType, 33 | cellVal: cellValPropType.isRequired, 34 | rowId: rowIdPropType.isRequired, 35 | columnId: columnIdPropType.isRequired, 36 | setRowEdited: setRowEditedPropType 37 | }; 38 | 39 | export default BooleanWrapper; 40 | -------------------------------------------------------------------------------- /src/components/MuiTheme.js: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from "@material-ui/core/styles"; 2 | 3 | export const mainTheme = overideTheme => 4 | createMuiTheme({ 5 | typography: { 6 | useNextVariants: true 7 | }, 8 | overrides: { 9 | MuiInput: { 10 | root: { 11 | fontSize: "0.9rem", 12 | lineHeight: "0.9rem", 13 | color: "black" 14 | } 15 | } 16 | }, 17 | ...overideTheme 18 | }); 19 | 20 | export const customVariant = () => ({ 21 | errorTooltip: { 22 | backgroundColor: "red", 23 | color: "white", 24 | "&:before": { 25 | borderBottom: "5px solid red" 26 | } 27 | }, 28 | disabledButtonPopper: { 29 | marginTop: "5px" 30 | }, 31 | enabledButtonPopper: { 32 | marginTop: "12px" 33 | }, 34 | defaultIcon: { 35 | color: "black" 36 | }, 37 | errorIcon: { 38 | color: "red" 39 | }, 40 | validIcon: { 41 | color: "#4caf50" 42 | }, 43 | whiteIcon: { 44 | color: "white" 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /stories/Override/customDataTypesStory.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { chunk } from "lodash"; 3 | import { Datatable } from "../../src/index"; 4 | import { storyOptionsSample2, customDataTypesSample } from "../../data/samples"; 5 | 6 | const refreshRows = () => { 7 | const { rows } = storyOptionsSample2.data; 8 | const randomRows = Math.floor(Math.random() * rows.length) + 1; 9 | const randomTime = Math.floor(Math.random() * 4000) + 1000; 10 | const randomResolve = Math.floor(Math.random() * 10) + 1; 11 | return new Promise((resolve, reject) => { 12 | setTimeout(() => { 13 | if (randomResolve > 3) { 14 | resolve(chunk(rows, randomRows)[0]); 15 | } 16 | reject(new Error("err")); 17 | }, randomTime); 18 | }); 19 | }; 20 | 21 | const customDataTypesStory = () => { 22 | return ( 23 | 29 | ); 30 | }; 31 | 32 | export default customDataTypesStory; 33 | -------------------------------------------------------------------------------- /data/mergedPageSample.js: -------------------------------------------------------------------------------- 1 | import { chunk } from "lodash"; 2 | import { 3 | title, 4 | dimensions, 5 | keyColumn, 6 | font, 7 | data, 8 | refreshRows, 9 | rowsGlobalEdited, 10 | newRows, 11 | rowsDeleted, 12 | isRefreshing, 13 | stripped, 14 | searchTerm, 15 | orderBy, 16 | features 17 | } from "./optionsObjectSample"; 18 | 19 | const mergedPageSample = { 20 | title, 21 | dimensions: { 22 | ...dimensions, 23 | datatable: { 24 | ...dimensions.datatable, 25 | totalWidthNumber: 0 26 | } 27 | }, 28 | pagination: { 29 | pageSelected: 5, 30 | pageTotal: 20, 31 | rowsPerPageSelected: 10, 32 | rowsCurrentPage: chunk(data.rows, 10)[4] 33 | }, 34 | keyColumn, 35 | rowsGlobalEdited, 36 | newRows, 37 | rowsDeleted, 38 | actions: null, 39 | refreshRows, 40 | isRefreshing, 41 | stripped, 42 | searchTerm, 43 | font, 44 | orderBy, 45 | data, 46 | features: { 47 | ...features, 48 | additionalIcons: [], 49 | additionalActions: [] 50 | } 51 | }; 52 | 53 | export default mergedPageSample; 54 | -------------------------------------------------------------------------------- /stories/Override/customTableBodyCellStory.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { chunk } from "lodash"; 3 | import { Datatable } from "../../src/index"; 4 | import { 5 | storyOptionsSample, 6 | customTableBodyCellSample 7 | } from "../../data/samples"; 8 | 9 | const refreshRows = () => { 10 | const { rows } = storyOptionsSample.data; 11 | const randomRows = Math.floor(Math.random() * rows.length) + 1; 12 | const randomTime = Math.floor(Math.random() * 4000) + 1000; 13 | const randomResolve = Math.floor(Math.random() * 10) + 1; 14 | return new Promise((resolve, reject) => { 15 | setTimeout(() => { 16 | if (randomResolve > 3) { 17 | resolve(chunk(rows, randomRows)[0]); 18 | } 19 | reject(new Error("err")); 20 | }, randomTime); 21 | }); 22 | }; 23 | 24 | const customTableBodyCellStory = () => { 25 | return ( 26 | 32 | ); 33 | }; 34 | 35 | export default customTableBodyCellStory; 36 | -------------------------------------------------------------------------------- /stories/Override/customTableHeaderRowStory.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { chunk } from "lodash"; 3 | import { Datatable } from "../../src/index"; 4 | import { 5 | storyOptionsSample, 6 | customTableHeaderRowSample 7 | } from "../../data/samples"; 8 | 9 | const refreshRows = () => { 10 | const { rows } = storyOptionsSample.data; 11 | const randomRows = Math.floor(Math.random() * rows.length) + 1; 12 | const randomTime = Math.floor(Math.random() * 4000) + 1000; 13 | const randomResolve = Math.floor(Math.random() * 10) + 1; 14 | return new Promise((resolve, reject) => { 15 | setTimeout(() => { 16 | if (randomResolve > 3) { 17 | resolve(chunk(rows, randomRows)[0]); 18 | } 19 | reject(new Error("err")); 20 | }, randomTime); 21 | }); 22 | }; 23 | 24 | const customTableHeaderRowStory = () => { 25 | return ( 26 | 32 | ); 33 | }; 34 | 35 | export default customTableHeaderRowStory; 36 | -------------------------------------------------------------------------------- /data/customTableHeaderCellSample.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { columnPropType } from "../src/proptypes"; 3 | 4 | const customTableHeaderCellSample = ({ column }) => { 5 | switch (column.dataType) { 6 | case "number": 7 | return ( 8 |
9 | {column.label} 10 |
11 | ); 12 | case "text": 13 | return ( 14 |
15 | {column.label} 16 |
17 | ); 18 | case "boolean": 19 | return ( 20 |
21 | {column.label} 22 |
23 | ); 24 | case "dateTime": 25 | return
{column.label}
; 26 | default: 27 | return ( 28 |
29 | {column.label} 30 |
31 | ); 32 | } 33 | }; 34 | 35 | customTableHeaderCellSample.propTypes = { 36 | column: columnPropType 37 | }; 38 | 39 | export default customTableHeaderCellSample; 40 | -------------------------------------------------------------------------------- /stories/Override/customTableHeaderCellStory.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { chunk } from "lodash"; 3 | import { Datatable } from "../../src/index"; 4 | import { 5 | storyOptionsSample, 6 | customTableHeaderCellSample 7 | } from "../../data/samples"; 8 | 9 | const refreshRows = () => { 10 | const { rows } = storyOptionsSample.data; 11 | const randomRows = Math.floor(Math.random() * rows.length) + 1; 12 | const randomTime = Math.floor(Math.random() * 4000) + 1000; 13 | const randomResolve = Math.floor(Math.random() * 10) + 1; 14 | return new Promise((resolve, reject) => { 15 | setTimeout(() => { 16 | if (randomResolve > 3) { 17 | resolve(chunk(rows, randomRows)[0]); 18 | } 19 | reject(new Error("err")); 20 | }, randomTime); 21 | }); 22 | }; 23 | 24 | const customTableHeaderCellStory = () => { 25 | return ( 26 | 32 | ); 33 | }; 34 | 35 | export default customTableHeaderCellStory; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 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 | -------------------------------------------------------------------------------- /stories/Override/customTableBodyRowStory.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { chunk } from "lodash"; 3 | import { Datatable } from "../../src/index"; 4 | import { 5 | storyOptionsNoActionSample, 6 | storyOptionsSample, 7 | customTableBodyRowSample 8 | } from "../../data/samples"; 9 | 10 | const refreshRows = () => { 11 | const { rows } = storyOptionsSample.data; 12 | const randomRows = Math.floor(Math.random() * rows.length) + 1; 13 | const randomTime = Math.floor(Math.random() * 4000) + 1000; 14 | const randomResolve = Math.floor(Math.random() * 10) + 1; 15 | return new Promise((resolve, reject) => { 16 | setTimeout(() => { 17 | if (randomResolve > 3) { 18 | resolve(chunk(rows, randomRows)[0]); 19 | } 20 | reject(new Error("err")); 21 | }, randomTime); 22 | }); 23 | }; 24 | 25 | const customTableBodyRowStory = () => { 26 | return ( 27 | 33 | ); 34 | }; 35 | 36 | export default customTableBodyRowStory; 37 | -------------------------------------------------------------------------------- /data/storeNoDataSample.js: -------------------------------------------------------------------------------- 1 | import { 2 | title, 3 | dimensions, 4 | keyColumn, 5 | pagination, 6 | font, 7 | refreshRows, 8 | isRefreshing, 9 | rowsGlobalEdited, 10 | stripped, 11 | orderBy, 12 | searchTerm, 13 | features 14 | } from "./optionsObjectSample"; 15 | import textReducer from "./textReducer"; 16 | 17 | const storeNoDataSample = { 18 | datatableReducer: { 19 | title, 20 | dimensions, 21 | keyColumn, 22 | font, 23 | pagination, 24 | data: { 25 | columns: [], 26 | rows: [] 27 | }, 28 | rowsEdited: [], 29 | rowsGlobalEdited, 30 | rowsSelected: [], 31 | refreshRows, 32 | isRefreshing, 33 | stripped, 34 | orderBy, 35 | searchTerm, 36 | actions: null, 37 | features: { 38 | ...features, 39 | additionalIcons: [] 40 | } 41 | }, 42 | customComponentsReducer: { 43 | CustomTableBodyCell: null, 44 | CustomTableBodyRow: null, 45 | CustomTableHeaderCell: null, 46 | CustomTableHeaderRow: null, 47 | customDataTypes: null 48 | }, 49 | notifierReducer: { notifications: [] }, 50 | textReducer 51 | }; 52 | 53 | export default storeNoDataSample; 54 | -------------------------------------------------------------------------------- /data/storeNoRowsDataSample.js: -------------------------------------------------------------------------------- 1 | import { 2 | title, 3 | dimensions, 4 | keyColumn, 5 | font, 6 | pagination, 7 | features, 8 | refreshRows, 9 | isRefreshing, 10 | rowsGlobalEdited, 11 | stripped, 12 | orderBy, 13 | searchTerm, 14 | columns 15 | } from "./optionsObjectSample"; 16 | import textReducer from "./textReducer"; 17 | 18 | const storeNoRowsDataSample = { 19 | datatableReducer: { 20 | title, 21 | dimensions, 22 | keyColumn, 23 | font, 24 | pagination, 25 | data: { 26 | columns, 27 | rows: [] 28 | }, 29 | rowsEdited: [], 30 | rowsGlobalEdited, 31 | rowsSelected: [], 32 | actions: null, 33 | refreshRows, 34 | isRefreshing, 35 | stripped, 36 | orderBy, 37 | searchTerm, 38 | features: { 39 | ...features, 40 | additionalIcons: [] 41 | } 42 | }, 43 | customComponentsReducer: { 44 | CustomTableBodyCell: null, 45 | CustomTableBodyRow: null, 46 | CustomTableHeaderCell: null, 47 | CustomTableHeaderRow: null, 48 | customDataTypes: null 49 | }, 50 | notifierReducer: { notifications: [] }, 51 | textReducer 52 | }; 53 | 54 | export default storeNoRowsDataSample; 55 | -------------------------------------------------------------------------------- /src/components/DatatableCore/InputTypes/PickersFunction.js: -------------------------------------------------------------------------------- 1 | import { moment } from "../../../moment.config"; 2 | 3 | export const checkValue = ({ cellVal, mounting, valueVerification }) => { 4 | const { message, error } = valueVerification(cellVal); 5 | const newState = { 6 | tooltipOpen: mounting ? false : error, 7 | message, 8 | error 9 | }; 10 | 11 | return newState; 12 | }; 13 | 14 | export const setValue = ({ 15 | date, 16 | value, 17 | dateFormat, 18 | rowId, 19 | columnId, 20 | setRowEdited, 21 | type, 22 | valueVerification 23 | }) => { 24 | let cellVal = value; 25 | if (cellVal !== null) { 26 | cellVal = date ? moment(date).format(dateFormat) : cellVal; 27 | cellVal = value || cellVal; 28 | cellVal = type === "number" ? Number(cellVal) : cellVal; 29 | } 30 | 31 | let newState = { 32 | error: false, 33 | tooltipOpen: false, 34 | message: "" 35 | }; 36 | 37 | if (valueVerification) { 38 | newState = checkValue({ cellVal, valueVerification }); 39 | } 40 | 41 | const { error } = newState; 42 | setRowEdited({ 43 | rowId, 44 | columnId, 45 | newValue: cellVal, 46 | error 47 | }); 48 | 49 | return newState; 50 | }; 51 | -------------------------------------------------------------------------------- /test/reduxTest/actionsTest/notifierActions.test.js: -------------------------------------------------------------------------------- 1 | import * as actions from "../../../src/redux/actions/notifierActions"; 2 | 3 | describe("Notifier actions", () => { 4 | it("should create an action to enqueue Snackbar", () => { 5 | const key = new Date().getTime() + Math.random(); 6 | const payload = { 7 | message: "Refresh error.", 8 | key, 9 | options: { 10 | key, 11 | variant: "error" 12 | } 13 | }; 14 | const expectedAction = { 15 | type: "ENQUEUE_SNACKBAR", 16 | payload 17 | }; 18 | expect(actions.enqueueSnackbar(payload)).toEqual(expectedAction); 19 | }); 20 | 21 | it("should create an action to close Snackbar", () => { 22 | const payload = new Date().getTime() + Math.random(); 23 | const expectedAction = { 24 | type: "CLOSE_SNACKBAR", 25 | payload 26 | }; 27 | expect(actions.closeSnackbar(payload)).toEqual(expectedAction); 28 | }); 29 | 30 | it("should create an action to remove Snackbar", () => { 31 | const payload = new Date().getTime() + Math.random(); 32 | const expectedAction = { 33 | type: "REMOVE_SNACKBAR", 34 | payload 35 | }; 36 | expect(actions.removeSnackbar(payload)).toEqual(expectedAction); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | ## Expected Behavior 13 | 14 | 15 | ## Current Behavior 16 | 17 | 18 | ## Possible Solution 19 | 20 | 21 | ## Steps to Reproduce 22 | 23 | 24 | 1. 25 | 2. 26 | 3. 27 | 4. 28 | 29 | ## Context (Environment) 30 | 31 | 32 | 33 | 34 | 35 | ## Detailed Description 36 | 37 | 38 | ## Possible Implementation 39 | 40 | -------------------------------------------------------------------------------- /data/customTableHeaderRowSample.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | columnsOrderPropType, 4 | columnSizeMultiplierPropType 5 | } from "../src/proptypes"; 6 | import { simpleOptionsSample } from "./samples"; 7 | 8 | const customTableHeaderRowSample = ({ columnsOrder, columnSizeMultiplier }) => { 9 | const { columns } = simpleOptionsSample.data; 10 | const columnAction = { 11 | id: "o2xpActions", 12 | label: "Actions", 13 | colSize: "150px", 14 | editable: false 15 | }; 16 | return ( 17 |
18 | {columnsOrder.map(columnId => { 19 | const column = 20 | columnId === "o2xpActions" 21 | ? columnAction 22 | : columns.find(col => col.id === columnId); 23 | const width = `${( 24 | column.colSize.split("px")[0] * columnSizeMultiplier 25 | ).toString()}px`; 26 | return ( 27 |
28 |
{column.label}
29 |
30 | ); 31 | })} 32 |
33 | ); 34 | }; 35 | 36 | customTableHeaderRowSample.propTypes = { 37 | columnsOrder: columnsOrderPropType, 38 | columnSizeMultiplier: columnSizeMultiplierPropType 39 | }; 40 | 41 | export default customTableHeaderRowSample; 42 | -------------------------------------------------------------------------------- /data/customTableBodyCellSample.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { cellValPropType, columnPropType } from "../src/proptypes"; 3 | 4 | const customTableBodyCellSample = ({ cellVal, column }) => { 5 | let val; 6 | if (cellVal === null || cellVal === undefined) { 7 | val =
; 8 | } else { 9 | switch (column.dataType) { 10 | case "boolean": 11 | if (cellVal) { 12 | val = ( 13 |
17 | Yes 18 |
19 | ); 20 | } else { 21 | val = ( 22 |
26 | No 27 |
28 | ); 29 | } 30 | break; 31 | default: 32 | val = ( 33 |
34 | {cellVal} 35 |
36 | ); 37 | break; 38 | } 39 | } 40 | 41 | return val; 42 | }; 43 | 44 | customTableBodyCellSample.propTypes = { 45 | cellVal: cellValPropType, 46 | column: columnPropType 47 | }; 48 | 49 | export default customTableBodyCellSample; 50 | -------------------------------------------------------------------------------- /src/redux/reducers/notifierReducer.js: -------------------------------------------------------------------------------- 1 | const defaultState = { 2 | notifications: [] 3 | }; 4 | 5 | const enqueueSnackbar = (state, payload) => { 6 | return { 7 | ...state, 8 | notifications: [ 9 | ...state.notifications, 10 | { 11 | key: payload.key, 12 | ...payload 13 | } 14 | ] 15 | }; 16 | }; 17 | 18 | const closeSnackbar = (state, payload) => { 19 | return { 20 | ...state, 21 | notifications: state.notifications.map(notification => 22 | notification.key === payload 23 | ? { ...notification, dismissed: true } 24 | : { ...notification } 25 | ) 26 | }; 27 | }; 28 | 29 | const removeSnackbar = (state, payload) => { 30 | return { 31 | ...state, 32 | notifications: state.notifications.filter( 33 | notification => notification.key !== payload 34 | ) 35 | }; 36 | }; 37 | 38 | const notifierReducer = (state = defaultState, action) => { 39 | const { payload, type } = action; 40 | switch (type) { 41 | case "ENQUEUE_SNACKBAR": 42 | return enqueueSnackbar(state, payload); 43 | case "CLOSE_SNACKBAR": 44 | return closeSnackbar(state, payload); 45 | case "REMOVE_SNACKBAR": 46 | return removeSnackbar(state, payload); 47 | default: 48 | return state; 49 | } 50 | }; 51 | 52 | export default notifierReducer; 53 | -------------------------------------------------------------------------------- /src/components/Loader.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import { ScrollSyncPane } from "react-scroll-sync"; 3 | import { PulseLoader } from "react-spinners"; 4 | import { 5 | heightNumberPropType, 6 | widthNumberPropType, 7 | columnSizeMultiplierPropType 8 | } from "../proptypes"; 9 | 10 | const Loader = props => { 11 | const { height, width, columnSizeMultiplier, totalWidthNumber } = props; 12 | 13 | return ( 14 | 15 |
16 | 17 |
18 | 19 |
27 |
32 | . 33 |
34 |
35 |
36 |
37 | ); 38 | }; 39 | 40 | Loader.propTypes = { 41 | height: heightNumberPropType.isRequired, 42 | width: widthNumberPropType.isRequired, 43 | totalWidthNumber: widthNumberPropType, 44 | columnSizeMultiplier: columnSizeMultiplierPropType 45 | }; 46 | 47 | export default Loader; 48 | -------------------------------------------------------------------------------- /data/maximumOptionsSample.js: -------------------------------------------------------------------------------- 1 | import { 2 | title, 3 | keyColumn, 4 | font, 5 | data, 6 | additionalIcons, 7 | selectionIcons, 8 | additionalActions, 9 | areFilterFieldsDisplayed, 10 | isSearchFieldDisplayed, 11 | filterTerms, 12 | filterResultForEachColumn 13 | } from "./optionsObjectSample"; 14 | 15 | const maximumOptionsSample = { 16 | title, 17 | dimensions: { 18 | datatable: { 19 | width: "500px", 20 | height: "40vh" 21 | }, 22 | row: { 23 | height: "33px" 24 | } 25 | }, 26 | areFilterFieldsDisplayed, 27 | isSearchFieldDisplayed, 28 | filterTerms, 29 | filterResultForEachColumn, 30 | keyColumn, 31 | font, 32 | data, 33 | features: { 34 | canEdit: true, 35 | canPrint: true, 36 | canDownload: true, 37 | canDelete: true, 38 | canSearch: true, 39 | canFilter: true, 40 | canDuplicate: true, 41 | canRefreshRows: true, 42 | canOrderColumns: true, 43 | canCreatePreset: false, 44 | columnsPresetsToDisplay: [], 45 | canSelectRow: true, 46 | canSaveUserConfiguration: true, 47 | userConfiguration: { 48 | columnsOrder: ["id", "name", "age"], 49 | copyToClipboard: true 50 | }, 51 | rowsPerPage: { 52 | available: [50], 53 | selected: 50 54 | }, 55 | additionalActions, 56 | additionalIcons, 57 | selectionIcons 58 | } 59 | }; 60 | 61 | export default maximumOptionsSample; 62 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Proposed changes 2 | 3 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 4 | 5 | ## Types of changes 6 | 7 | What types of changes does your code introduce to @o2xp/react-datatable? 8 | _Put an `x` in the boxes that apply_ 9 | 10 | - [ ] Bugfix (non-breaking change which fixes an issue) 11 | - [ ] New feature (non-breaking change which adds functionality) 12 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 13 | - [ ] Code improvement 14 | 15 | ## Checklist 16 | 17 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 18 | 19 | - [ ] I have read the [CONTRIBUTING](https://github.com/o2xp/react-datatable/blob/develop/CONTRIBUTING.md) doc 20 | - [ ] I have signed the [CLA]() 21 | - [ ] Lint and unit tests pass locally with my changes 22 | - [ ] I have added tests that prove my fix is effective or that my feature works 23 | - [ ] I have added necessary documentation (if appropriate) 24 | 25 | ## Further comments 26 | 27 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 28 | -------------------------------------------------------------------------------- /stories/index.stories.js: -------------------------------------------------------------------------------- 1 | import { storiesOf } from "@storybook/react"; 2 | import { withKnobs } from "@storybook/addon-knobs"; 3 | import defaultStory from "./Basics/defaultStory"; 4 | import noDataStory from "./Basics/noDataStory"; 5 | import customTableHeaderRowStory from "./Override/customTableHeaderRowStory"; 6 | import customTableHeaderCellStory from "./Override/customTableHeaderCellStory"; 7 | import customTableBodyRowStory from "./Override/customTableBodyRowStory"; 8 | import customTableBodyCellStory from "./Override/customTableBodyCellStory"; 9 | import customDataTypesStory from "./Override/customDataTypesStory"; 10 | 11 | // import { action } from "@storybook/addon-actions"; 12 | // import { linkTo } from "@storybook/addon-links"; 13 | // import { withState } from "@dump247/storybook-state"; 14 | 15 | const storiesBasics = storiesOf("React Datatable|Basics", module); 16 | 17 | storiesBasics.addDecorator(withKnobs); 18 | storiesBasics.add("default", defaultStory); 19 | 20 | storiesBasics.add("no data", noDataStory); 21 | 22 | const storiesOverride = storiesOf("React Datatable|Override", module); 23 | storiesOverride.addDecorator(withKnobs); 24 | storiesOverride.add("custom table header row", customTableHeaderRowStory); 25 | storiesOverride.add("custom table header cell", customTableHeaderCellStory); 26 | storiesOverride.add("custom table body row", customTableBodyRowStory); 27 | storiesOverride.add("custom table body cell", customTableBodyCellStory); 28 | storiesOverride.add("custom dataTypes", customDataTypesStory); 29 | -------------------------------------------------------------------------------- /src/components/DatatableHeader/Widgets/AdditionalIcons.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from "react"; 2 | import { connect } from "react-redux"; 3 | import { IconButton, Tooltip, Zoom } from "@material-ui/core"; 4 | import { additionalIconsPropType } from "../../../proptypes"; 5 | 6 | class AdditionalIcons extends Component { 7 | render() { 8 | const { additionalIcons } = this.props; 9 | return ( 10 | 11 | {additionalIcons.map((icon, i) => ( 12 | 18 | 19 | icon.onClick()} 26 | disabled={icon.disabled} 27 | > 28 | {icon.icon} 29 | 30 | 31 | 32 | ))} 33 | 34 | ); 35 | } 36 | } 37 | 38 | AdditionalIcons.propTypes = { 39 | additionalIcons: additionalIconsPropType.isRequired 40 | }; 41 | 42 | const mapStateToProps = state => { 43 | return { 44 | additionalIcons: state.datatableReducer.features.additionalIcons 45 | }; 46 | }; 47 | 48 | export default connect(mapStateToProps)(AdditionalIcons); 49 | -------------------------------------------------------------------------------- /test/componentsTest/DatatableHeaderTest/Widgets/Filter.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import configureStore from "redux-mock-store"; 3 | import { Provider } from "react-redux"; 4 | import { shallow, mount } from "enzyme"; 5 | import { storeSample } from "../../../../data/samples"; 6 | import Filter from "../../../../src/components/DatatableHeader/Widgets/Filter"; 7 | 8 | const mockStore = configureStore(); 9 | const store = mockStore(storeSample); 10 | const { rows } = storeSample.datatableReducer.data; 11 | describe("Filter component", () => { 12 | it("connected should render without errors", () => { 13 | const wrapper = shallow( 14 | 15 | 16 | 17 | ); 18 | expect(wrapper.find("Connect(Filter)")).toHaveLength(1); 19 | }); 20 | 21 | it("connected should mount without errors", () => { 22 | const wrapper = mount( 23 | 24 | 25 | 26 | ); 27 | const button = wrapper.find("button.filter-icon"); 28 | button.simulate("click"); 29 | expect(wrapper.find("Connect(Filter)")).toHaveLength(1); 30 | }); 31 | 32 | it("filter with props", () => { 33 | const wrapper = mount( 34 | 35 | 36 | 37 | ); 38 | const button = wrapper.find("button.filter-icon"); 39 | button.simulate("click"); 40 | wrapper.setProps({ filterText: "sd" }); 41 | expect(wrapper).toBeTruthy(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /data/mergedSimpleOptionsSample.js: -------------------------------------------------------------------------------- 1 | import { 2 | title, 3 | dimensions, 4 | keyColumn, 5 | font, 6 | data, 7 | columnAction, 8 | userConfiguration, 9 | pagination, 10 | rowsEdited, 11 | rowsGlobalEdited, 12 | rowsSelected, 13 | refreshRows, 14 | isRefreshing, 15 | stripped, 16 | orderBy, 17 | newRows, 18 | rowsDeleted, 19 | searchTerm, 20 | filterTerms, 21 | filterResultForEachColumn, 22 | areFilterFieldsDisplayed, 23 | isSearchFieldDisplayed, 24 | features 25 | } from "./optionsObjectSample"; 26 | 27 | const mergedSimpleOptionsSample = { 28 | title, 29 | currentScreen: "", 30 | dimensions: { 31 | ...dimensions, 32 | datatable: { 33 | ...dimensions.datatable, 34 | totalWidthNumber: 0 35 | } 36 | }, 37 | data: { 38 | ...data, 39 | columns: [columnAction, ...data.columns] 40 | }, 41 | pagination: { 42 | ...pagination, 43 | rowsCurrentPage: data.rows 44 | }, 45 | keyColumn, 46 | font, 47 | rowsEdited, 48 | rowsGlobalEdited, 49 | refreshRows, 50 | isRefreshing, 51 | stripped, 52 | orderBy, 53 | newRows, 54 | rowsDeleted, 55 | searchTerm, 56 | filterTerms, 57 | filterResultForEachColumn, 58 | areFilterFieldsDisplayed, 59 | isSearchFieldDisplayed, 60 | rowsSelected, 61 | actions: null, 62 | features: { 63 | ...features, 64 | userConfiguration: { 65 | ...userConfiguration, 66 | columnsOrder: ["o2xpActions", ...userConfiguration.columnsOrder] 67 | }, 68 | additionalActions: [], 69 | additionalIcons: [] 70 | } 71 | }; 72 | 73 | export default mergedSimpleOptionsSample; 74 | -------------------------------------------------------------------------------- /test/componentsTest/DatatableCoreTest/InputTypesTest/BooleanWrapper.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from "enzyme"; 2 | import { Checkbox } from "@material-ui/core"; 3 | import BooleanWrapper from "../../../../src/components/DatatableCore/InputTypes/BooleanWrapper"; 4 | 5 | const setRowEdited = jest.fn(); 6 | const booleanValue = { 7 | cellVal: true, 8 | rowId: "5cd9307025f4f0572995990f", 9 | columnId: "adult", 10 | setRowEdited: ({ rowId, columnId, newValue }) => 11 | setRowEdited({ rowId, columnId, newValue }) 12 | }; 13 | 14 | describe("Boolean wrapper", () => { 15 | it("should render a Checkbox", () => { 16 | const wrapper = mount(BooleanWrapper(booleanValue)); 17 | expect(wrapper.find(Checkbox)).toHaveLength(1); 18 | }); 19 | 20 | it("should render a Checkbox that is checked", () => { 21 | const wrapper = mount(BooleanWrapper(booleanValue)); 22 | expect(wrapper.find("input").props().checked).toBeTruthy(); 23 | }); 24 | 25 | it("should render a Checkbox that is not checked", () => { 26 | const wrapper = mount(BooleanWrapper({ ...booleanValue, cellVal: false })); 27 | expect(wrapper.find("input").props().checked).toBeFalsy(); 28 | }); 29 | 30 | it("should call setRowEdited onChange", () => { 31 | const wrapper = mount(BooleanWrapper(booleanValue)); 32 | wrapper.find("input").simulate("change", { target: { checked: false } }); 33 | const { rowId, columnId } = booleanValue; 34 | expect(setRowEdited).toHaveBeenCalled(); 35 | expect(setRowEdited).toHaveBeenCalledWith({ 36 | rowId, 37 | columnId, 38 | newValue: false 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /examples/override/headerCell.md: -------------------------------------------------------------------------------- 1 | Component example : 2 | 3 | [**Live implementation**](https://codesandbox.io/s/header-cell-override-example-for-o2xpreact-datatable-33tg2) 4 | 5 | ```jsx 6 | 7 | // ES6 8 | import { Datatable } from "@o2xp/react-datatable"; 9 | import React, { Component } from "react"; 10 | 11 | // Custom table header cell Example 12 | const options = { 13 | keyColumn: "id", 14 | data: { 15 | columns: [ 16 | { 17 | id: "id", 18 | label: "id", 19 | colSize: "80px", 20 | dataType: "text" 21 | }, 22 | { 23 | id: "name", 24 | label: "name", 25 | colSize: "150px", 26 | dataType: "name" 27 | }, 28 | { 29 | id: "age", 30 | label: "age", 31 | colSize: "50px", 32 | dataType: "number" 33 | } 34 | ], 35 | rows: [ 36 | { 37 | id: "50cf", 38 | age: 28, 39 | name: "Kerr Mayo" 40 | }, 41 | { 42 | id: "209", 43 | age: 34, 44 | name: "Freda Bowman" 45 | }, 46 | { 47 | id: "2dd81ef", 48 | age: 14, 49 | name: "Becky Lawrence" 50 | } 51 | ] 52 | } 53 | }; 54 | 55 | class App extends Component { 56 | buildCustomTableHeaderCell = ({ column }) => { 57 | return
{column.label}
; 58 | }; 59 | 60 | render() { 61 | return ( 62 | 66 | ); 67 | } 68 | } 69 | 70 | export default App; 71 | 72 | ``` 73 | 74 | -------------------------------------------------------------------------------- /data/mergedSimpleOptionsSampleCustomSize.js: -------------------------------------------------------------------------------- 1 | import { 2 | title, 3 | currentScreen, 4 | dimensions, 5 | keyColumn, 6 | font, 7 | data, 8 | columnAction, 9 | userConfiguration, 10 | pagination, 11 | rowsEdited, 12 | rowsGlobalEdited, 13 | refreshRows, 14 | isRefreshing, 15 | stripped, 16 | orderBy, 17 | newRows, 18 | rowsDeleted, 19 | searchTerm, 20 | rowsSelected, 21 | features, 22 | areFilterFieldsDisplayed, 23 | isSearchFieldDisplayed, 24 | filterTerms, 25 | filterResultForEachColumn 26 | } from "./optionsObjectSample"; 27 | 28 | const mergedSimpleOptionsSampleCustomSize = { 29 | title, 30 | currentScreen, 31 | dimensions: { 32 | ...dimensions, 33 | datatable: { 34 | ...dimensions.datatable, 35 | totalWidthNumber: 1288 36 | } 37 | }, 38 | pagination: { 39 | ...pagination, 40 | rowsCurrentPage: data.rows 41 | }, 42 | keyColumn, 43 | actions: null, 44 | refreshRows, 45 | isRefreshing, 46 | newRows, 47 | rowsDeleted, 48 | areFilterFieldsDisplayed, 49 | isSearchFieldDisplayed, 50 | filterTerms, 51 | filterResultForEachColumn, 52 | stripped, 53 | orderBy, 54 | searchTerm, 55 | font, 56 | data: { 57 | ...data, 58 | columns: [columnAction, ...data.columns] 59 | }, 60 | rowsEdited, 61 | rowsGlobalEdited, 62 | rowsSelected, 63 | features: { 64 | ...features, 65 | userConfiguration: { 66 | ...userConfiguration, 67 | columnsOrder: ["o2xpActions", ...userConfiguration.columnsOrder] 68 | }, 69 | additionalActions: [], 70 | additionalIcons: [] 71 | } 72 | }; 73 | 74 | export default mergedSimpleOptionsSampleCustomSize; 75 | -------------------------------------------------------------------------------- /test/componentsTest/DatatableHeaderTest/Widgets/AdditionalIcons.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import configureStore from "redux-mock-store"; 3 | import { Provider } from "react-redux"; 4 | import { shallow, mount } from "enzyme"; 5 | import { CallSplit as CallSplitIcon } from "@material-ui/icons"; 6 | import AdditionalIcons from "../../../../src/components/DatatableHeader/Widgets/AdditionalIcons"; 7 | import { storeSample } from "../../../../data/samples"; 8 | 9 | const onClick = jest.fn(); 10 | const additionalIcon = { 11 | title: "Coffee", 12 | icon: , 13 | onClick 14 | }; 15 | const mockStore = configureStore(); 16 | const store = mockStore({ 17 | ...storeSample, 18 | datatableReducer: { 19 | ...storeSample.datatableReducer, 20 | features: { 21 | ...storeSample.datatableReducer.features, 22 | additionalIcons: [additionalIcon] 23 | } 24 | } 25 | }); 26 | 27 | describe("SelectionIcons component", () => { 28 | it("connected should render without errors", () => { 29 | const wrapper = shallow( 30 | 31 | 32 | 33 | ); 34 | expect(wrapper.find("Connect(AdditionalIcons)")).toHaveLength(1); 35 | }); 36 | 37 | describe("should", () => { 38 | it("onClick excute the function passed", () => { 39 | const wrapper = mount( 40 | 41 | 42 | 43 | ); 44 | 45 | const additionalButton0 = wrapper.find("button.additional-icon-0"); 46 | additionalButton0.simulate("click"); 47 | expect(onClick).toHaveBeenCalled(); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /data/textReducer.js: -------------------------------------------------------------------------------- 1 | const textReducer = { 2 | search: "Toggle", 3 | searchPlaceholder: "Search..", 4 | edit: "Edit", 5 | clear: "Clear", 6 | save: "Save", 7 | delete: "Delete", 8 | confirmDelete: "Confirm delete", 9 | cancelDelete: "Cancel delete", 10 | download: "Download data", 11 | downloadTitle: "Download Data", 12 | downloadDescription: "Data will be exported in", 13 | downloadSelectedRows: "Selected rows", 14 | downloadCurrentRows: "Rows of current page", 15 | downloadAllRows: "All rows", 16 | display: "Display columns", 17 | refresh: "Refresh", 18 | configuration: "Configuration", 19 | configurationTitle: "User Configuration", 20 | configurationCopy: "Save cell's content to clipboard on click", 21 | configurationColumn: 22 | "Do you want to save the configuration of the columns and copy to clipboard feature ?", 23 | configurationReset: "Reset", 24 | configurationSave: "Save", 25 | create: "Create", 26 | createTitle: "Create a new row", 27 | createCancel: "Cancel", 28 | createSubmit: "Create", 29 | duplicate: "Duplicate", 30 | print: "Print", 31 | printTitle: "Print", 32 | printDescription: "Choose what you want to print.", 33 | orderBy: "Order by", 34 | drag: "Drag", 35 | paginationRows: "Rows", 36 | paginationPage: "Page", 37 | 38 | createPresetTitle: "Create New Preset", 39 | createPresetDescription: "Select the columns to save in the preset", 40 | createPresetTooltipText: "Create a new preset", 41 | createPresetNamingPlaceholder: "Preset name", 42 | createPresetCancelBtn: "Cancel", 43 | createPresetCreateBtn: "Create", 44 | }; 45 | 46 | export default textReducer; -------------------------------------------------------------------------------- /src/components/DatatableCore/InputTypes/SelectWrapper.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import moment from "moment"; 3 | import { Select, MenuItem, InputLabel, FormControl } from "@material-ui/core"; 4 | import { 5 | cellValPropType, 6 | rowIdPropType, 7 | columnIdPropType, 8 | setRowEditedPropType, 9 | valuesPropType, 10 | labelPropType, 11 | dateFormatPropType, 12 | requiredPropType 13 | } from "../../../proptypes"; 14 | 15 | const SelectWrapper = ({ 16 | cellVal, 17 | label, 18 | rowId, 19 | columnId, 20 | setRowEdited, 21 | values, 22 | dateFormatIn, 23 | dateFormatOut, 24 | required 25 | }) => { 26 | return ( 27 | 28 | {label} 29 | 45 | 46 | ); 47 | }; 48 | 49 | SelectWrapper.propTypes = { 50 | required: requiredPropType, 51 | label: labelPropType, 52 | cellVal: cellValPropType.isRequired, 53 | rowId: rowIdPropType.isRequired, 54 | columnId: columnIdPropType.isRequired, 55 | setRowEdited: setRowEditedPropType, 56 | values: valuesPropType.isRequired, 57 | dateFormatIn: dateFormatPropType.isRequired, 58 | dateFormatOut: dateFormatPropType.isRequired 59 | }; 60 | 61 | export default SelectWrapper; 62 | -------------------------------------------------------------------------------- /data/mergedSimpleOptionsSampleHeightResize.js: -------------------------------------------------------------------------------- 1 | import { 2 | title, 3 | currentScreen, 4 | dimensions, 5 | keyColumn, 6 | font, 7 | data, 8 | columnAction, 9 | userConfiguration, 10 | pagination, 11 | rowsEdited, 12 | rowsGlobalEdited, 13 | rowsSelected, 14 | refreshRows, 15 | isRefreshing, 16 | stripped, 17 | newRows, 18 | rowsDeleted, 19 | orderBy, 20 | searchTerm, 21 | areFilterFieldsDisplayed, 22 | isSearchFieldDisplayed, 23 | filterTerms, 24 | filterResultForEachColumn, 25 | features 26 | } from "./optionsObjectSample"; 27 | 28 | const mergedSimpleOptionsSampleHeightResize = { 29 | title, 30 | currentScreen, 31 | dimensions: { 32 | ...dimensions, 33 | datatable: { 34 | ...dimensions.datatable, 35 | height: "40vh", 36 | totalWidthNumber: 1288 37 | }, 38 | body: { 39 | heightNumber: 20 40 | } 41 | }, 42 | pagination: { 43 | ...pagination, 44 | rowsCurrentPage: data.rows 45 | }, 46 | keyColumn, 47 | actions: null, 48 | refreshRows, 49 | isRefreshing, 50 | newRows, 51 | rowsDeleted, 52 | areFilterFieldsDisplayed, 53 | isSearchFieldDisplayed, 54 | filterTerms, 55 | filterResultForEachColumn, 56 | stripped, 57 | orderBy, 58 | searchTerm, 59 | font, 60 | data: { 61 | ...data, 62 | columns: [columnAction, ...data.columns] 63 | }, 64 | rowsEdited, 65 | rowsGlobalEdited, 66 | rowsSelected, 67 | features: { 68 | ...features, 69 | userConfiguration: { 70 | ...userConfiguration, 71 | columnsOrder: ["o2xpActions", ...userConfiguration.columnsOrder] 72 | }, 73 | additionalActions: [], 74 | additionalIcons: [] 75 | } 76 | }; 77 | 78 | export default mergedSimpleOptionsSampleHeightResize; 79 | -------------------------------------------------------------------------------- /data/mergedSetRowsPerPageSample.js: -------------------------------------------------------------------------------- 1 | import { chunk } from "lodash"; 2 | import { 3 | title, 4 | currentScreen, 5 | dimensions, 6 | keyColumn, 7 | font, 8 | data, 9 | columnAction, 10 | userConfiguration, 11 | rowsEdited, 12 | rowsGlobalEdited, 13 | rowsSelected, 14 | refreshRows, 15 | isRefreshing, 16 | stripped, 17 | orderBy, 18 | searchTerm, 19 | newRows, 20 | rowsDeleted, 21 | areFilterFieldsDisplayed, 22 | isSearchFieldDisplayed, 23 | filterTerms, 24 | filterResultForEachColumn, 25 | features 26 | } from "./optionsObjectSample"; 27 | 28 | const mergedSetRowsPerPageSample = { 29 | title, 30 | currentScreen, 31 | dimensions: { 32 | ...dimensions, 33 | datatable: { 34 | ...dimensions.datatable, 35 | totalWidthNumber: 0 36 | } 37 | }, 38 | pagination: { 39 | pageSelected: 1, 40 | pageTotal: 20, 41 | rowsPerPageSelected: 10, 42 | rowsCurrentPage: chunk(data.rows, 10)[0], 43 | rowsToUse: data.rows 44 | }, 45 | keyColumn, 46 | refreshRows, 47 | isRefreshing, 48 | stripped, 49 | orderBy, 50 | searchTerm, 51 | areFilterFieldsDisplayed, 52 | isSearchFieldDisplayed, 53 | filterTerms, 54 | filterResultForEachColumn, 55 | newRows, 56 | rowsDeleted, 57 | actions: null, 58 | font, 59 | data: { 60 | ...data, 61 | columns: [columnAction, ...data.columns] 62 | }, 63 | rowsEdited, 64 | rowsGlobalEdited, 65 | rowsSelected, 66 | features: { 67 | ...features, 68 | userConfiguration: { 69 | ...userConfiguration, 70 | columnsOrder: ["o2xpActions", ...userConfiguration.columnsOrder] 71 | }, 72 | additionalActions: [], 73 | additionalIcons: [] 74 | } 75 | }; 76 | 77 | export default mergedSetRowsPerPageSample; 78 | -------------------------------------------------------------------------------- /data/mergedSimpleOptionsSampleWidthResize.js: -------------------------------------------------------------------------------- 1 | import { 2 | title, 3 | currentScreen, 4 | dimensions, 5 | keyColumn, 6 | font, 7 | data, 8 | columnAction, 9 | userConfiguration, 10 | pagination, 11 | rowsEdited, 12 | rowsGlobalEdited, 13 | rowsSelected, 14 | refreshRows, 15 | isRefreshing, 16 | stripped, 17 | orderBy, 18 | searchTerm, 19 | newRows, 20 | rowsDeleted, 21 | areFilterFieldsDisplayed, 22 | isSearchFieldDisplayed, 23 | filterTerms, 24 | filterResultForEachColumn, 25 | features 26 | } from "./optionsObjectSample"; 27 | 28 | const mergedSimpleOptionsSampleWidthResize = { 29 | title, 30 | currentScreen, 31 | dimensions: { 32 | ...dimensions, 33 | datatable: { 34 | ...dimensions.datatable, 35 | width: "90vw", 36 | widthNumber: 1800, 37 | totalWidthNumber: 1288 38 | }, 39 | columnSizeMultiplier: 1228 / 1205 40 | }, 41 | 42 | pagination: { 43 | ...pagination, 44 | rowsCurrentPage: data.rows 45 | }, 46 | keyColumn, 47 | actions: null, 48 | refreshRows, 49 | isRefreshing, 50 | newRows, 51 | rowsDeleted, 52 | areFilterFieldsDisplayed, 53 | isSearchFieldDisplayed, 54 | filterTerms, 55 | filterResultForEachColumn, 56 | stripped, 57 | orderBy, 58 | searchTerm, 59 | font, 60 | data: { 61 | ...data, 62 | columns: [columnAction, ...data.columns] 63 | }, 64 | rowsEdited, 65 | rowsGlobalEdited, 66 | rowsSelected, 67 | features: { 68 | ...features, 69 | userConfiguration: { 70 | ...userConfiguration, 71 | columnsOrder: ["o2xpActions", ...userConfiguration.columnsOrder] 72 | }, 73 | additionalActions: [], 74 | additionalIcons: [] 75 | } 76 | }; 77 | 78 | export default mergedSimpleOptionsSampleWidthResize; 79 | -------------------------------------------------------------------------------- /stories/Basics/noDataStory.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { chunk } from "lodash"; 3 | import { Datatable } from "../../src/index"; 4 | import { 5 | simpleOptionsNoDataSample, 6 | storyOptionsSample 7 | } from "../../data/samples"; 8 | 9 | const refreshRows = () => { 10 | const { rows } = storyOptionsSample.data; 11 | const randomRows = Math.floor(Math.random() * rows.length) + 1; 12 | const randomTime = Math.floor(Math.random() * 4000) + 1000; 13 | const randomResolve = Math.floor(Math.random() * 10) + 1; 14 | return new Promise((resolve, reject) => { 15 | setTimeout(() => { 16 | if (randomResolve > 3) { 17 | resolve(chunk(rows, randomRows)[0]); 18 | } 19 | reject(new Error("err")); 20 | }, randomTime); 21 | }); 22 | }; 23 | 24 | const noDataStory = () => { 25 | return ( 26 | 58 | ); 59 | }; 60 | 61 | export default noDataStory; 62 | -------------------------------------------------------------------------------- /data/mergedSimpleOptionsSampleWidthHeightResize.js: -------------------------------------------------------------------------------- 1 | import { 2 | title, 3 | currentScreen, 4 | dimensions, 5 | keyColumn, 6 | font, 7 | data, 8 | columnAction, 9 | userConfiguration, 10 | pagination, 11 | rowsEdited, 12 | rowsGlobalEdited, 13 | rowsSelected, 14 | refreshRows, 15 | isRefreshing, 16 | stripped, 17 | orderBy, 18 | searchTerm, 19 | newRows, 20 | rowsDeleted, 21 | areFilterFieldsDisplayed, 22 | isSearchFieldDisplayed, 23 | filterTerms, 24 | filterResultForEachColumn, 25 | features 26 | } from "./optionsObjectSample"; 27 | 28 | const mergedSimpleOptionsSampleWidthHeightResize = { 29 | title, 30 | currentScreen, 31 | dimensions: { 32 | ...dimensions, 33 | datatable: { 34 | width: "90vw", 35 | height: "40vh", 36 | widthNumber: 1800, 37 | totalWidthNumber: 1288 38 | }, 39 | body: { 40 | heightNumber: 20 41 | }, 42 | columnSizeMultiplier: 1228 / 1205 43 | }, 44 | pagination: { 45 | ...pagination, 46 | rowsCurrentPage: data.rows 47 | }, 48 | keyColumn, 49 | refreshRows, 50 | isRefreshing, 51 | areFilterFieldsDisplayed, 52 | isSearchFieldDisplayed, 53 | filterTerms, 54 | filterResultForEachColumn, 55 | stripped, 56 | newRows, 57 | rowsDeleted, 58 | orderBy, 59 | searchTerm, 60 | actions: null, 61 | font, 62 | data: { 63 | ...data, 64 | columns: [columnAction, ...data.columns] 65 | }, 66 | rowsEdited, 67 | rowsGlobalEdited, 68 | rowsSelected, 69 | features: { 70 | ...features, 71 | userConfiguration: { 72 | ...userConfiguration, 73 | columnsOrder: ["o2xpActions", ...userConfiguration.columnsOrder] 74 | }, 75 | additionalActions: [], 76 | additionalIcons: [] 77 | } 78 | }; 79 | 80 | export default mergedSimpleOptionsSampleWidthHeightResize; 81 | -------------------------------------------------------------------------------- /data/mergedDatableReducerRowsEdited.js: -------------------------------------------------------------------------------- 1 | import { 2 | title, 3 | currentScreen, 4 | dimensions, 5 | keyColumn, 6 | font, 7 | data, 8 | columnAction, 9 | userConfiguration, 10 | pagination, 11 | rowsSelected, 12 | rowsGlobalEdited, 13 | refreshRows, 14 | isRefreshing, 15 | stripped, 16 | searchTerm, 17 | orderBy, 18 | newRows, 19 | rowsDeleted, 20 | areFilterFieldsDisplayed, 21 | isSearchFieldDisplayed, 22 | filterTerms, 23 | filterResultForEachColumn, 24 | features 25 | } from "./optionsObjectSample"; 26 | 27 | const mergedDatableReducerRowsEdited = { 28 | title, 29 | currentScreen, 30 | dimensions: { 31 | ...dimensions, 32 | datatable: { 33 | ...dimensions.datatable, 34 | totalWidthNumber: 0 35 | } 36 | }, 37 | data: { 38 | ...data, 39 | columns: [columnAction, ...data.columns] 40 | }, 41 | pagination: { 42 | ...pagination, 43 | rowsCurrentPage: data.rows 44 | }, 45 | keyColumn, 46 | actions: null, 47 | refreshRows, 48 | isRefreshing, 49 | stripped, 50 | searchTerm, 51 | newRows, 52 | rowsDeleted, 53 | areFilterFieldsDisplayed, 54 | isSearchFieldDisplayed, 55 | filterTerms, 56 | filterResultForEachColumn, 57 | font, 58 | orderBy, 59 | rowsGlobalEdited, 60 | rowsEdited: [ 61 | { ...data.rows[0], idOfColumnErr: [], hasBeenEdited: false }, 62 | { ...data.rows[5], idOfColumnErr: [], hasBeenEdited: false }, 63 | { ...data.rows[45], idOfColumnErr: [], hasBeenEdited: false } 64 | ], 65 | rowsSelected, 66 | features: { 67 | ...features, 68 | userConfiguration: { 69 | ...userConfiguration, 70 | columnsOrder: ["o2xpActions", ...userConfiguration.columnsOrder] 71 | }, 72 | additionalActions: [], 73 | additionalIcons: [] 74 | } 75 | }; 76 | 77 | export default mergedDatableReducerRowsEdited; 78 | -------------------------------------------------------------------------------- /test/componentsTest/DatatableHeaderTest/Widgets/SelectionIcons.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import configureStore from "redux-mock-store"; 3 | import { Provider } from "react-redux"; 4 | import { shallow, mount } from "enzyme"; 5 | import SelectionIcons from "../../../../src/components/DatatableHeader/Widgets/SelectionIcons"; 6 | import { storeSample } from "../../../../data/samples"; 7 | 8 | const mockStore = configureStore(); 9 | const store = mockStore({ 10 | ...storeSample, 11 | datatableReducer: { 12 | ...storeSample.datatableReducer, 13 | rowsSelected: [storeSample.datatableReducer.data.rows[1]] 14 | } 15 | }); 16 | const storeNoExport = mockStore(storeSample); 17 | 18 | describe("SelectionIcons component", () => { 19 | it("connected should render without errors", () => { 20 | const wrapper = shallow( 21 | 22 | 23 | 24 | ); 25 | expect(wrapper.find("Connect(SelectionIcons)")).toHaveLength(1); 26 | }); 27 | 28 | describe("should", () => { 29 | it("dispatch action type SET_ROWS_SELECTED", () => { 30 | const wrapper = mount( 31 | 32 | 33 | 34 | ); 35 | 36 | const selectionButton0 = wrapper.find("button.selection-icon-0"); 37 | selectionButton0.simulate("click"); 38 | const action = store.getActions()[0]; 39 | expect(action.type).toEqual("SET_ROWS_SELECTED"); 40 | }); 41 | 42 | it("should be disabled", () => { 43 | const wrapper = mount( 44 | 45 | 46 | 47 | ); 48 | 49 | const selectionButton0 = wrapper.find("button.selection-icon-0"); 50 | expect(selectionButton0.props().disabled).toBeTruthy(); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /examples/override/datatypes.md: -------------------------------------------------------------------------------- 1 | Component example : 2 | 3 | [**Live implementation**](https://codesandbox.io/s/custom-datatype-example-for-o2xpreact-datatable-ppl29) 4 | 5 | ```jsx 6 | // ES6 7 | import { Datatable } from "@o2xp/react-datatable"; 8 | import React, { Component } from "react"; 9 | 10 | // Custom datatype Example 11 | const options = { 12 | keyColumn: 'id', 13 | data: { 14 | columns: [ 15 | { 16 | id: "id", 17 | label: "id", 18 | colSize: "80px", 19 | dataType: "text" 20 | }, 21 | { 22 | id: "name", 23 | label: "name", 24 | colSize: "150px", 25 | dataType: "name" 26 | }, 27 | { 28 | id: "age", 29 | label: "age", 30 | colSize: "50px", 31 | dataType: "number" 32 | }, 33 | ], 34 | rows: [ 35 | { 36 | id: "50cf", 37 | age: 28, 38 | name: "Kerr Mayo" 39 | }, 40 | { 41 | id: "209", 42 | age: 34, 43 | name: "Freda Bowman" 44 | }, 45 | { 46 | id: "2dd81ef", 47 | age: 14, 48 | name: "Becky Lawrence" 49 | } 50 | ], 51 | } 52 | } 53 | 54 | const customDataTypes = [ 55 | { 56 | dataType: "text", 57 | component: cellVal =>
{cellVal}
58 | }, 59 | { 60 | dataType: "name", 61 | component: cellVal =>
{cellVal}
62 | } 63 | ]; 64 | 65 | class App extends Component { 66 | render() { 67 | return ; 68 | } 69 | } 70 | 71 | export default App; 72 | ``` 73 | -------------------------------------------------------------------------------- /data/customTableBodyRowSample.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | rowPropType, 4 | columnsOrderPropType, 5 | indexPropType, 6 | heightNumberPropType, 7 | columnSizeMultiplierPropType 8 | } from "../src/proptypes"; 9 | import { simpleOptionsSample } from "./samples"; 10 | 11 | const customTableBodyRowSample = ({ 12 | row, 13 | columnsOrder, 14 | rowIndex, 15 | columnSizeMultiplier, 16 | height 17 | }) => { 18 | const { columns } = simpleOptionsSample.data; 19 | const columnAction = { 20 | id: "o2xpActions", 21 | label: "Actions", 22 | colSize: "150px", 23 | editable: false 24 | }; 25 | return ( 26 |
34 | {columnsOrder.map(columnId => { 35 | const column = 36 | columnId === "o2xpActions" 37 | ? columnAction 38 | : columns.find(col => col.id === columnId); 39 | const width = `${( 40 | column.colSize.split("px")[0] * columnSizeMultiplier 41 | ).toString()}px`; 42 | return ( 43 |
50 |
56 | {row[columnId]} 57 |
58 |
59 | ); 60 | })} 61 |
62 | ); 63 | }; 64 | 65 | customTableBodyRowSample.propTypes = { 66 | row: rowPropType, 67 | columnsOrder: columnsOrderPropType, 68 | rowIndex: indexPropType, 69 | height: heightNumberPropType, 70 | columnSizeMultiplier: columnSizeMultiplierPropType 71 | }; 72 | 73 | export default customTableBodyRowSample; 74 | -------------------------------------------------------------------------------- /test/componentsTest/DatatableCoreTest/HeaderTest/Header.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import configureStore from "redux-mock-store"; 3 | import { Provider } from "react-redux"; 4 | import { shallow, mount } from "enzyme"; 5 | import { ScrollSync, ScrollSyncPane } from "react-scroll-sync"; 6 | import Header from "../../../../src/components/DatatableCore/Header/Header"; 7 | import HeaderRow from "../../../../src/components/DatatableCore/Header/HeaderRow"; 8 | import { 9 | storeNoCustomComponentsSample, 10 | storeCustomTableHeaderRowComponentSample 11 | } from "../../../../data/samples"; 12 | 13 | const mockStore = configureStore(); 14 | const store = mockStore(storeNoCustomComponentsSample); 15 | const storeCustomComponent = mockStore( 16 | storeCustomTableHeaderRowComponentSample 17 | ); 18 | 19 | describe("Header component", () => { 20 | it("connected should render without errors", () => { 21 | const wrapper = shallow( 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | ); 30 | expect(wrapper.find("Connect(Header)")).toHaveLength(1); 31 | }); 32 | 33 | it("should create a header with 1 row", () => { 34 | const wrapper = mount( 35 | 36 | 37 | 38 |
39 | 40 | 41 | 42 | ); 43 | 44 | expect(wrapper.find(HeaderRow)).toHaveLength(1); 45 | }); 46 | 47 | it("should create a body with custom row", () => { 48 | const wrapper = mount( 49 | 50 | 51 | 52 |
53 | 54 | 55 | 56 | ); 57 | 58 | expect(wrapper.find(".Table-Row")).toHaveLength(1); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /examples/override/bodyCell.md: -------------------------------------------------------------------------------- 1 | Component example : 2 | 3 | [**Live implementation**](https://codesandbox.io/s/body-cell-override-example-for-o2xpreact-datatable-12rof) 4 | 5 | ```jsx 6 | // ES6 7 | import { Datatable } from "@o2xp/react-datatable"; 8 | import React, { Component } from "react"; 9 | 10 | // Custom table body cell Example 11 | const options = { 12 | keyColumn: "id", 13 | data: { 14 | columns: [ 15 | { 16 | id: "id", 17 | label: "id", 18 | colSize: "80px", 19 | dataType: "text" 20 | }, 21 | { 22 | id: "name", 23 | label: "name", 24 | colSize: "150px", 25 | dataType: "name" 26 | }, 27 | { 28 | id: "age", 29 | label: "age", 30 | colSize: "50px", 31 | dataType: "number" 32 | } 33 | ], 34 | rows: [ 35 | { 36 | id: "50cf", 37 | age: 28, 38 | name: "Kerr Mayo" 39 | }, 40 | { 41 | id: "209", 42 | age: 34, 43 | name: "Freda Bowman" 44 | }, 45 | { 46 | id: "2dd81ef", 47 | age: 14, 48 | name: "Becky Lawrence" 49 | } 50 | ] 51 | } 52 | }; 53 | 54 | class App extends Component { 55 | buildCustomTableBodyCell = ({ cellVal, column, rowId }) => { 56 | let val; 57 | switch (column.dataType) { 58 | case "boolean": 59 | if (cellVal) { 60 | val =
Yes
; 61 | } else { 62 | val =
No
; 63 | } 64 | break; 65 | default: 66 | val =
{cellVal}
; 67 | break; 68 | } 69 | return val; 70 | }; 71 | 72 | render() { 73 | return ( 74 | 78 | ); 79 | } 80 | } 81 | 82 | export default App; 83 | 84 | ``` 85 | -------------------------------------------------------------------------------- /src/redux/reducers/textReducer.js: -------------------------------------------------------------------------------- 1 | const defaultState = { 2 | search: "Global searching", 3 | filter: "Toggle filtering", 4 | searchPlaceholder: "Search..", 5 | edit: "Edit", 6 | clear: "Clear", 7 | save: "Save", 8 | delete: "Delete", 9 | confirmDelete: "Confirm delete", 10 | cancelDelete: "Cancel delete", 11 | download: "Download data", 12 | downloadTitle: "Download Data", 13 | downloadDescription: "Data will be exported in", 14 | downloadSelectedRows: "Selected rows", 15 | downloadCurrentRows: "Rows of current page", 16 | downloadAllRows: "All rows", 17 | display: "Display columns", 18 | refresh: "Refresh", 19 | configuration: "Configuration", 20 | configurationTitle: "User Configuration", 21 | configurationCopy: "Save cell's content to clipboard on click", 22 | configurationColumn: 23 | "Do you want to save the configuration of the columns and copy to clipboard feature ?", 24 | configurationReset: "Reset", 25 | configurationSave: "Save", 26 | create: "Create", 27 | createTitle: "Create a new row", 28 | createCancel: "Cancel", 29 | createSubmit: "Create", 30 | duplicate: "Duplicate", 31 | print: "Print", 32 | printTitle: "Print", 33 | printDescription: "Choose what you want to print.", 34 | orderBy: "Order by", 35 | drag: "Drag", 36 | paginationRows: "Rows", 37 | paginationPage: "Page", 38 | 39 | createPresetTitle: "Create New Preset", 40 | createPresetDescription: "Select the columns to save in the preset", 41 | createPresetTooltipText: "Create a new preset", 42 | createPresetNamingPlaceholder: "Preset name", 43 | createPresetCancelBtn: "Cancel", 44 | createPresetCreateBtn: "Create" 45 | }; 46 | 47 | const initText = (state, payload) => ({ ...state, ...payload }); 48 | 49 | const textReducer = (state = defaultState, action) => { 50 | const { payload, type } = action; 51 | switch (type) { 52 | case "INIT_TEXT": 53 | return initText(state, payload); 54 | default: 55 | return state; 56 | } 57 | }; 58 | 59 | export default textReducer; 60 | -------------------------------------------------------------------------------- /test/componentsTest/DatatableCoreTest/HeaderTest/HeaderColumnsFilterBar.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow, mount } from "enzyme"; 3 | import configureStore from "redux-mock-store"; 4 | import { Provider } from "react-redux"; 5 | import { storeSample } from "../../../../data/samples"; 6 | import HeaderColumnsFilterBar from "../../../../src/components/DatatableCore/Header/HeaderColumnsFilterBar"; 7 | 8 | const mockStore = configureStore(); 9 | const store = mockStore({ 10 | ...storeSample, 11 | datatableReducer: { 12 | ...storeSample.datatableReducer, 13 | features: { 14 | ...storeSample.datatableReducer.features 15 | } 16 | } 17 | }); 18 | 19 | const column = { 20 | id: "id", 21 | label: "id", 22 | colSize: "200px", 23 | editable: false, 24 | required: true, 25 | dataType: "text", 26 | valueVerification: val => { 27 | const error = val === "whatever"; 28 | const message = val === "whatever" ? "Value is not valid" : ""; 29 | return { 30 | error, 31 | message 32 | }; 33 | } 34 | }; 35 | 36 | const filterInColumn = jest.fn(); 37 | 38 | describe("HeaderColumnsFilterBar component should filter", () => { 39 | it("should render component connect", () => { 40 | const wrapper = shallow( 41 | 42 | 48 | 49 | ); 50 | expect(wrapper.find("Connect(HeaderColumnsFilterBar)")).toHaveLength(1); 51 | }); 52 | 53 | it("a column based on a string", () => { 54 | const wrapper = mount( 55 | 56 | 62 | 63 | ); 64 | expect(wrapper.text()).toEqual(""); 65 | }); 66 | 67 | }); 68 | -------------------------------------------------------------------------------- /test/componentsTest/DatatableCoreTest/InputTypesTest/CreateInput.test.js: -------------------------------------------------------------------------------- 1 | import { shallow, mount } from "enzyme"; 2 | import { Select, Checkbox } from "@material-ui/core"; 3 | import CreateInput from "../../../../src/components/DatatableCore/InputTypes/CreateInput"; 4 | 5 | const setRowEdited = jest.fn(); 6 | const value = { 7 | cellVal: "Morgan Dubois", 8 | rowId: "5cd9307025f4f0572995990f", 9 | columnId: "name", 10 | values: ["John Doe", "Jahn Dae", "Jyhn Dye"], 11 | type: "text", 12 | setRowEdited: ({ rowId, columnId, newValue }) => 13 | setRowEdited({ rowId, columnId, newValue }) 14 | }; 15 | 16 | describe("CreateInput should render a", () => { 17 | it("DatePicker", () => { 18 | const wrapper = shallow(CreateInput({ ...value, inputType: "datePicker" })); 19 | expect(wrapper.find("DatePickerWrapper")).toHaveLength(1); 20 | }); 21 | it("TimePicker", () => { 22 | const wrapper = shallow(CreateInput({ ...value, inputType: "timePicker" })); 23 | expect(wrapper.find("TimePickerWrapper")).toHaveLength(1); 24 | }); 25 | it("DateTimePicker", () => { 26 | const wrapper = shallow( 27 | CreateInput({ ...value, inputType: "dateTimePicker" }) 28 | ); 29 | expect(wrapper.find("DateTimePickerWrapper")).toHaveLength(1); 30 | }); 31 | it("Select", () => { 32 | const wrapper = mount( 33 | CreateInput({ ...value, cellVal: "John Doe", inputType: "select" }) 34 | ); 35 | expect(wrapper.find(Select)).toHaveLength(1); 36 | }); 37 | it("Checkbox", () => { 38 | const wrapper = mount( 39 | CreateInput({ ...value, cellVal: true, inputType: "boolean" }) 40 | ); 41 | expect(wrapper.find(Checkbox)).toHaveLength(1); 42 | }); 43 | it("TextField", () => { 44 | const wrapper = shallow(CreateInput({ ...value, inputType: "input" })); 45 | expect(wrapper.find("TextFieldWrapper")).toHaveLength(1); 46 | }); 47 | it("TextField on default", () => { 48 | const wrapper = shallow(CreateInput(value)); 49 | expect(wrapper.find("TextFieldWrapper")).toHaveLength(1); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /data/defaultOptionsSample.js: -------------------------------------------------------------------------------- 1 | const defaultOptionsSample = { 2 | title: "", 3 | currentScreen: "", 4 | dimensions: { 5 | datatable: { 6 | width: "100%", 7 | height: "100%", 8 | widthNumber: 0, 9 | totalWidthNumber: 0 10 | }, 11 | header: { 12 | height: "0px", 13 | heightNumber: 0 14 | }, 15 | body: { 16 | heightNumber: 0 17 | }, 18 | row: { 19 | height: "33px", 20 | heightNumber: 0 21 | }, 22 | columnSizeMultiplier: 1, 23 | isScrolling: false 24 | }, 25 | keyColumn: null, 26 | font: "Roboto", 27 | data: { 28 | columns: [], 29 | rows: [] 30 | }, 31 | rowsEdited: [], 32 | rowsGlobalEdited: [], 33 | rowsSelected: [], 34 | newRows: [], 35 | rowsDeleted: [], 36 | actions: null, 37 | refreshRows: null, 38 | areFilterFieldsDisplayed: false, 39 | isSearchFieldDisplayed: false, 40 | isRefreshing: false, 41 | stripped: false, 42 | searchTerm: "", 43 | filterTerms: {}, 44 | filterResultForEachColumn: {}, 45 | orderBy: [], 46 | pagination: { 47 | pageSelected: 1, 48 | pageTotal: 1, 49 | rowsPerPageSelected: "", 50 | rowsCurrentPage: [], 51 | rowsToUse: [] 52 | }, 53 | features: { 54 | canEdit: false, 55 | canAdd: false, 56 | canCreatePreset: false, 57 | canEditRow: null, 58 | canGlobalEdit: false, 59 | canPrint: false, 60 | canDownload: false, 61 | canDuplicate: false, 62 | canDelete: false, 63 | canSearch: false, 64 | canRefreshRows: false, 65 | canSelectRow: false, 66 | canOrderColumns: false, 67 | canSaveUserConfiguration: false, 68 | columnsPresetsToDisplay: [], 69 | localStoragePresets: [], 70 | editableIdNewRow: [], 71 | userConfiguration: { 72 | columnsOrder: [], 73 | copyToClipboard: false 74 | }, 75 | rowsPerPage: { 76 | available: [10, 25, 50, 100, "All"], 77 | selected: "All" 78 | }, 79 | additionalActions: [], 80 | additionalIcons: [], 81 | selectionIcons: [] 82 | } 83 | }; 84 | 85 | export default defaultOptionsSample; 86 | -------------------------------------------------------------------------------- /test/componentsTest/DatatableCoreTest/InputTypesTest/SelectWrapper.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from "enzyme"; 2 | import { Select } from "@material-ui/core"; 3 | import SelectWrapper from "../../../../src/components/DatatableCore/InputTypes/SelectWrapper"; 4 | 5 | const setRowEdited = jest.fn(); 6 | const selectValue = { 7 | cellVal: "green", 8 | rowId: "5cd9307025f4f0572995990f", 9 | columnId: "eyeColor", 10 | setRowEdited: ({ rowId, columnId, newValue }) => 11 | setRowEdited({ rowId, columnId, newValue }), 12 | values: ["green", "blue", "brown"] 13 | }; 14 | 15 | const selectValueDateFormat = { 16 | cellVal: "2017-06-02T11:22", 17 | rowId: "5cd9307025f4f0572995990f", 18 | columnId: "birthDate", 19 | setRowEdited: ({ rowId, columnId, newValue }) => 20 | setRowEdited({ rowId, columnId, newValue }), 21 | values: ["2017-06-02T11:22", "1944-12-08T04:35", "1965-02-12T18:38"], 22 | dateFormat: "YYYY-MM-DDTHH:mm" 23 | }; 24 | 25 | describe("Select wrapper", () => { 26 | it("should render a Select", () => { 27 | const wrapper = mount(SelectWrapper(selectValue)); 28 | expect(wrapper.find(Select)).toHaveLength(1); 29 | }); 30 | 31 | it("should call setRowEdited onChange", () => { 32 | const wrapper = mount(SelectWrapper(selectValue)); 33 | wrapper 34 | .find(Select) 35 | .props() 36 | .onChange({ target: { value: "brown" } }); 37 | const { rowId, columnId } = selectValue; 38 | expect(setRowEdited).toHaveBeenCalled(); 39 | expect(setRowEdited).toHaveBeenCalledWith({ 40 | rowId, 41 | columnId, 42 | newValue: "brown" 43 | }); 44 | }); 45 | 46 | it("should call setRowEdited onChange with formated date", () => { 47 | const wrapper = mount(SelectWrapper(selectValueDateFormat)); 48 | wrapper 49 | .find(Select) 50 | .props() 51 | .onChange({ target: { value: "1944-12-08T04:35" } }); 52 | const { rowId, columnId } = selectValueDateFormat; 53 | expect(setRowEdited).toHaveBeenCalled(); 54 | expect(setRowEdited).toHaveBeenCalledWith({ 55 | rowId, 56 | columnId, 57 | newValue: "1944-12-08T04:35" 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/components/DatatableHeader/Widgets/Filter.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from "react"; 2 | import { connect } from "react-redux"; 3 | import { IconButton, Tooltip, Zoom } from "@material-ui/core"; 4 | import { FilterList as FilterIcon } from "@material-ui/icons"; 5 | import { 6 | rowsPropType, 7 | isRefreshingPropType, 8 | textPropType, 9 | toggleFilterFieldsDisplayPropType 10 | } from "../../../proptypes"; 11 | import { toggleFilterFieldsDisplay as toggleFilterFieldsDisplayAction } from "../../../redux/actions/datatableActions"; 12 | 13 | // TODO: rename 14 | export class Filter extends Component { 15 | render() { 16 | const { rows, isRefreshing, filterText } = this.props; 17 | const disabled = rows.length === 0 || isRefreshing; 18 | return ( 19 | 20 | 25 | 26 | { 29 | const { toggleFilterFieldsDisplay } = this.props; 30 | toggleFilterFieldsDisplay(); 31 | }} 32 | disabled={disabled} 33 | > 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | } 41 | } 42 | 43 | Filter.propTypes = { 44 | rows: rowsPropType.isRequired, 45 | isRefreshing: isRefreshingPropType.isRequired, 46 | filterText: textPropType, 47 | toggleFilterFieldsDisplay: toggleFilterFieldsDisplayPropType.isRequired 48 | }; 49 | 50 | const mapDispatchToProps = dispatch => { 51 | return { 52 | toggleFilterFieldsDisplay: () => dispatch(toggleFilterFieldsDisplayAction()) 53 | }; 54 | }; 55 | 56 | const mapStateToProps = state => { 57 | return { 58 | isRefreshing: state.datatableReducer.isRefreshing, 59 | rows: state.datatableReducer.data.rows, 60 | filterText: state.textReducer.filter 61 | }; 62 | }; 63 | 64 | export default connect(mapStateToProps, mapDispatchToProps)(Filter); 65 | -------------------------------------------------------------------------------- /test/componentsTest/Loader.test.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import { ScrollSyncPane } from "react-scroll-sync"; 3 | import { PulseLoader } from "react-spinners"; 4 | import Loader from "../../src/components/Loader"; 5 | 6 | describe("Loader component should render ", () => { 7 | it("without errors", () => { 8 | const LoaderWrapper = Loader({ 9 | height: 500, 10 | width: 100, 11 | columnSizeMultiplier: 1, 12 | totalWidthNumber: 1000 13 | }); 14 | expect(LoaderWrapper).toEqual( 15 | 16 |
17 | 18 |
19 | 20 |
28 |
33 | . 34 |
35 |
36 |
37 |
38 | ); 39 | }); 40 | 41 | it("without errors with columnSizeMultiplier", () => { 42 | const LoaderWrapper = Loader({ 43 | height: 500, 44 | width: 100, 45 | columnSizeMultiplier: 1.5, 46 | totalWidthNumber: 1000 47 | }); 48 | expect(LoaderWrapper).toEqual( 49 | 50 |
51 | 52 |
53 | 54 |
62 |
67 | . 68 |
69 |
70 |
71 |
72 | ); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /data/mergedMinimumOptionsSample.js: -------------------------------------------------------------------------------- 1 | import { 2 | currentScreen, 3 | dimensions, 4 | keyColumn, 5 | font, 6 | data, 7 | userConfiguration, 8 | pagination, 9 | rowsEdited, 10 | rowsGlobalEdited, 11 | rowsSelected, 12 | refreshRows, 13 | isRefreshing, 14 | stripped, 15 | searchTerm, 16 | newRows, 17 | rowsDeleted, 18 | orderBy, 19 | rowsPerPage, 20 | areFilterFieldsDisplayed, 21 | isSearchFieldDisplayed, 22 | filterTerms, 23 | filterResultForEachColumn 24 | } from "./optionsObjectSample"; 25 | 26 | const mergedMinimumOptionsSample = { 27 | title: "", 28 | currentScreen, 29 | dimensions: { 30 | ...dimensions, 31 | datatable: { 32 | width: "100vw", 33 | height: "100vh", 34 | widthNumber: 1024, 35 | totalWidthNumber: 0 36 | }, 37 | header: { height: "0px", heightNumber: 0 }, 38 | body: { 39 | heightNumber: 648 40 | } 41 | }, 42 | rowsEdited, 43 | rowsGlobalEdited, 44 | rowsSelected, 45 | refreshRows, 46 | isRefreshing, 47 | newRows, 48 | rowsDeleted, 49 | stripped, 50 | searchTerm, 51 | actions: null, 52 | keyColumn, 53 | data, 54 | orderBy, 55 | font, 56 | areFilterFieldsDisplayed, 57 | isSearchFieldDisplayed, 58 | filterTerms, 59 | filterResultForEachColumn, 60 | pagination: { 61 | ...pagination, 62 | rowsCurrentPage: data.rows 63 | }, 64 | features: { 65 | canEdit: false, 66 | canEditRow: null, 67 | canGlobalEdit: false, 68 | canAdd: false, 69 | canPrint: false, 70 | canDownload: false, 71 | canDuplicate: false, 72 | canSearch: false, 73 | canDelete: false, 74 | canSelectRow: false, 75 | localStoragePresets: null, 76 | canCreatePreset: false, 77 | columnsPresetsToDisplay: [], 78 | canRefreshRows: false, 79 | canOrderColumns: false, 80 | canSaveUserConfiguration: false, 81 | editableIdNewRow: [], 82 | userConfiguration: { 83 | ...userConfiguration, 84 | columnsOrder: [...userConfiguration.columnsOrder] 85 | }, 86 | rowsPerPage, 87 | additionalActions: [], 88 | additionalIcons: [], 89 | selectionIcons: [] 90 | } 91 | }; 92 | export default mergedMinimumOptionsSample; 93 | -------------------------------------------------------------------------------- /src/components/DatatableCore/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import { ScrollSyncPane } from "react-scroll-sync"; 4 | import { 5 | columnsOrderPropType, 6 | columnSizeMultiplierPropType, 7 | widthNumberPropType, 8 | CustomTableHeaderRowPropType, 9 | customPropsPropType 10 | } from "../../../proptypes"; 11 | import HeaderRow from "./HeaderRow"; 12 | 13 | class Header extends Component { 14 | headerRowBuilder = () => { 15 | const { 16 | columnsOrder, 17 | CustomTableHeaderRow, 18 | columnSizeMultiplier, 19 | widthDatatable, 20 | customProps 21 | } = this.props; 22 | 23 | if (CustomTableHeaderRow !== null) { 24 | return ( 25 |
32 | 37 |
38 | ); 39 | } 40 | return ; 41 | }; 42 | 43 | render() { 44 | return {this.headerRowBuilder()}; 45 | } 46 | } 47 | 48 | Header.propTypes = { 49 | customProps: customPropsPropType, 50 | columnsOrder: columnsOrderPropType.isRequired, 51 | columnSizeMultiplier: columnSizeMultiplierPropType.isRequired, 52 | widthDatatable: widthNumberPropType.isRequired, 53 | CustomTableHeaderRow: CustomTableHeaderRowPropType 54 | }; 55 | 56 | const mapStateToProps = state => { 57 | return { 58 | customProps: state.customComponentsReducer.customProps, 59 | columnsOrder: 60 | state.datatableReducer.features.userConfiguration.columnsOrder, 61 | widthDatatable: state.datatableReducer.dimensions.datatable.widthNumber, 62 | columnSizeMultiplier: 63 | state.datatableReducer.dimensions.columnSizeMultiplier, 64 | CustomTableHeaderRow: state.customComponentsReducer.CustomTableHeaderRow 65 | }; 66 | }; 67 | 68 | export default connect(mapStateToProps)(Header); 69 | -------------------------------------------------------------------------------- /src/components/DatatableHeader/Widgets/SelectionIcons.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from "react"; 2 | import { connect } from "react-redux"; 3 | import { IconButton, Tooltip, Zoom } from "@material-ui/core"; 4 | import { 5 | selectionIconsPropType, 6 | rowsSelectedPropType, 7 | setRowsSelectedPropType 8 | } from "../../../proptypes"; 9 | import { setRowsSelected as setRowsSelectedAction } from "../../../redux/actions/datatableActions"; 10 | 11 | class SelectionIcons extends Component { 12 | render() { 13 | const { rowsSelected, selectionIcons, setRowsSelected } = this.props; 14 | const disabled = rowsSelected.length === 0; 15 | return ( 16 | 17 | {selectionIcons.map((icon, i) => ( 18 | 24 | 25 | { 32 | icon.onClick(rowsSelected); 33 | setRowsSelected(); 34 | }} 35 | disabled={disabled} 36 | > 37 | {icon.icon} 38 | 39 | 40 | 41 | ))} 42 | 43 | ); 44 | } 45 | } 46 | 47 | SelectionIcons.propTypes = { 48 | rowsSelected: rowsSelectedPropType.isRequired, 49 | selectionIcons: selectionIconsPropType.isRequired, 50 | setRowsSelected: setRowsSelectedPropType 51 | }; 52 | 53 | const mapDispatchToProps = dispatch => { 54 | return { 55 | setRowsSelected: () => dispatch(setRowsSelectedAction([])) 56 | }; 57 | }; 58 | 59 | const mapStateToProps = state => { 60 | return { 61 | rowsSelected: state.datatableReducer.rowsSelected, 62 | selectionIcons: state.datatableReducer.features.selectionIcons 63 | }; 64 | }; 65 | 66 | export default connect(mapStateToProps, mapDispatchToProps)(SelectionIcons); 67 | -------------------------------------------------------------------------------- /src/components/DatatableCore/Header/HeaderColumnsFilterBar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { TextField } from "@material-ui/core"; 3 | import { connect } from "react-redux"; 4 | import { 5 | columnPropType, 6 | isRefreshingPropType, 7 | filterInColumnPropType, 8 | filterTermsPropType 9 | } from "../../../proptypes"; 10 | import { filterInColumn as filterInColumnAction } from "../../../redux/actions/datatableActions"; 11 | 12 | export class HeaderColumnsFilterBar extends Component { 13 | getFilterBarValueFromStore = () => { 14 | const { filterTerms, column } = this.props; 15 | if (filterTerms[column.id]) { 16 | return filterTerms[column.id]; 17 | } 18 | return ""; 19 | }; 20 | 21 | render() { 22 | const { column, isRefreshing, filterInColumn } = this.props; 23 | return ( 24 | { 28 | filterInColumn([e.target.value, column.id]); 29 | }} 30 | disabled={isRefreshing} 31 | value={this.getFilterBarValueFromStore()} 32 | /> 33 | ); 34 | } 35 | } 36 | 37 | HeaderColumnsFilterBar.propTypes = { 38 | column: columnPropType.isRequired, 39 | isRefreshing: isRefreshingPropType.isRequired, 40 | filterInColumn: filterInColumnPropType, 41 | filterTerms: filterTermsPropType.isRequired 42 | }; 43 | 44 | const mapDispatchToProps = dispatch => { 45 | return { 46 | // TODO: Dispatch an action by sending "the search text" and "the column to search in" to the store 47 | filterInColumn: (searchText, column) => 48 | dispatch(filterInColumnAction(searchText, column)) 49 | }; 50 | }; 51 | 52 | const mapStateToProps = state => { 53 | return { 54 | filterTerms: state.datatableReducer.filterTerms, 55 | canOrderColumns: state.datatableReducer.features.canOrderColumns, 56 | areFilterFieldsDisplayed: state.datatableReducer.areFilterFieldsDisplayed, 57 | isRefreshing: state.datatableReducer.isRefreshing, 58 | orderBy: state.datatableReducer.orderBy, 59 | orderByText: state.textReducer.orderBy, 60 | dragText: state.textReducer.drag, 61 | isScrolling: state.datatableReducer.dimensions.isScrolling 62 | }; 63 | }; 64 | 65 | export default connect( 66 | mapStateToProps, 67 | mapDispatchToProps 68 | )(HeaderColumnsFilterBar); 69 | -------------------------------------------------------------------------------- /examples/override/headerRow.md: -------------------------------------------------------------------------------- 1 | Component example : 2 | 3 | [**Live implementation**](https://codesandbox.io/s/header-row-override-example-for-o2xpreact-datatable-ssf72) 4 | 5 | ```jsx 6 | // ES6 7 | import { Datatable } from "@o2xp/react-datatable"; 8 | import React, { Component } from "react"; 9 | 10 | // Custom table header row Example 11 | const options = { 12 | keyColumn: "id", 13 | data: { 14 | columns: [ 15 | { 16 | id: "id", 17 | label: "id", 18 | colSize: "80px", 19 | dataType: "text" 20 | }, 21 | { 22 | id: "name", 23 | label: "name", 24 | colSize: "150px", 25 | dataType: "name" 26 | }, 27 | { 28 | id: "age", 29 | label: "age", 30 | colSize: "50px", 31 | dataType: "number" 32 | } 33 | ], 34 | rows: [ 35 | { 36 | id: "50cf", 37 | age: 28, 38 | name: "Kerr Mayo" 39 | }, 40 | { 41 | id: "209", 42 | age: 34, 43 | name: "Freda Bowman" 44 | }, 45 | { 46 | id: "2dd81ef", 47 | age: 14, 48 | name: "Becky Lawrence" 49 | } 50 | ] 51 | } 52 | }; 53 | 54 | class App extends Component { 55 | buildCustomTableHeaderRow = ({ columnsOrder, columnSizeMultiplier }) => { 56 | let columns = options.data.columns; 57 | const columnAction = { 58 | id: "o2xpActions", 59 | label: "Actions", 60 | colSize: "150px", 61 | editable: false 62 | }; 63 | return ( 64 |
65 | {columnsOrder.map(columnId => { 66 | let column = 67 | columnId === "o2xpActions" 68 | ? columnAction 69 | : columns.find(col => col.id === columnId); 70 | const width = `${( 71 | column.colSize.split("px")[0] * columnSizeMultiplier 72 | ).toString()}px`; 73 | return ( 74 |
75 |
{column.label}
76 |
77 | ); 78 | })} 79 |
80 | ); 81 | }; 82 | 83 | render() { 84 | return ( 85 | 89 | ); 90 | } 91 | } 92 | 93 | export default App; 94 | 95 | ``` 96 | -------------------------------------------------------------------------------- /data/storyOptionsSample.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | CallSplit as CallSplitIcon, 4 | Launch as LaunchIcon, 5 | FreeBreakfast as CoffeeIcon 6 | } from "@material-ui/icons"; 7 | import { title, keyColumn, data } from "./optionsObjectSample"; 8 | import rows from "./rows"; 9 | const storyOptionsSample = { 10 | title, 11 | currentScreen: "testScreen1", 12 | dimensions: { 13 | datatable: { 14 | width: "100%", 15 | height: "70vh" 16 | } 17 | }, 18 | keyColumn, 19 | data: { 20 | ...data, 21 | rows 22 | }, 23 | features: { 24 | canEdit: true, 25 | canDelete: true, 26 | canPrint: true, 27 | canDownload: true, 28 | canSearch: true, 29 | canFilter: true, 30 | canRefreshRows: true, 31 | canOrderColumns: true, 32 | canSelectRow: true, 33 | canCreatePreset: true, 34 | canSaveUserConfiguration: true, 35 | columnsPresetsToDisplay: [ 36 | { presetName:"Show blue columns", columnsToShow:["id","name","age"], isActive:false, type:"predefinedPreset" }, 37 | { presetName:"Show one columns", columnsToShow:["age"], isActive:false, type:"predefinedPreset" }, 38 | { presetName:"Show something ", columnsToShow:["id","name","age","adult","birthDate","eyeColor","iban" ] , isActive:false, type:"predefinedPreset" }, 39 | { presetName:"Show nothing ", columnsToShow:[], isActive:false, type:"predefinedPreset" } 40 | ], 41 | userConfiguration: { 42 | columnsOrder: [ 43 | "id", 44 | "name", 45 | "age", 46 | "adult", 47 | "birthDate", 48 | "eyeColor", 49 | "iban" 50 | ], 51 | copyToClipboard: true 52 | }, 53 | selectionIcons: [ 54 | { 55 | title: "Action 1", 56 | icon: , 57 | onClick: res => alert(`You have dispatched ${res.length} rows !`) 58 | }, 59 | { 60 | title: "Action 2", 61 | icon: , 62 | onClick: res => alert(`You have exported ${res.length} rows !`) 63 | } 64 | ], 65 | additionalActions: [ 66 | { 67 | title: "Action 3", 68 | icon: , 69 | onClick: res => alert(res) 70 | } 71 | ], 72 | additionalIcons: [ 73 | { 74 | title: "Action 3", 75 | icon: , 76 | onClick: () => alert("Coffee Time") 77 | } 78 | ] 79 | } 80 | }; 81 | export default storyOptionsSample; -------------------------------------------------------------------------------- /data/storyOptionsSample2.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | CallSplit as CallSplitIcon, 4 | Launch as LaunchIcon, 5 | FreeBreakfast as CoffeeIcon 6 | } from "@material-ui/icons"; 7 | import { title, keyColumn, data } from "./optionsObjectSample"; 8 | import rows from "./rows"; 9 | const storyOptionsSample2 = { 10 | title, 11 | currentScreen: "testScreen2", 12 | dimensions: { 13 | datatable: { 14 | width: "100%", 15 | height: "70vh" 16 | } 17 | }, 18 | keyColumn, 19 | data: { 20 | ...data, 21 | rows 22 | }, 23 | features: { 24 | canEdit: true, 25 | canDelete: true, 26 | canPrint: true, 27 | canDownload: true, 28 | canSearch: true, 29 | canFilter: true, 30 | canRefreshRows: true, 31 | canOrderColumns: true, 32 | canSelectRow: true, 33 | canCreatePreset: true, 34 | canSaveUserConfiguration: true, 35 | columnsPresetsToDisplay: [ 36 | { presetName:"Show blue columns", columnsToShow:["id","name","age"], isActive:false, type:"predefinedPreset" }, 37 | { presetName:"Show one columns", columnsToShow:["age"], isActive:false, type:"predefinedPreset" }, 38 | { presetName:"Show something ", columnsToShow:["id","name","age","adult","birthDate","eyeColor","iban" ] , isActive:false, type:"predefinedPreset" }, 39 | { presetName:"Show nothing ", columnsToShow:[], isActive:false, type:"predefinedPreset" } 40 | ], 41 | userConfiguration: { 42 | columnsOrder: [ 43 | "id", 44 | "name", 45 | "age", 46 | "adult", 47 | "birthDate", 48 | "eyeColor", 49 | "iban" 50 | ], 51 | copyToClipboard: true 52 | }, 53 | selectionIcons: [ 54 | { 55 | title: "Action 1", 56 | icon: , 57 | onClick: res => alert(`You have dispatched ${res.length} rows !`) 58 | }, 59 | { 60 | title: "Action 2", 61 | icon: , 62 | onClick: res => alert(`You have exported ${res.length} rows !`) 63 | } 64 | ], 65 | additionalActions: [ 66 | { 67 | title: "Action 3", 68 | icon: , 69 | onClick: res => alert(res) 70 | } 71 | ], 72 | additionalIcons: [ 73 | { 74 | title: "Action 3", 75 | icon: , 76 | onClick: () => alert("Coffee Time") 77 | } 78 | ] 79 | } 80 | }; 81 | export default storyOptionsSample2; -------------------------------------------------------------------------------- /data/mergedMaximumOptionsSample.js: -------------------------------------------------------------------------------- 1 | import { chunk } from "lodash"; 2 | import { 3 | currentScreen, 4 | dimensions, 5 | keyColumn, 6 | font, 7 | data, 8 | additionalIcons, 9 | rowsEdited, 10 | rowsGlobalEdited, 11 | rowsSelected, 12 | columnAction, 13 | refreshRows, 14 | isRefreshing, 15 | stripped, 16 | searchTerm, 17 | newRows, 18 | rowsDeleted, 19 | orderBy, 20 | selectionIcons, 21 | additionalActions, 22 | areFilterFieldsDisplayed, 23 | isSearchFieldDisplayed, 24 | filterTerms, 25 | filterResultForEachColumn 26 | } from "./optionsObjectSample"; 27 | 28 | const mergedMaximumOptionsSample = { 29 | title: "My super datatable", 30 | currentScreen, 31 | dimensions: { 32 | ...dimensions, 33 | datatable: { 34 | ...dimensions.datatable, 35 | width: "500px", 36 | widthNumber: 500, 37 | totalWidthNumber: 0 38 | } 39 | }, 40 | rowsEdited, 41 | rowsGlobalEdited, 42 | rowsSelected, 43 | refreshRows, 44 | isRefreshing, 45 | newRows, 46 | rowsDeleted, 47 | stripped, 48 | searchTerm, 49 | areFilterFieldsDisplayed, 50 | isSearchFieldDisplayed, 51 | filterTerms, 52 | filterResultForEachColumn, 53 | actions: null, 54 | keyColumn, 55 | font, 56 | orderBy, 57 | data: { 58 | ...data, 59 | columns: [{ ...columnAction, colSize: "250px" }, ...data.columns] 60 | }, 61 | pagination: { 62 | pageSelected: 1, 63 | pageTotal: 4, 64 | rowsPerPageSelected: 50, 65 | rowsCurrentPage: chunk(data.rows, 50)[0], 66 | rowsToUse: data.rows 67 | }, 68 | features: { 69 | canEdit: true, 70 | canEditRow: null, 71 | canGlobalEdit: false, 72 | canAdd: false, 73 | canPrint: true, 74 | canDownload: true, 75 | canDuplicate: true, 76 | canSearch: true, 77 | localStoragePresets: null, 78 | canFilter: true, 79 | canDelete: true, 80 | canRefreshRows: true, 81 | canSelectRow: true, 82 | canOrderColumns: true, 83 | canCreatePreset: false, 84 | columnsPresetsToDisplay: [], 85 | canSaveUserConfiguration: true, 86 | editableIdNewRow: [], 87 | userConfiguration: { 88 | columnsOrder: ["o2xpActions", "id", "name", "age"], 89 | copyToClipboard: true 90 | }, 91 | rowsPerPage: { 92 | available: [50], 93 | selected: 50 94 | }, 95 | additionalActions, 96 | additionalIcons, 97 | selectionIcons 98 | } 99 | }; 100 | 101 | export default mergedMaximumOptionsSample; 102 | -------------------------------------------------------------------------------- /test/componentsTest/DatatableCoreTest/HeaderTest/HeaderActionsCell.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import configureStore from "redux-mock-store"; 3 | import { Provider } from "react-redux"; 4 | import { shallow, mount } from "enzyme"; 5 | import HeaderActionsCell, { 6 | HeaderActionsCell as HeaderActionsCellPureComponent 7 | } from "../../../../src/components/DatatableCore/Header/HeaderActionsCell"; 8 | import { storeSample } from "../../../../data/samples"; 9 | 10 | const mockStore = configureStore(); 11 | const store = mockStore(storeSample); 12 | 13 | const column = { 14 | id: "o2xpActions", 15 | label: "Actions", 16 | colSize: "150px", 17 | editable: false 18 | }; 19 | 20 | describe("HeaderActionsCell component", () => { 21 | it("connected should render without errors", () => { 22 | const wrapper = mount( 23 | 24 | 25 | 26 | ); 27 | expect(wrapper.find("Connect(HeaderActionsCell)")).toHaveLength(1); 28 | }); 29 | 30 | describe("pure Component should render a div", () => { 31 | it("without .scrolling-shadow when no scrolling", () => { 32 | const setRowsGlobalSelected = jest.fn(); 33 | const wrapper = shallow( 34 | 39 | ); 40 | expect(wrapper.find(".Table-Header-Cell.action")).toHaveLength(1); 41 | }); 42 | 43 | it("with .scrolling-shadow when scrolling", () => { 44 | const setRowsGlobalSelected = jest.fn(); 45 | const wrapper = shallow( 46 | 52 | ); 53 | expect( 54 | wrapper.find(".Table-Header-Cell.action.scrolling-shadow") 55 | ).toHaveLength(1); 56 | }); 57 | 58 | it("click on checkbox", () => { 59 | const setRowsGlobalSelected = jest.fn(); 60 | const wrapper = shallow( 61 | 68 | ); 69 | const checkbox = wrapper.find(".select-all"); 70 | checkbox.simulate("change", { target: { checked: true } }); 71 | expect(setRowsGlobalSelected).toHaveBeenCalled(); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from "react"; 2 | import { Provider } from "react-redux"; 3 | import DatatableInitializer from "./components/DatatableInitializer"; 4 | import "./app.css"; 5 | import { cloneDeep } from "lodash"; 6 | import { SnackbarProvider } from "notistack"; 7 | import { createStore, applyMiddleware } from "redux"; 8 | import thunk from "redux-thunk"; 9 | import reducers from "./redux/reducers/reducers"; 10 | 11 | 12 | 13 | 14 | class Datatable extends Component { 15 | constructor(props) { 16 | super(props) 17 | this.store = createStore(reducers, applyMiddleware(thunk)); 18 | } 19 | 20 | render() { 21 | const { 22 | options = {}, 23 | forceRerender = false, 24 | actions = null, 25 | refreshRows = null, 26 | stripped = false, 27 | customProps = null, 28 | CustomTableBodyCell = null, 29 | CustomTableBodyRow = null, 30 | CustomTableHeaderCell = null, 31 | CustomTableHeaderRow = null, 32 | customDataTypes = [], 33 | text = {}, 34 | theme = {} 35 | } = this.props; 36 | 37 | if (options.data && !options.keyColumn) { 38 | console.log("@o2xp/react-datatable : You forgot to give keyColumn.."); 39 | } 40 | 41 | if ((!options.data || 42 | !options.data.columns || 43 | options.data.columns.length === 0) && 44 | options.keyColumn) { 45 | console.log("@o2xp/react-datatable : You forgot to give data.."); 46 | } 47 | 48 | 49 | if (!options.data && !options.keyColumn) { 50 | console.log("@o2xp/react-datatable : You forgot to give data and keyColumn.."); 51 | } 52 | 53 | 54 | return ( 55 | <> 56 | {options.data && 57 | options.data.columns && 58 | options.data.columns.length > 0 && 59 | options.keyColumn && ( 60 | 61 | 62 | 77 | 78 | 79 | )} 80 | 81 | ); 82 | } 83 | } 84 | 85 | export { Datatable }; -------------------------------------------------------------------------------- /examples/override/bodyRow.md: -------------------------------------------------------------------------------- 1 | Component example : 2 | 3 | [**Live implementation**](https://codesandbox.io/s/body-row-override-example-for-o2xpreact-datatable-56sc1) 4 | 5 | ```jsx 6 | // ES6 7 | import { Datatable } from "@o2xp/react-datatable"; 8 | import React, { Component } from "react"; 9 | 10 | // Custom table body row Example 11 | const options = { 12 | keyColumn: "id", 13 | data: { 14 | columns: [ 15 | { 16 | id: "id", 17 | label: "id", 18 | colSize: "80px", 19 | dataType: "text" 20 | }, 21 | { 22 | id: "name", 23 | label: "name", 24 | colSize: "150px", 25 | dataType: "name" 26 | }, 27 | { 28 | id: "age", 29 | label: "age", 30 | colSize: "50px", 31 | dataType: "number" 32 | } 33 | ], 34 | rows: [ 35 | { 36 | id: "50cf", 37 | age: 28, 38 | name: "Kerr Mayo" 39 | }, 40 | { 41 | id: "209", 42 | age: 34, 43 | name: "Freda Bowman" 44 | }, 45 | { 46 | id: "2dd81ef", 47 | age: 14, 48 | name: "Becky Lawrence" 49 | } 50 | ] 51 | } 52 | }; 53 | 54 | class App extends Component { 55 | buildCustomTableBodyRow = ({ 56 | row, 57 | columnsOrder, 58 | rowIndex, 59 | columnSizeMultiplier, 60 | height 61 | }) => { 62 | let columns = options.data.columns; 63 | const columnAction = { 64 | id: "o2xpActions", 65 | label: "Actions", 66 | colSize: "150px", 67 | editable: false 68 | }; 69 | return ( 70 |
76 | {columnsOrder.map(columnId => { 77 | let column = 78 | columnId === "o2xpActions" 79 | ? columnAction 80 | : columns.find(col => col.id === columnId); 81 | const width = `${( 82 | column.colSize.split("px")[0] * columnSizeMultiplier 83 | ).toString()}px`; 84 | return ( 85 |
92 |
98 | {row[columnId]} 99 |
100 |
101 | ); 102 | })} 103 |
104 | ); 105 | }; 106 | 107 | render() { 108 | return ( 109 | 113 | ); 114 | } 115 | } 116 | 117 | export default App; 118 | ``` 119 | -------------------------------------------------------------------------------- /src/components/Notifier.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import React, { Component } from "react"; 3 | import { connect } from "react-redux"; 4 | import { withSnackbar } from "notistack"; 5 | import { IconButton, withStyles } from "@material-ui/core"; 6 | import { Close as CloseIcon } from "@material-ui/icons"; 7 | import { customVariant } from "./MuiTheme"; 8 | import { 9 | removeSnackbar as removeSnackbarAction, 10 | closeSnackbar as closeSnackbarAction 11 | } from "../redux/actions/notifierActions"; 12 | 13 | export class Notifier extends Component { 14 | displayed = []; 15 | 16 | shouldComponentUpdate({ notifications: newSnacks = [] }) { 17 | if (!newSnacks.length) { 18 | this.displayed = []; 19 | return false; 20 | } 21 | 22 | const { 23 | notifications: currentSnacks, 24 | closeSnackbar, 25 | removeSnackbar 26 | } = this.props; 27 | let notExists = false; 28 | for (let i = 0; i < newSnacks.length; i += 1) { 29 | const newSnack = newSnacks[i]; 30 | if (newSnack.dismissed) { 31 | closeSnackbar(newSnack.key); 32 | removeSnackbar(newSnack.key); 33 | } 34 | 35 | if (!notExists) { 36 | notExists = 37 | notExists || 38 | !currentSnacks.filter(({ key }) => newSnack.key === key).length; 39 | } 40 | } 41 | return notExists; 42 | } 43 | 44 | componentDidUpdate() { 45 | const { 46 | notifications = [], 47 | classes, 48 | enqueueSnackbar, 49 | closeSnackbarFunc, 50 | removeSnackbar 51 | } = this.props; 52 | 53 | notifications.forEach(({ key, message, options = {} }) => { 54 | if (this.displayed.includes(key)) return; 55 | enqueueSnackbar(message, { 56 | ...options, 57 | action: () => ( 58 | closeSnackbarFunc(key)} 61 | > 62 | 63 | 64 | ), 65 | onClose: (event, reason) => { 66 | if (options.onClose) { 67 | options.onClose(event, reason, key); 68 | } 69 | removeSnackbar(key); 70 | } 71 | }); 72 | this.storeDisplayed(key); 73 | }); 74 | } 75 | 76 | storeDisplayed = id => { 77 | this.displayed = [...this.displayed, id]; 78 | }; 79 | 80 | render() { 81 | return null; 82 | } 83 | } 84 | 85 | const mapDispatchToProps = dispatch => { 86 | return { 87 | removeSnackbar: key => dispatch(removeSnackbarAction(key)), 88 | closeSnackbarFunc: key => dispatch(closeSnackbarAction(key)) 89 | }; 90 | }; 91 | 92 | const mapStateToProps = state => ({ 93 | notifications: state.notifierReducer.notifications 94 | }); 95 | 96 | export default withSnackbar( 97 | connect( 98 | mapStateToProps, 99 | mapDispatchToProps 100 | )(withStyles(customVariant)(Notifier)) 101 | ); 102 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import configureStore from "redux-mock-store"; 3 | import { Provider } from "react-redux"; 4 | import { mount } from "enzyme"; 5 | import { Datatable } from "../src/index"; 6 | import { storeSample, simpleOptionsSample } from "../data/samples"; 7 | 8 | const mockStore = configureStore(); 9 | const store = mockStore(storeSample); 10 | const refreshRows = jest.fn(); 11 | const mockConsole = jest.fn(); 12 | 13 | describe("Datatable component", () => { 14 | beforeEach(() => { 15 | console.log = mockConsole; 16 | }); 17 | 18 | it("should render DatatableInitializer", () => { 19 | const div = document.createElement("div"); 20 | window.domNode = div; 21 | document.body.appendChild(div); 22 | 23 | const wrapper = mount( 24 | 25 | 38 | , 39 | { attachTo: window.domNode } 40 | ); 41 | 42 | expect(wrapper.find("div#no-data").hostNodes()).toHaveLength(0); 43 | expect(wrapper.find("div#no-keyColumn").hostNodes()).toHaveLength(0); 44 | expect( 45 | wrapper.find("div#no-data-and-no-keyColumn").hostNodes() 46 | ).toHaveLength(0); 47 | expect(wrapper.find("DatatableInitializer")).toHaveLength(1); 48 | }); 49 | 50 | it("with missing data and keyColumn should render div error", () => { 51 | const wrapper2 = mount( 52 | 53 | 54 | 55 | ); 56 | 57 | expect(wrapper2.find("DatatableInitializer")).toHaveLength(0); 58 | expect(mockConsole).toHaveBeenCalled(); 59 | expect(mockConsole).toHaveBeenCalledWith( 60 | "@o2xp/react-datatable : You forgot to give data and keyColumn.." 61 | ); 62 | }); 63 | 64 | it("with missing data should render div error", () => { 65 | const wrapper3 = mount( 66 | 67 | 68 | 69 | ); 70 | 71 | expect(wrapper3.find("DatatableInitializer")).toHaveLength(0); 72 | expect(mockConsole).toHaveBeenCalled(); 73 | expect(mockConsole).toHaveBeenCalledWith( 74 | "@o2xp/react-datatable : You forgot to give data.." 75 | ); 76 | }); 77 | 78 | it("with missing keyColumn should render div error", () => { 79 | const wrapper4 = mount( 80 | 81 | 82 | 83 | ); 84 | 85 | expect(wrapper4.find("DatatableInitializer")).toHaveLength(0); 86 | expect(mockConsole).toHaveBeenCalled(); 87 | expect(mockConsole).toHaveBeenCalledWith( 88 | "@o2xp/react-datatable : You forgot to give data and keyColumn.." 89 | ); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /src/components/DatatableCore/CellTypes.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { Checkbox } from "@material-ui/core"; 4 | import moment from "moment"; 5 | import CreateInput from "./InputTypes/CreateInput"; 6 | 7 | export const NumberWrapper = styled.div` 8 | text-align: center; 9 | `; 10 | 11 | export const NumberType = properties => { 12 | const { cellVal, editing } = properties; 13 | const type = "number"; 14 | if (editing) { 15 | return CreateInput({ ...properties, type }); 16 | } 17 | 18 | return ( 19 | 20 | {cellVal.toString().replace(/\B(? 22 | ); 23 | }; 24 | 25 | export const TextWrapper = styled.div` 26 | text-align: center; 27 | `; 28 | 29 | export const TextType = properties => { 30 | const { cellVal, editing } = properties; 31 | const type = "text"; 32 | if (editing) { 33 | return CreateInput({ ...properties, type }); 34 | } 35 | return {cellVal}; 36 | }; 37 | 38 | export const BooleanWrapper = styled.div` 39 | text-align: center; 40 | `; 41 | 42 | export const BooleanType = properties => { 43 | const { editing, cellVal, inputType = "boolean" } = properties; 44 | if (editing) { 45 | return CreateInput({ ...properties, inputType }); 46 | } 47 | return ( 48 | 49 | 55 | 56 | ); 57 | }; 58 | 59 | export const DateWrapper = styled.div` 60 | text-align: left; 61 | `; 62 | 63 | export const DateType = properties => { 64 | const { 65 | cellVal, 66 | editing, 67 | inputType = "datePicker", 68 | dateFormatIn, 69 | dateFormatOut 70 | } = properties; 71 | if (editing) { 72 | return CreateInput({ 73 | ...properties, 74 | inputType 75 | }); 76 | } 77 | 78 | return ( 79 | 80 | {moment(cellVal, dateFormatIn).format(dateFormatOut)} 81 | 82 | ); 83 | }; 84 | 85 | export const TimeWrapper = styled.div` 86 | text-align: left; 87 | `; 88 | 89 | export const TimeType = properties => { 90 | const { 91 | cellVal, 92 | editing, 93 | inputType = "timePicker", 94 | dateFormatIn, 95 | dateFormatOut 96 | } = properties; 97 | if (editing) { 98 | return CreateInput({ 99 | ...properties, 100 | inputType 101 | }); 102 | } 103 | return ( 104 | 105 | {moment(cellVal, dateFormatIn).format(dateFormatOut)} 106 | 107 | ); 108 | }; 109 | 110 | export const DateTimeWrapper = styled.div` 111 | text-align: left; 112 | `; 113 | 114 | export const DateTimeType = properties => { 115 | const { 116 | cellVal, 117 | editing, 118 | inputType = "dateTimePicker", 119 | dateFormatIn, 120 | dateFormatOut 121 | } = properties; 122 | if (editing) { 123 | return CreateInput({ 124 | ...properties, 125 | inputType 126 | }); 127 | } 128 | return ( 129 | 130 | {moment(cellVal, dateFormatIn).format(dateFormatOut)} 131 | 132 | ); 133 | }; 134 | -------------------------------------------------------------------------------- /test/reduxTest/reducersTest/notifierReducer.test.js: -------------------------------------------------------------------------------- 1 | import notifierReducer from "../../../src/redux/reducers/notifierReducer"; 2 | 3 | const key = new Date().getTime() + Math.random(); 4 | const notification = { 5 | message: "Refresh error.", 6 | key, 7 | options: { 8 | key, 9 | variant: "error" 10 | } 11 | }; 12 | 13 | const key2 = new Date().getTime() + Math.random(); 14 | const notification2 = { 15 | message: "Refresh error.", 16 | key: key2, 17 | options: { 18 | key: key2, 19 | variant: "info" 20 | } 21 | }; 22 | describe("notifierReducer reducer", () => { 23 | it("should return the initial state", () => { 24 | expect(notifierReducer(undefined, {})).toEqual({ notifications: [] }); 25 | }); 26 | 27 | describe("should handle ENQUEUE_SNACKBAR", () => { 28 | it("new notification", () => { 29 | const result = notifierReducer(undefined, { 30 | type: "ENQUEUE_SNACKBAR", 31 | payload: notification 32 | }); 33 | 34 | expect(result).toEqual({ notifications: [notification] }); 35 | }); 36 | 37 | it("with multiple notification", () => { 38 | let result = notifierReducer(undefined, { 39 | type: "ENQUEUE_SNACKBAR", 40 | payload: notification 41 | }); 42 | result = notifierReducer(result, { 43 | type: "ENQUEUE_SNACKBAR", 44 | payload: notification2 45 | }); 46 | 47 | expect(result).toEqual({ notifications: [notification, notification2] }); 48 | }); 49 | }); 50 | 51 | describe("should handle CLOSE_SNACKBAR", () => { 52 | it("with one notification", () => { 53 | const result = notifierReducer( 54 | { notifications: [notification] }, 55 | { 56 | type: "CLOSE_SNACKBAR", 57 | payload: key 58 | } 59 | ); 60 | 61 | const resultExpected = { 62 | notifications: [{ ...notification, dismissed: true }] 63 | }; 64 | 65 | expect(result).toEqual(resultExpected); 66 | }); 67 | it("with multiple notifications", () => { 68 | const result = notifierReducer( 69 | { notifications: [notification, notification2] }, 70 | { 71 | type: "CLOSE_SNACKBAR", 72 | payload: key2 73 | } 74 | ); 75 | 76 | const resultExpected = { 77 | notifications: [notification, { ...notification2, dismissed: true }] 78 | }; 79 | 80 | expect(result).toEqual(resultExpected); 81 | }); 82 | }); 83 | describe("should handle REMOVE_SNACKBAR", () => { 84 | it("with one notification", () => { 85 | const result = notifierReducer( 86 | { notifications: [notification] }, 87 | { 88 | type: "REMOVE_SNACKBAR", 89 | payload: key 90 | } 91 | ); 92 | 93 | const resultExpected = { 94 | notifications: [] 95 | }; 96 | 97 | expect(result).toEqual(resultExpected); 98 | }); 99 | it("with multiple notifications", () => { 100 | const result = notifierReducer( 101 | { notifications: [notification, notification2] }, 102 | { 103 | type: "REMOVE_SNACKBAR", 104 | payload: key2 105 | } 106 | ); 107 | 108 | const resultExpected = { 109 | notifications: [notification] 110 | }; 111 | 112 | expect(result).toEqual(resultExpected); 113 | }); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /data/samples.js: -------------------------------------------------------------------------------- 1 | import storeSample from "./storeSample"; 2 | import storyOptionsSample from "./storyOptionsSample"; 3 | import storyOptionsSample2 from "./storyOptionsSample2"; 4 | import storyOptionsNoActionSample from "./storyOptionsNoActionSample"; 5 | import storeSampleWithPages from "./storeSampleWithPages"; 6 | import storeNoCustomComponentsSample from "./storeNoCustomComponentsSample"; 7 | import storeCustomTableBodyCellComponentSample from "./storeCustomTableBodyCellComponentSample"; 8 | import storeCustomTableBodyRowComponentSample from "./storeCustomTableBodyRowComponentSample"; 9 | import storeCustomTableHeaderCellComponentSample from "./storeCustomTableHeaderCellComponentSample"; 10 | import storeCustomTableHeaderRowComponentSample from "./storeCustomTableHeaderRowComponentSample"; 11 | import storeNoDataSample from "./storeNoDataSample"; 12 | import storeNoRowsDataSample from "./storeNoRowsDataSample"; 13 | import simpleOptionsSample from "./simpleOptionsSample"; 14 | import simpleOptionsNoDataSample from "./simpleOptionsNoDataSample"; 15 | import maximumOptionsSample from "./maximumOptionsSample"; 16 | import minimumOptionsSample from "./minimumOptionsSample"; 17 | import defaultOptionsSample from "./defaultOptionsSample"; 18 | import mergedPageSample from "./mergedPageSample"; 19 | import mergedDatableReducerRowsEdited from "./mergedDatableReducerRowsEdited"; 20 | import mergedSimpleOptionsSample from "./mergedSimpleOptionsSample"; 21 | import mergedSimpleOptionsSampleCustomSize from "./mergedSimpleOptionsSampleCustomSize"; 22 | import mergedSimpleOptionsSampleWidthResize from "./mergedSimpleOptionsSampleWidthResize"; 23 | import mergedSimpleOptionsSampleHeightResize from "./mergedSimpleOptionsSampleHeightResize"; 24 | import mergedSimpleOptionsSampleWidthHeightResize from "./mergedSimpleOptionsSampleWidthHeightResize"; 25 | import mergedSetRowsPerPageSample from "./mergedSetRowsPerPageSample"; 26 | import mergedMaximumOptionsSample from "./mergedMaximumOptionsSample"; 27 | import mergedMinimumOptionsSample from "./mergedMinimumOptionsSample"; 28 | import customTableBodyRowSample from "./customTableBodyRowSample"; 29 | import customTableBodyCellSample from "./customTableBodyCellSample"; 30 | import customTableHeaderRowSample from "./customTableHeaderRowSample"; 31 | import customTableHeaderCellSample from "./customTableHeaderCellSample"; 32 | import customDataTypesSample from "./customDataTypesSample"; 33 | 34 | export { 35 | storeSample, 36 | storyOptionsSample, 37 | storyOptionsSample2, 38 | storyOptionsNoActionSample, 39 | storeSampleWithPages, 40 | storeNoCustomComponentsSample, 41 | storeCustomTableBodyCellComponentSample, 42 | storeCustomTableBodyRowComponentSample, 43 | storeCustomTableHeaderCellComponentSample, 44 | storeCustomTableHeaderRowComponentSample, 45 | storeNoDataSample, 46 | storeNoRowsDataSample, 47 | simpleOptionsSample, 48 | simpleOptionsNoDataSample, 49 | maximumOptionsSample, 50 | minimumOptionsSample, 51 | defaultOptionsSample, 52 | mergedPageSample, 53 | mergedDatableReducerRowsEdited, 54 | mergedSimpleOptionsSample, 55 | mergedSimpleOptionsSampleCustomSize, 56 | mergedSimpleOptionsSampleWidthResize, 57 | mergedSimpleOptionsSampleHeightResize, 58 | mergedSimpleOptionsSampleWidthHeightResize, 59 | mergedSetRowsPerPageSample, 60 | mergedMaximumOptionsSample, 61 | mergedMinimumOptionsSample, 62 | customTableBodyRowSample, 63 | customTableBodyCellSample, 64 | customTableHeaderRowSample, 65 | customTableHeaderCellSample, 66 | customDataTypesSample 67 | }; 68 | -------------------------------------------------------------------------------- /src/components/DatatableCore/InputTypes/DatePickerWrapper.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import equal from "fast-deep-equal"; 3 | import { 4 | ClickAwayListener, 5 | Tooltip, 6 | Zoom, 7 | IconButton, 8 | InputAdornment, 9 | withStyles 10 | } from "@material-ui/core"; 11 | import { Event as CalendarIcon } from "@material-ui/icons"; 12 | import { DatePicker } from "@material-ui/pickers"; 13 | import { checkValue, setValue } from "./PickersFunction"; 14 | import { customVariant } from "../../MuiTheme"; 15 | import { dateFormatUser } from "../../../moment.config"; 16 | import { 17 | valueVerificationPropType, 18 | cellValPropType, 19 | labelPropType, 20 | classesPropType, 21 | requiredPropType 22 | } from "../../../proptypes"; 23 | 24 | export class DatePickerWrapper extends Component { 25 | constructor(props) { 26 | super(props); 27 | this.state = { 28 | error: false, 29 | tooltipOpen: false, 30 | message: "" 31 | }; 32 | } 33 | 34 | componentDidMount() { 35 | const { valueVerification } = this.props; 36 | if (valueVerification) { 37 | const newState = checkValue({ 38 | ...this.props, 39 | mounting: true 40 | }); 41 | if (!equal(this.state, newState)) { 42 | this.setState(newState); 43 | } 44 | } 45 | } 46 | 47 | onDateChange = date => { 48 | const newState = setValue({ 49 | ...this.props, 50 | date 51 | }); 52 | if (!equal(this.state, newState)) { 53 | this.setState(newState); 54 | } 55 | }; 56 | 57 | toggleTooltip = open => { 58 | const { error } = this.state; 59 | if (error) { 60 | this.setState({ tooltipOpen: open }); 61 | } 62 | }; 63 | 64 | render() { 65 | const { cellVal, label, classes, required } = this.props; 66 | const { tooltipOpen, message, error } = this.state; 67 | 68 | return ( 69 | this.toggleTooltip(false)}> 70 | 80 |
81 | this.setState({ tooltipOpen: false })} 87 | format={dateFormatUser} 88 | InputProps={{ 89 | endAdornment: ( 90 | 91 | 92 | 93 | 94 | 95 | ) 96 | }} 97 | helperText={null} 98 | value={cellVal === "" ? null : cellVal} 99 | onChange={this.onDateChange} 100 | /> 101 |
102 |
103 |
104 | ); 105 | } 106 | } 107 | 108 | DatePickerWrapper.propTypes = { 109 | required: requiredPropType, 110 | label: labelPropType, 111 | classes: classesPropType.isRequired, 112 | cellVal: cellValPropType.isRequired, 113 | valueVerification: valueVerificationPropType 114 | }; 115 | 116 | export default withStyles(customVariant)(DatePickerWrapper); 117 | -------------------------------------------------------------------------------- /src/components/DatatableCore/InputTypes/TimePickerWrapper.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import equal from "fast-deep-equal"; 3 | import { 4 | ClickAwayListener, 5 | Tooltip, 6 | Zoom, 7 | IconButton, 8 | InputAdornment, 9 | withStyles 10 | } from "@material-ui/core"; 11 | import { AccessTime as TimeIcon } from "@material-ui/icons"; 12 | import { TimePicker } from "@material-ui/pickers"; 13 | import { checkValue, setValue } from "./PickersFunction"; 14 | import { customVariant } from "../../MuiTheme"; 15 | import { timeFormatUser } from "../../../moment.config"; 16 | import { 17 | valueVerificationPropType, 18 | cellValPropType, 19 | labelPropType, 20 | classesPropType, 21 | requiredPropType 22 | } from "../../../proptypes"; 23 | 24 | export class TimePickerWrapper extends Component { 25 | constructor(props) { 26 | super(props); 27 | this.state = { 28 | error: false, 29 | tooltipOpen: false, 30 | message: "" 31 | }; 32 | } 33 | 34 | componentDidMount() { 35 | const { valueVerification } = this.props; 36 | if (valueVerification) { 37 | const newState = checkValue({ 38 | ...this.props, 39 | mounting: true 40 | }); 41 | if (!equal(this.state, newState)) { 42 | this.setState(newState); 43 | } 44 | } 45 | } 46 | 47 | onDateChange = date => { 48 | const newState = setValue({ 49 | ...this.props, 50 | date 51 | }); 52 | if (!equal(this.state, newState)) { 53 | this.setState(newState); 54 | } 55 | }; 56 | 57 | toggleTooltip = open => { 58 | const { error } = this.state; 59 | if (error) { 60 | this.setState({ tooltipOpen: open }); 61 | } 62 | }; 63 | 64 | render() { 65 | const { cellVal, classes, label, required } = this.props; 66 | const { tooltipOpen, message, error } = this.state; 67 | return ( 68 | this.toggleTooltip(false)}> 69 | 79 |
80 | this.setState({ tooltipOpen: false })} 87 | InputProps={{ 88 | endAdornment: ( 89 | 90 | 91 | 92 | 93 | 94 | ) 95 | }} 96 | helperText={null} 97 | value={cellVal === "" ? null : cellVal} 98 | onChange={this.onDateChange} 99 | /> 100 |
101 |
102 |
103 | ); 104 | } 105 | } 106 | 107 | TimePickerWrapper.propTypes = { 108 | required: requiredPropType, 109 | label: labelPropType, 110 | classes: classesPropType.isRequired, 111 | cellVal: cellValPropType.isRequired, 112 | valueVerification: valueVerificationPropType 113 | }; 114 | 115 | export default withStyles(customVariant)(TimePickerWrapper); 116 | -------------------------------------------------------------------------------- /test/componentsTest/DatatableCoreTest/InputTypesTest/PickersFunction.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | checkValue, 3 | setValue 4 | } from "../../../../src/components/DatatableCore/InputTypes/PickersFunction"; 5 | import { moment } from "../../../../src/moment.config"; 6 | 7 | const setRowEdited = jest.fn(); 8 | const valueVerification = val => { 9 | let error; 10 | let message; 11 | switch (true) { 12 | case val > 100: 13 | error = true; 14 | message = "Value is too big"; 15 | break; 16 | default: 17 | error = false; 18 | message = ""; 19 | break; 20 | } 21 | return { 22 | error, 23 | message 24 | }; 25 | }; 26 | const goodCheckValue = { 27 | cellVal: 10, 28 | mounting: false, 29 | valueVerification 30 | }; 31 | 32 | const errorCheckValue = { 33 | cellVal: 101, 34 | mounting: false, 35 | valueVerification 36 | }; 37 | 38 | const defaultSetValue = { 39 | value: 10, 40 | rowId: "5cd9307025f4f0572995990f", 41 | columnId: "age", 42 | setRowEdited 43 | }; 44 | 45 | const dateSetValue = { 46 | date: moment().format("YYYY-MM-DDTHH:mm"), 47 | dateFormat: "YYYY-MM-DDTHH:mm", 48 | rowId: "5cd9307025f4f0572995990f", 49 | columnId: "age", 50 | setRowEdited 51 | }; 52 | 53 | describe("PickersFunction", () => { 54 | describe("checkValue", () => { 55 | it("with good value", () => { 56 | const res = checkValue(goodCheckValue); 57 | const expectedRes = { 58 | message: "", 59 | error: false, 60 | tooltipOpen: false 61 | }; 62 | expect(res).toEqual(expectedRes); 63 | }); 64 | 65 | it("with error value", () => { 66 | const res = checkValue(errorCheckValue); 67 | const expectedRes = { 68 | message: "Value is too big", 69 | error: true, 70 | tooltipOpen: true 71 | }; 72 | expect(res).toEqual(expectedRes); 73 | }); 74 | 75 | it("with good value while mouting", () => { 76 | const res = checkValue({ ...goodCheckValue, mounting: true }); 77 | const expectedRes = { 78 | message: "", 79 | error: false, 80 | tooltipOpen: false 81 | }; 82 | expect(res).toEqual(expectedRes); 83 | }); 84 | 85 | it("with error value while mouting", () => { 86 | const res = checkValue({ ...errorCheckValue, mounting: true }); 87 | const expectedRes = { 88 | message: "Value is too big", 89 | error: true, 90 | tooltipOpen: false 91 | }; 92 | expect(res).toEqual(expectedRes); 93 | }); 94 | }); 95 | 96 | describe("setValue", () => { 97 | it("with default value", () => { 98 | const res = setValue(defaultSetValue); 99 | const expectedRes = { 100 | message: "", 101 | error: false, 102 | tooltipOpen: false 103 | }; 104 | expect(res).toEqual(expectedRes); 105 | }); 106 | 107 | it("with default value and valueVerification", () => { 108 | const res = setValue({ ...defaultSetValue, valueVerification }); 109 | const expectedRes = { 110 | message: "", 111 | error: false, 112 | tooltipOpen: false 113 | }; 114 | expect(res).toEqual(expectedRes); 115 | }); 116 | 117 | it("with default value and without valueVerification", () => { 118 | const res = setValue(dateSetValue); 119 | const expectedRes = { 120 | message: "", 121 | error: false, 122 | tooltipOpen: false 123 | }; 124 | expect(res).toEqual(expectedRes); 125 | }); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /src/components/DatatableCore/InputTypes/DateTimePickerWrapper.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import equal from "fast-deep-equal"; 3 | import { 4 | ClickAwayListener, 5 | Tooltip, 6 | Zoom, 7 | IconButton, 8 | InputAdornment, 9 | withStyles 10 | } from "@material-ui/core"; 11 | import { Event as CalendarIcon } from "@material-ui/icons"; 12 | import { DateTimePicker } from "@material-ui/pickers"; 13 | import { checkValue, setValue } from "./PickersFunction"; 14 | import { customVariant } from "../../MuiTheme"; 15 | import { dateTimeFormatUser } from "../../../moment.config"; 16 | import { 17 | valueVerificationPropType, 18 | cellValPropType, 19 | labelPropType, 20 | classesPropType, 21 | requiredPropType 22 | } from "../../../proptypes"; 23 | 24 | export class DateTimePickerWrapper extends Component { 25 | constructor(props) { 26 | super(props); 27 | this.state = { 28 | error: false, 29 | tooltipOpen: false, 30 | message: "" 31 | }; 32 | } 33 | 34 | componentDidMount() { 35 | const { valueVerification } = this.props; 36 | if (valueVerification) { 37 | const newState = checkValue({ 38 | ...this.props, 39 | mounting: true 40 | }); 41 | if (!equal(this.state, newState)) { 42 | this.setState(newState); 43 | } 44 | } 45 | } 46 | 47 | onDateChange = date => { 48 | const newState = setValue({ 49 | ...this.props, 50 | date 51 | }); 52 | if (!equal(this.state, newState)) { 53 | this.setState(newState); 54 | } 55 | }; 56 | 57 | toggleTooltip = open => { 58 | const { error } = this.state; 59 | if (error) { 60 | this.setState({ tooltipOpen: open }); 61 | } 62 | }; 63 | 64 | render() { 65 | const { cellVal, classes, label, required } = this.props; 66 | const { tooltipOpen, message, error } = this.state; 67 | 68 | return ( 69 | this.toggleTooltip(false)}> 70 | 80 |
81 | this.setState({ tooltipOpen: false })} 88 | format={dateTimeFormatUser} 89 | InputProps={{ 90 | endAdornment: ( 91 | 92 | 93 | 94 | 95 | 96 | ) 97 | }} 98 | helperText={null} 99 | value={cellVal === "" ? null : cellVal} 100 | onChange={this.onDateChange} 101 | /> 102 |
103 |
104 |
105 | ); 106 | } 107 | } 108 | 109 | DateTimePickerWrapper.propTypes = { 110 | required: requiredPropType, 111 | label: labelPropType, 112 | classes: classesPropType.isRequired, 113 | cellVal: cellValPropType.isRequired, 114 | valueVerification: valueVerificationPropType 115 | }; 116 | 117 | export default withStyles(customVariant)(DateTimePickerWrapper); 118 | -------------------------------------------------------------------------------- /test/componentsTest/DatatableHeaderTest/Widgets/CreatePreset.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import configureStore from "redux-mock-store"; 3 | import { Provider } from "react-redux"; 4 | import { shallow, mount } from "enzyme"; 5 | import { storeSample } from "../../../../data/samples"; 6 | import CreatePreset, { 7 | CreatePreset as CreatePresetPureComponent 8 | } from "../../../../src/components/DatatableHeader/Widgets/CreatePreset"; 9 | 10 | const mockStore = configureStore(); 11 | const store = mockStore(storeSample); 12 | const { columns } = storeSample.datatableReducer.data; 13 | describe("CreatePreset component", () => { 14 | it("connected should render without errors", () => { 15 | const wrapper = shallow( 16 | 17 | 18 | 19 | ); 20 | expect(wrapper.find("Connect(CreatePreset)")).toHaveLength(1); 21 | }); 22 | 23 | it("connected should mount without errors", () => { 24 | const wrapper = mount( 25 | 26 | 27 | 28 | ); 29 | const button = wrapper.find("button.create-preset-icon"); 30 | button.simulate("click"); 31 | expect(wrapper.find("Connect(CreatePreset)")).toHaveLength(1); 32 | }); 33 | 34 | it("create preset with props", () => { 35 | const wrapper = mount( 36 | 37 | 46 | 47 | ); 48 | const button = wrapper.find("button.create-preset-icon"); 49 | button.simulate("click"); 50 | 51 | // insert preset name 52 | wrapper 53 | .find("input") 54 | .at(0) 55 | .simulate("change", { target: { value: "NEW PRESET" } }); 56 | 57 | // check 1st checkbox 58 | const input = wrapper.find("input"); 59 | input.at(0).getDOMNode().checked = !input.at(0).getDOMNode().checked; 60 | input.at(0).simulate("change"); 61 | 62 | // click on Create button 63 | const createButton = wrapper.findWhere(node => { 64 | return node.type() && node.name() && node.text() === "Create"; 65 | }); 66 | createButton.at(0).simulate("click"); 67 | 68 | expect(createButton.at(0).text()).toEqual("Create"); 69 | expect(wrapper).toBeTruthy(); 70 | }); 71 | 72 | it("cancel preset with props", () => { 73 | const wrapper = mount( 74 | 75 | 84 | 85 | ); 86 | const button = wrapper.find("button.create-preset-icon"); 87 | button.simulate("click"); 88 | 89 | // click on Create button 90 | const createButton = wrapper.findWhere(node => { 91 | return node.type() && node.name() && node.text() === "Cancel"; 92 | }); 93 | createButton.at(0).simulate("click"); 94 | expect(createButton.at(0).text()).toEqual("Cancel"); 95 | expect(wrapper).toBeTruthy(); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /src/components/DatatableHeader/Widgets/Search.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from "react"; 2 | import { connect } from "react-redux"; 3 | import { IconButton, Tooltip, Zoom, TextField } from "@material-ui/core"; 4 | import { Search as SearchIcon } from "@material-ui/icons"; 5 | import { 6 | searchPropType, 7 | searchTermPropType, 8 | rowsPropType, 9 | isRefreshingPropType, 10 | textPropType, 11 | isSearchFieldDisplayedPropType, 12 | toggleSearchFieldDisplayPropType 13 | } from "../../../proptypes"; 14 | import { 15 | search as searchAction, 16 | toggleSearchFieldDisplay as toggleSearchFieldDisplayAction 17 | } from "../../../redux/actions/datatableActions"; 18 | 19 | export class Search extends Component { 20 | constructor(props) { 21 | super(props); 22 | this.searchInput = React.createRef(); 23 | } 24 | 25 | searchUpdate = e => { 26 | const { search } = this.props; 27 | const { value } = e.target; 28 | search(value); 29 | }; 30 | 31 | toggleSearch = () => { 32 | const { toggleSearchFieldDisplay, isSearchFieldDisplayed } = this.props; 33 | if (!isSearchFieldDisplayed) { 34 | this.searchInput.current.focus(); 35 | } 36 | toggleSearchFieldDisplay(); 37 | }; 38 | 39 | render() { 40 | const { 41 | searchTerm, 42 | rows, 43 | isRefreshing, 44 | searchText, 45 | searchPlaceholderText, 46 | isSearchFieldDisplayed 47 | } = this.props; 48 | const disabled = rows.length === 0 || isRefreshing; 49 | 50 | return ( 51 | 52 | 64 | 69 | 70 | this.toggleSearch()} 73 | disabled={disabled} 74 | > 75 | 76 | 77 | 78 | 79 | 80 | ); 81 | } 82 | } 83 | 84 | Search.propTypes = { 85 | search: searchPropType, 86 | searchTerm: searchTermPropType.isRequired, 87 | rows: rowsPropType.isRequired, 88 | isRefreshing: isRefreshingPropType.isRequired, 89 | searchText: textPropType, 90 | searchPlaceholderText: textPropType, 91 | isSearchFieldDisplayed: isSearchFieldDisplayedPropType.isRequired, 92 | toggleSearchFieldDisplay: toggleSearchFieldDisplayPropType.isRequired 93 | }; 94 | 95 | const mapDispatchToProps = dispatch => { 96 | return { 97 | search: term => dispatch(searchAction(term)), 98 | toggleSearchFieldDisplay: () => dispatch(toggleSearchFieldDisplayAction()) 99 | }; 100 | }; 101 | 102 | const mapStateToProps = state => { 103 | return { 104 | rowsSelected: state.datatableReducer.rowsSelected, 105 | isRefreshing: state.datatableReducer.isRefreshing, 106 | rows: state.datatableReducer.data.rows, 107 | searchTerm: state.datatableReducer.searchTerm, 108 | searchText: state.textReducer.search, 109 | searchPlaceholderText: state.textReducer.searchPlaceholder, 110 | isSearchFieldDisplayed: state.datatableReducer.isSearchFieldDisplayed 111 | }; 112 | }; 113 | 114 | export default connect(mapStateToProps, mapDispatchToProps)(Search); 115 | -------------------------------------------------------------------------------- /src/components/DatatableCore/Header/HeaderActionsCell.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unused-prop-types */ 2 | /* eslint-disable camelcase */ 3 | import React, { Component } from "react"; 4 | import { connect } from "react-redux"; 5 | import { difference } from "lodash"; 6 | import Checkbox from "@material-ui/core/Checkbox"; 7 | import Grid from "@material-ui/core/Grid"; 8 | import { setRowsGlobalSelected as setRowsGlobalSelectedAction } from "../../../redux/actions/datatableActions"; 9 | import { 10 | columnPropType, 11 | isScrollingPropType, 12 | canSelectRowPropType, 13 | rowsPropType, 14 | rowsSelectedPropType, 15 | keyColumnPropType, 16 | isLastLockedPropType, 17 | setRowsSelectedPropType 18 | } from "../../../proptypes"; 19 | 20 | export class HeaderActionsCell extends Component { 21 | constructor(props) { 22 | super(props); 23 | this.state = { 24 | checked: false 25 | }; 26 | } 27 | 28 | // eslint-disable-next-line react/sort-comp 29 | UNSAFE_componentWillReceiveProps(nextProps) { 30 | const { rowsToUse, rowsSelected, keyColumn } = nextProps; 31 | 32 | const checked = 33 | difference( 34 | rowsToUse.map(r => r[keyColumn]), 35 | rowsSelected.map(r => r[keyColumn]) 36 | ).length === 0; 37 | 38 | this.setState({ checked }); 39 | } 40 | 41 | handleChange = () => { 42 | const { setRowsGlobalSelected, rowsToUse } = this.props; 43 | const { checked } = this.state; 44 | setRowsGlobalSelected({ 45 | rows: rowsToUse, 46 | checked: !checked 47 | }); 48 | }; 49 | 50 | render() { 51 | const { canSelect, column, isScrolling, isLastLocked } = this.props; 52 | const { checked } = this.state; 53 | 54 | let className = ""; 55 | switch (true) { 56 | case isLastLocked && isScrolling: 57 | className = "Table-Header-Cell action scrolling-shadow"; 58 | break; 59 | case isLastLocked && !isScrolling: 60 | className = "Table-Header-Cell action no-scrolling-shadow"; 61 | break; 62 | default: 63 | className = `Table-Header-Cell action`; 64 | break; 65 | } 66 | 67 | return ( 68 |
69 | 70 | {canSelect && ( 71 | 72 | 79 | 80 | )} 81 | {!canSelect && ( 82 | 83 | Actions 84 | 85 | )} 86 | 87 |
88 | ); 89 | } 90 | } 91 | 92 | const mapDispatchToProps = dispatch => { 93 | return { 94 | setRowsGlobalSelected: payload => 95 | dispatch(setRowsGlobalSelectedAction(payload)) 96 | }; 97 | }; 98 | const mapStateToProps = state => { 99 | return { 100 | isScrolling: state.datatableReducer.dimensions.isScrolling, 101 | canSelect: state.datatableReducer.features.canSelectRow, 102 | rowsToUse: state.datatableReducer.pagination.rowsToUse, 103 | rowsSelected: state.datatableReducer.rowsSelected, 104 | keyColumn: state.datatableReducer.keyColumn 105 | }; 106 | }; 107 | 108 | HeaderActionsCell.propTypes = { 109 | column: columnPropType.isRequired, 110 | isScrolling: isScrollingPropType, 111 | canSelect: canSelectRowPropType, 112 | isLastLocked: isLastLockedPropType, 113 | rowsToUse: rowsPropType, 114 | rowsSelected: rowsSelectedPropType, 115 | keyColumn: keyColumnPropType, 116 | setRowsGlobalSelected: setRowsSelectedPropType 117 | }; 118 | 119 | export default connect(mapStateToProps, mapDispatchToProps)(HeaderActionsCell); 120 | -------------------------------------------------------------------------------- /test/componentsTest/DatatableHeaderTest/DatatableHeader.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import configureStore from "redux-mock-store"; 3 | import { Provider } from "react-redux"; 4 | import { shallow, mount } from "enzyme"; 5 | import { CallSplit as CallSplitIcon } from "@material-ui/icons"; 6 | import DatatableHeader from "../../../src/components/DatatableHeader/DatatableHeader"; 7 | import { storeSample } from "../../../data/samples"; 8 | 9 | const mockStore = configureStore(); 10 | const store = mockStore({ 11 | ...storeSample, 12 | datatableReducer: { 13 | ...storeSample.datatableReducer, 14 | refreshRows: jest.fn(), 15 | features: { 16 | ...storeSample.datatableReducer.features, 17 | canEdit: false, 18 | canGlobalEdit: true, 19 | canSearch: true, 20 | canDownload: true, 21 | canOrderColumns: true, 22 | canPrint: true, 23 | canRefreshRows: true, 24 | canCreatePreset: true, 25 | canSaveUserConfiguration: true, 26 | additionalIcons: [ 27 | { 28 | title: "Coffee", 29 | icon: , 30 | onClick: () => true 31 | } 32 | ] 33 | } 34 | } 35 | }); 36 | 37 | const storeBasicIcons = mockStore({ 38 | ...storeSample, 39 | datatableReducer: { 40 | ...storeSample.datatableReducer, 41 | features: { 42 | ...storeSample.datatableReducer.features, 43 | canSearch: false, 44 | canDownload: false, 45 | canOrderColumns: false, 46 | canPrint: false, 47 | canRefreshRows: false, 48 | canCreatePreset: false, 49 | canSaveUserConfiguration: false, 50 | selectionIcons: [] 51 | } 52 | } 53 | }); 54 | 55 | describe("DatatableHeader component", () => { 56 | it("connected should render without errors", () => { 57 | const wrapper = shallow( 58 | 59 | 60 | 61 | ); 62 | expect(wrapper.find("Connect(DatatableHeader)")).toHaveLength(1); 63 | }); 64 | 65 | describe("should render", () => { 66 | const wrapper = mount( 67 | 68 | 69 | 70 | ); 71 | 72 | it("a title", () => { 73 | expect(wrapper.find("div.title")).toHaveLength(1); 74 | }); 75 | 76 | it("a downloadData button", () => { 77 | expect(wrapper.find("DownloadData")).toHaveLength(1); 78 | }); 79 | 80 | it("a selection icons separator", () => { 81 | const element = wrapper.find("div.selection-icons-separator"); 82 | expect(element.props().style.height).toEqual("45%"); 83 | }); 84 | 85 | it("selection icons", () => { 86 | expect(wrapper.find("SelectionIcons")).toHaveLength(1); 87 | }); 88 | 89 | it("a global edit icon separator", () => { 90 | const element = wrapper.find("div.global-edit-icon-separator"); 91 | expect(element.props().style.height).toEqual("45%"); 92 | }); 93 | 94 | it("global edit", () => { 95 | expect(wrapper.find("GlobalEdit")).toHaveLength(1); 96 | }); 97 | 98 | it("an additional icons separator", () => { 99 | const element = wrapper.find("div.additional-icons-separator"); 100 | expect(element.props().style.height).toEqual("45%"); 101 | }); 102 | 103 | it("additional icons", () => { 104 | expect(wrapper.find("AdditionalIcons")).toHaveLength(1); 105 | }); 106 | }); 107 | 108 | describe("with basic icons should not render render", () => { 109 | const wrapper = mount( 110 | 111 | 112 | 113 | ); 114 | 115 | it("a selection icons separator", () => { 116 | const element = wrapper.find("div.selection-icons-separator"); 117 | expect(element.props().style.height).toEqual("0%"); 118 | }); 119 | 120 | it("an additional icons separator", () => { 121 | const element = wrapper.find("div.additional-icons-separator"); 122 | expect(element.props().style.height).toEqual("0%"); 123 | }); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /test/componentsTest/DatatableCoreTest/HeaderTest/HeaderRow.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import configureStore from "redux-mock-store"; 3 | import { Provider } from "react-redux"; 4 | import { shallow, mount } from "enzyme"; 5 | import { cloneDeep } from "lodash"; 6 | import HeaderRow, { 7 | HeaderRow as HeaderRowPureComponent 8 | } from "../../../../src/components/DatatableCore/Header/HeaderRow"; 9 | import HeaderCell from "../../../../src/components/DatatableCore/Header/HeaderCell"; 10 | import { 11 | storeNoCustomComponentsSample, 12 | storeCustomTableHeaderCellComponentSample 13 | } from "../../../../data/samples"; 14 | import { 15 | NumberWrapper, 16 | TextWrapper, 17 | BooleanWrapper, 18 | DateTimeWrapper 19 | } from "../../../../src/components/DatatableCore/CellTypes"; 20 | 21 | const mockStore = configureStore(); 22 | const store = mockStore(storeNoCustomComponentsSample); 23 | const storeCustomComponent = mockStore( 24 | storeCustomTableHeaderCellComponentSample 25 | ); 26 | 27 | const { columns } = storeNoCustomComponentsSample.datatableReducer.data; 28 | const { 29 | columnsOrder 30 | } = storeNoCustomComponentsSample.datatableReducer.features.userConfiguration; 31 | 32 | describe("HeaderRow component", () => { 33 | it("connected should render without errors", () => { 34 | const wrapper = shallow( 35 | 36 | 41 | 42 | ); 43 | expect(wrapper.find("Connect(HeaderRow)")).toHaveLength(1); 44 | }); 45 | 46 | describe("should create a row", () => { 47 | const wrapper = mount( 48 | 49 | 50 | 51 | ); 52 | 53 | it("of 7 cells", () => { 54 | expect(wrapper.find(HeaderCell)).toHaveLength(7); 55 | }); 56 | 57 | it("with 1 number cell", () => { 58 | expect(wrapper.find(NumberWrapper)).toHaveLength(1); 59 | }); 60 | 61 | it("with 4 text cells", () => { 62 | expect(wrapper.find(TextWrapper)).toHaveLength(4); 63 | }); 64 | 65 | it("with 1 boolean cell", () => { 66 | expect(wrapper.find(BooleanWrapper)).toHaveLength(1); 67 | }); 68 | 69 | it("with 1 dateTime cell", () => { 70 | expect(wrapper.find(DateTimeWrapper)).toHaveLength(1); 71 | }); 72 | }); 73 | 74 | describe("should create a row with custom cell", () => { 75 | const wrapper = mount( 76 | 77 | 78 | 79 | ); 80 | 81 | it("of 8 cells", () => { 82 | expect(wrapper.find(".Table-Header-Cell")).toHaveLength(8); 83 | }); 84 | 85 | it("with 1 actions cell", () => { 86 | expect(wrapper.find(".action")).toHaveLength(1); 87 | }); 88 | 89 | it("with 1 number cell", () => { 90 | expect(wrapper.find(".number").hostNodes()).toHaveLength(1); 91 | }); 92 | 93 | it("with 2 text cells", () => { 94 | expect(wrapper.find(".text").hostNodes()).toHaveLength(2); 95 | }); 96 | 97 | it("with 1 boolean cell", () => { 98 | expect(wrapper.find(".boolean").hostNodes()).toHaveLength(1); 99 | }); 100 | 101 | it("with 1 date cell", () => { 102 | expect(wrapper.find(".dateTime").hostNodes()).toHaveLength(1); 103 | }); 104 | 105 | it("with 2 default cell", () => { 106 | expect(wrapper.find(".default").hostNodes()).toHaveLength(2); 107 | }); 108 | }); 109 | 110 | it("should call on sort end without errors", () => { 111 | const onSortEnd = jest.fn(); 112 | const wrapper = shallow( 113 | 121 | ); 122 | wrapper.instance().onSortEnd({ newIndex: 0, oldIndex: 1 }); 123 | expect(onSortEnd).toBeCalled(); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /src/components/DatatableCore/InputTypes/TextFieldWrapper.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from "react"; 2 | import equal from "fast-deep-equal"; 3 | import { 4 | Tooltip, 5 | Zoom, 6 | withStyles, 7 | FormControl, 8 | Input, 9 | InputLabel 10 | } from "@material-ui/core"; 11 | import MaskedInput from "react-text-mask"; 12 | import { checkValue, setValue } from "./PickersFunction"; 13 | import { customVariant } from "../../MuiTheme"; 14 | import { 15 | valueVerificationPropType, 16 | cellValPropType, 17 | classesPropType, 18 | maskPropType, 19 | labelPropType, 20 | typePropType, 21 | requiredPropType 22 | } from "../../../proptypes"; 23 | 24 | export class TextFieldWrapper extends Component { 25 | constructor(props) { 26 | super(props); 27 | this.state = { 28 | tooltipOpen: false, 29 | message: "", 30 | error: false 31 | }; 32 | } 33 | 34 | componentDidMount() { 35 | const { valueVerification } = this.props; 36 | if (valueVerification) { 37 | const newState = checkValue({ 38 | ...this.props, 39 | mounting: true 40 | }); 41 | if (!equal(this.state, newState)) { 42 | this.setState(newState); 43 | } 44 | } 45 | } 46 | 47 | onValueChange = value => { 48 | const newValue = value.length > 0 ? value : null; 49 | const newState = setValue({ 50 | ...this.props, 51 | value: newValue 52 | }); 53 | 54 | if (!equal(this.state, newState)) { 55 | this.setState(newState); 56 | } 57 | }; 58 | 59 | toggleTooltip = open => { 60 | const { error } = this.state; 61 | if (error) { 62 | this.setState({ tooltipOpen: open }); 63 | } 64 | }; 65 | 66 | textMaskCustom = properties => { 67 | const { inputRef, ...other } = properties; 68 | const { mask } = this.props; 69 | 70 | return ( 71 | 72 | {(!mask || mask.length === 0) && ( 73 | { 77 | inputRef(ref ? ref.inputElement : null); 78 | }} 79 | /> 80 | )} 81 | {mask && mask.length > 0 && ( 82 | { 86 | inputRef(ref ? ref.inputElement : null); 87 | }} 88 | mask={mask} 89 | showMask 90 | /> 91 | )} 92 | 93 | ); 94 | }; 95 | 96 | render() { 97 | const { type, cellVal, classes, label, required } = this.props; 98 | const { tooltipOpen, message, error } = this.state; 99 | const inputValue = 100 | type === "number" && !cellVal && cellVal !== 0 ? "" : cellVal; 101 | return ( 102 | 112 | 113 | {label} 114 | this.toggleTooltip(true)} 118 | onBlur={() => this.setState({ tooltipOpen: false })} 119 | onChange={e => this.onValueChange(e.target.value)} 120 | type={type} 121 | style={{ marginTop: 0 }} 122 | fullWidth 123 | inputComponent={this.textMaskCustom} 124 | /> 125 | 126 | 127 | ); 128 | } 129 | } 130 | 131 | TextFieldWrapper.propTypes = { 132 | required: requiredPropType, 133 | label: labelPropType, 134 | cellVal: cellValPropType, 135 | classes: classesPropType.isRequired, 136 | type: typePropType.isRequired, 137 | mask: maskPropType, 138 | valueVerification: valueVerificationPropType 139 | }; 140 | 141 | export default withStyles(customVariant)(TextFieldWrapper); 142 | -------------------------------------------------------------------------------- /src/components/DatatableFooter/DatatableFooter.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import { Select, MenuItem, IconButton } from "@material-ui/core"; 4 | import { 5 | NavigateNext as NavigateNextIcon, 6 | NavigateBefore as NavigateBeforeIcon 7 | } from "@material-ui/icons"; 8 | import { 9 | paginationPropType, 10 | widthNumberPropType, 11 | rowsPerPagePropType, 12 | setPagePagePropType, 13 | setRowsPerPagePropType, 14 | textPropType 15 | } from "../../proptypes"; 16 | import { 17 | setPage as setPageAction, 18 | setRowsPerPage as setRowsPerPageAction 19 | } from "../../redux/actions/datatableActions"; 20 | 21 | class DatatableFooter extends Component { 22 | render() { 23 | const { 24 | width, 25 | rowsPerPage, 26 | pagination, 27 | setPage, 28 | setRowsPerPage, 29 | paginationRowsText, 30 | paginationPageText 31 | } = this.props; 32 | 33 | return ( 34 |
35 |
36 | {paginationRowsText} : 37 | 51 |
52 | 53 |
54 | {paginationPageText} : 55 | 69 |
70 | 71 |
72 | setPage(pagination.pageSelected - 1)} 78 | > 79 | 80 | 81 | setPage(pagination.pageSelected + 1)} 88 | > 89 | 90 | 91 |
92 |
93 | ); 94 | } 95 | } 96 | 97 | DatatableFooter.propTypes = { 98 | pagination: paginationPropType.isRequired, 99 | width: widthNumberPropType.isRequired, 100 | rowsPerPage: rowsPerPagePropType.isRequired, 101 | setPage: setPagePagePropType, 102 | setRowsPerPage: setRowsPerPagePropType, 103 | paginationRowsText: textPropType, 104 | paginationPageText: textPropType 105 | }; 106 | 107 | const mapDispatchToProps = dispatch => { 108 | return { 109 | setPage: pageNumber => dispatch(setPageAction(pageNumber)), 110 | setRowsPerPage: rowsPerPage => dispatch(setRowsPerPageAction(rowsPerPage)) 111 | }; 112 | }; 113 | 114 | const mapStateToProps = state => { 115 | return { 116 | width: state.datatableReducer.dimensions.datatable.widthNumber, 117 | pagination: state.datatableReducer.pagination, 118 | rowsPerPage: state.datatableReducer.features.rowsPerPage, 119 | paginationRowsText: state.textReducer.paginationRows, 120 | paginationPageText: state.textReducer.paginationPage 121 | }; 122 | }; 123 | 124 | export default connect(mapStateToProps, mapDispatchToProps)(DatatableFooter); 125 | -------------------------------------------------------------------------------- /test/componentsTest/DatatableContainer.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import configureStore from "redux-mock-store"; 3 | import { Provider } from "react-redux"; 4 | import { SnackbarProvider } from "notistack"; 5 | import { shallow, mount } from "enzyme"; 6 | import DatatableContainer from "../../src/components/DatatableContainer"; 7 | import Header from "../../src/components/DatatableCore/Header/Header"; 8 | import Body from "../../src/components/DatatableCore/Body/Body"; 9 | import DatatableFooter from "../../src/components/DatatableFooter/DatatableFooter"; 10 | import { 11 | storeSample, 12 | storeNoDataSample, 13 | storeNoRowsDataSample 14 | } from "../../data/samples"; 15 | 16 | const mockStore = configureStore(); 17 | const store = mockStore(storeSample); 18 | const storeNoData = mockStore(storeNoDataSample); 19 | const storeNoRowsData = mockStore(storeNoRowsDataSample); 20 | const storeIsRefreshing = mockStore({ 21 | ...storeSample, 22 | datatableReducer: { ...storeSample.datatableReducer, isRefreshing: true } 23 | }); 24 | const refreshRows = jest.fn(); 25 | 26 | describe("Datatable container component", () => { 27 | it("connected should render without errors", () => { 28 | const wrapper = shallow( 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | expect(wrapper.find("Connect(DatatableContainer)")).toHaveLength(1); 36 | }); 37 | 38 | describe("when you have data should create a table", () => { 39 | const wrapper = mount( 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | 47 | it("without errors", () => { 48 | expect(wrapper.find("div.Table")).toHaveLength(1); 49 | }); 50 | 51 | it("with a Header", () => { 52 | expect(wrapper.find(Header)).toHaveLength(1); 53 | }); 54 | 55 | it("with a Body", () => { 56 | expect(wrapper.find(Body)).toHaveLength(1); 57 | }); 58 | 59 | it("and a Footer", () => { 60 | expect(wrapper.find(DatatableFooter)).toHaveLength(1); 61 | }); 62 | }); 63 | 64 | describe("when you don't have rows data should create a table", () => { 65 | const wrapperNoRowsData = mount( 66 | 67 | 68 | 69 | 70 | 71 | ); 72 | 73 | it("with a Header", () => { 74 | expect(wrapperNoRowsData.find(Header)).toHaveLength(1); 75 | }); 76 | 77 | it("without a Body", () => { 78 | expect(wrapperNoRowsData.find(Body)).toHaveLength(0); 79 | }); 80 | 81 | it("with a div telling no data", () => { 82 | expect(wrapperNoRowsData.find("div#no-rows").hostNodes()).toHaveLength(1); 83 | }); 84 | }); 85 | 86 | describe("when you don't have data should create a table", () => { 87 | const wrapperNoData = mount( 88 | 89 | 90 | 91 | 92 | 93 | ); 94 | 95 | it("without Header", () => { 96 | expect(wrapperNoData.find(Header)).toHaveLength(0); 97 | }); 98 | 99 | it("without Body", () => { 100 | expect(wrapperNoData.find(Body)).toHaveLength(0); 101 | }); 102 | 103 | it("with a div telling no data", () => { 104 | expect(wrapperNoData.find("div#no-rows").hostNodes()).toHaveLength(1); 105 | }); 106 | 107 | it("and a Footer", () => { 108 | expect(wrapperNoData.find(DatatableFooter)).toHaveLength(1); 109 | }); 110 | }); 111 | 112 | describe("when you is Refreshing", () => { 113 | const wrapperNoData = mount( 114 | 115 | 116 | 117 | 118 | 119 | ); 120 | 121 | it("Loader", () => { 122 | expect(wrapperNoData.find("Loader")).toHaveLength(1); 123 | }); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /src/components/DatatableCore/InputTypes/CreateInput.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import DatePickerWrapper from "./DatePickerWrapper"; 3 | import TimePickerWrapper from "./TimePickerWrapper"; 4 | import DateTimePickerWrapper from "./DateTimePickerWrapper"; 5 | import TextFieldWrapper from "./TextFieldWrapper"; 6 | import SelectWrapper from "./SelectWrapper"; 7 | import BooleanWrapper from "./BooleanWrapper"; 8 | import { 9 | cellValPropType, 10 | valueVerificationPropType, 11 | rowIdPropType, 12 | columnIdPropType, 13 | setRowEditedPropType, 14 | valuesPropType, 15 | dateFormatPropType, 16 | typePropType, 17 | maskPropType, 18 | inputTypePropType, 19 | labelPropType, 20 | requiredPropType 21 | } from "../../../proptypes"; 22 | 23 | const CreateInput = ({ 24 | cellVal, 25 | valueVerification, 26 | rowId, 27 | columnId, 28 | setRowEdited, 29 | values, 30 | dateFormatIn, 31 | dateFormatOut, 32 | type, 33 | mask, 34 | inputType, 35 | required = false, 36 | label = "" 37 | }) => { 38 | const val = 39 | cellVal || 40 | (type === "number" && cellVal === 0) || 41 | (inputType === "boolean" && !cellVal) 42 | ? cellVal 43 | : ""; 44 | const isNull = cellVal == null; 45 | 46 | switch (inputType) { 47 | case "datePicker": 48 | return ( 49 | 61 | ); 62 | case "timePicker": 63 | return ( 64 | 76 | ); 77 | case "dateTimePicker": 78 | return ( 79 | 91 | ); 92 | case "select": 93 | return SelectWrapper({ 94 | cellVal: val, 95 | isNull, 96 | values, 97 | rowId, 98 | dateFormatIn, 99 | dateFormatOut, 100 | columnId, 101 | setRowEdited, 102 | label, 103 | required 104 | }); 105 | case "boolean": 106 | return BooleanWrapper({ 107 | cellVal: val, 108 | isNull, 109 | rowId, 110 | columnId, 111 | setRowEdited, 112 | label, 113 | required 114 | }); 115 | case "input": 116 | default: 117 | return ( 118 | 130 | ); 131 | } 132 | }; 133 | 134 | CreateInput.propTypes = { 135 | required: requiredPropType, 136 | cellVal: cellValPropType.isRequired, 137 | label: labelPropType, 138 | valueVerification: valueVerificationPropType, 139 | mask: maskPropType, 140 | rowId: rowIdPropType.isRequired, 141 | columnId: columnIdPropType.isRequired, 142 | setRowEdited: setRowEditedPropType, 143 | values: valuesPropType.isRequired, 144 | dateFormatIn: dateFormatPropType.isRequired, 145 | dateFormatOut: dateFormatPropType.isRequired, 146 | type: typePropType.isRequired, 147 | inputType: inputTypePropType.isRequired 148 | }; 149 | 150 | export default CreateInput; 151 | -------------------------------------------------------------------------------- /test/componentsTest/DatatableInitializer.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import configureStore from "redux-mock-store"; 3 | import { Provider } from "react-redux"; 4 | import { shallow, mount } from "enzyme"; 5 | import { SnackbarProvider } from "notistack"; 6 | import DatatableInitializer, { 7 | DatatableInitializer as DatatableInitializerPureComponent 8 | } from "../../src/components/DatatableInitializer"; 9 | import { storeSample, simpleOptionsSample } from "../../data/samples"; 10 | 11 | const mockStore = configureStore(); 12 | const store = mockStore(storeSample); 13 | const refreshRows = jest.fn(); 14 | const initText = jest.fn(); 15 | 16 | describe("Datatable initializer component", () => { 17 | it("connected should render without errors", () => { 18 | const wrapper = shallow( 19 | 20 | 21 | 22 | ); 23 | expect(wrapper.find("Connect(DatatableInitializer)")).toHaveLength(1); 24 | }); 25 | 26 | it("should render DatatableInitializer component", () => { 27 | const wrapper = mount( 28 | 29 | 30 | 34 | 35 | 36 | ); 37 | expect(wrapper.find("Connect(DatatableInitializer)")).toHaveLength(1); 38 | }); 39 | 40 | describe("on mount should ", () => { 41 | const div = document.createElement("div"); 42 | window.domNode = div; 43 | document.body.appendChild(div); 44 | 45 | const componentDidMount = jest.spyOn( 46 | DatatableInitializerPureComponent.prototype, 47 | "componentDidMount" 48 | ); 49 | 50 | mount( 51 | 52 | 53 | 57 | 58 | , 59 | { attachTo: window.domNode } 60 | ); 61 | 62 | it("call componentDidMount", () => { 63 | global.innerWidth = 30000; 64 | global.dispatchEvent(new Event("resize")); 65 | 66 | expect(componentDidMount).toHaveBeenCalled(); 67 | }); 68 | 69 | describe("dispatch action type", () => { 70 | it("INITIALIZE_OPTIONS", () => { 71 | const action = store.getActions()[0]; 72 | expect(action.type).toEqual("INITIALIZE_OPTIONS"); 73 | }); 74 | it("INITIALIZE_CUSTOM_COMPONENTS", () => { 75 | const action = store.getActions()[1]; 76 | expect(action.type).toEqual("INITIALIZE_CUSTOM_COMPONENTS"); 77 | }); 78 | it("UPDATE_COMPONENT_SIZE", () => { 79 | const action = store.getActions()[3]; 80 | expect(action.type).toEqual("UPDATE_COMPONENT_SIZE"); 81 | }); 82 | }); 83 | }); 84 | 85 | describe("should handle shouldComponentUpdate", () => { 86 | it("no update if same options init", () => { 87 | const initializeOptions = jest.fn(); 88 | const initializeCustomComponents = jest.fn(); 89 | const updateComponentSize = jest.fn(); 90 | const wrapper = shallow( 91 | 98 | ); 99 | 100 | const shouldUpdate = wrapper 101 | .instance() 102 | .shouldComponentUpdate({ optionsInit: simpleOptionsSample }); 103 | expect(shouldUpdate).toBe(false); 104 | }); 105 | 106 | it("update if different options init", () => { 107 | const initializeOptions = jest.fn(); 108 | const initializeCustomComponents = jest.fn(); 109 | const updateComponentSize = jest.fn(); 110 | const wrapper = shallow( 111 | 118 | ); 119 | 120 | const shouldUpdate2 = wrapper.instance().shouldComponentUpdate({ 121 | optionsInit: { ...simpleOptionsSample, title: "change" }, 122 | initializeOptions 123 | }); 124 | expect(shouldUpdate2).toBe(true); 125 | }); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /test/reduxTest/reducersTest/customComponentsReducer.test.js: -------------------------------------------------------------------------------- 1 | import equal from "fast-deep-equal"; 2 | import customComponentsReducer from "../../../src/redux/reducers/customComponentsReducer"; 3 | import { 4 | customTableBodyRowSample, 5 | customTableBodyCellSample, 6 | customTableHeaderRowSample, 7 | customTableHeaderCellSample, 8 | customDataTypesSample 9 | } from "../../../data/samples"; 10 | 11 | const defaultState = { 12 | CustomTableBodyCell: null, 13 | CustomTableBodyRow: null, 14 | CustomTableHeaderCell: null, 15 | CustomTableHeaderRow: null, 16 | customDataTypes: [], 17 | customProps: null 18 | }; 19 | 20 | describe("componentReducer reducer", () => { 21 | it("should return the initial state", () => { 22 | expect(customComponentsReducer(undefined, {})).toEqual(defaultState); 23 | }); 24 | 25 | describe("should handle INITIALIZE_CUSTOM_COMPONENTS action with", () => { 26 | describe("custom table body", () => { 27 | it("row", () => { 28 | const newState = defaultState; 29 | defaultState.CustomTableBodyRow = customTableBodyRowSample; 30 | newState.CustomTableBodyRow = customTableBodyRowSample; 31 | 32 | const initializedCustomTableBodyRow = customComponentsReducer( 33 | undefined, 34 | { 35 | type: "INITIALIZE_CUSTOM_COMPONENTS", 36 | payload: newState 37 | } 38 | ); 39 | 40 | expect(equal(initializedCustomTableBodyRow, defaultState)).toBeTruthy(); 41 | }); 42 | 43 | it("cell", () => { 44 | const newState = defaultState; 45 | defaultState.CustomTableBodyCell = customTableBodyCellSample; 46 | newState.CustomTableBodyCell = customTableBodyCellSample; 47 | 48 | const initializedCustomTableBodyCell = customComponentsReducer( 49 | undefined, 50 | { 51 | type: "INITIALIZE_CUSTOM_COMPONENTS", 52 | payload: newState 53 | } 54 | ); 55 | 56 | expect( 57 | equal(initializedCustomTableBodyCell, defaultState) 58 | ).toBeTruthy(); 59 | }); 60 | }); 61 | 62 | describe("custom table header", () => { 63 | it("row", () => { 64 | const newState = defaultState; 65 | defaultState.CustomTableHeaderRow = customTableHeaderRowSample; 66 | newState.CustomTableHeaderRow = customTableHeaderRowSample; 67 | 68 | const initializedCustomTableHeaderRow = customComponentsReducer( 69 | undefined, 70 | { 71 | type: "INITIALIZE_CUSTOM_COMPONENTS", 72 | payload: newState 73 | } 74 | ); 75 | 76 | expect( 77 | equal(initializedCustomTableHeaderRow, defaultState) 78 | ).toBeTruthy(); 79 | }); 80 | 81 | it("cell", () => { 82 | const newState = defaultState; 83 | defaultState.CustomTableHeaderCell = customTableHeaderCellSample; 84 | newState.CustomTableHeaderCell = customTableHeaderCellSample; 85 | 86 | const initializedCustomTableHeaderCell = customComponentsReducer( 87 | undefined, 88 | { 89 | type: "INITIALIZE_CUSTOM_COMPONENTS", 90 | payload: newState 91 | } 92 | ); 93 | 94 | expect( 95 | equal(initializedCustomTableHeaderCell, defaultState) 96 | ).toBeTruthy(); 97 | }); 98 | }); 99 | 100 | it("custom dataType", () => { 101 | const newState = defaultState; 102 | defaultState.customDataTypes = customDataTypesSample; 103 | newState.customDataTypes = customDataTypesSample; 104 | 105 | const initializedCustomDataTypes = customComponentsReducer(undefined, { 106 | type: "INITIALIZE_CUSTOM_COMPONENTS", 107 | payload: newState 108 | }); 109 | 110 | expect(equal(initializedCustomDataTypes, defaultState)).toBeTruthy(); 111 | }); 112 | 113 | it("multiple custom components", () => { 114 | const newState = defaultState; 115 | defaultState.CustomTableHeaderCell = customTableHeaderCellSample; 116 | newState.CustomTableHeaderCell = customTableHeaderCellSample; 117 | defaultState.CustomTableBodyRow = customTableBodyRowSample; 118 | newState.CustomTableBodyRow = customTableBodyRowSample; 119 | defaultState.customDataTypes = customDataTypesSample; 120 | newState.customDataTypes = customDataTypesSample; 121 | 122 | const initializedMultipleCustomComponents = customComponentsReducer( 123 | undefined, 124 | { 125 | type: "INITIALIZE_CUSTOM_COMPONENTS", 126 | payload: newState 127 | } 128 | ); 129 | 130 | expect( 131 | equal(initializedMultipleCustomComponents, defaultState) 132 | ).toBeTruthy(); 133 | }); 134 | }); 135 | }); 136 | -------------------------------------------------------------------------------- /src/redux/actions/datatableActions.js: -------------------------------------------------------------------------------- 1 | import { enqueueSnackbar } from "./notifierActions"; 2 | 3 | export const initializeOptions = payload => ({ 4 | type: "INITIALIZE_OPTIONS", 5 | payload 6 | }); 7 | 8 | export const updateComponentSize = () => ({ 9 | type: "UPDATE_COMPONENT_SIZE" 10 | }); 11 | 12 | export const sortColumns = payload => ({ 13 | type: "SORT_COLUMNS", 14 | payload 15 | }); 16 | 17 | export const setRowsPerPage = payload => ({ 18 | type: "SET_ROWS_PER_PAGE", 19 | payload 20 | }); 21 | 22 | export const setPage = payload => ({ 23 | type: "SET_PAGE", 24 | payload 25 | }); 26 | 27 | export const setIsScrolling = payload => ({ 28 | type: "SET_IS_SCROLLING", 29 | payload 30 | }); 31 | 32 | export const addRowEdited = payload => ({ 33 | type: "ADD_ROW_EDITED", 34 | payload 35 | }); 36 | 37 | export const addNewRow = payload => ({ 38 | type: "ADD_NEW_ROW", 39 | payload 40 | }); 41 | 42 | export const addAllRowsToEdited = () => ({ 43 | type: "ADD_ALL_ROWS_TO_EDITED" 44 | }); 45 | 46 | export const setRowEdited = payload => ({ 47 | type: "SET_ROW_EDITED", 48 | payload 49 | }); 50 | 51 | export const saveRowEdited = payload => ({ 52 | type: "SAVE_ROW_EDITED", 53 | payload 54 | }); 55 | 56 | export const saveAllRowsEdited = () => ({ 57 | type: "SAVE_ALL_ROWS_EDITED" 58 | }); 59 | 60 | export const revertRowEdited = payload => ({ 61 | type: "REVERT_ROW_EDITED", 62 | payload 63 | }); 64 | 65 | export const revertAllRowsToEdited = () => ({ 66 | type: "REVERT_ALL_ROWS_TO_EDITED" 67 | }); 68 | 69 | export const deleteRow = payload => ({ 70 | type: "DELETE_ROW", 71 | payload 72 | }); 73 | 74 | export const addToDeleteRow = payload => ({ 75 | type: "ADD_TO_DELETE_ROW", 76 | payload 77 | }); 78 | 79 | export const selectRow = payload => ({ 80 | type: "SELECT_ROW", 81 | payload 82 | }); 83 | 84 | export const setRowsSelected = payload => ({ 85 | type: "SET_ROWS_SELECTED", 86 | payload 87 | }); 88 | 89 | export const setRowsGlobalSelected = payload => ({ 90 | type: "SET_ROWS_GLOBAL_SELECTED", 91 | payload 92 | }); 93 | 94 | export const search = payload => ({ 95 | type: "SEARCH", 96 | payload 97 | }); 98 | 99 | export const toggleFilterFieldsDisplay = () => ({ 100 | type: "TOGGLE_FILTERFIELDS_DISPLAY" 101 | }); 102 | 103 | export const toggleSearchFieldDisplay = () => ({ 104 | type: "TOGGLE_SEARCHFIELD_DISPLAY" 105 | }); 106 | 107 | export const filterInColumn = payload => ({ 108 | type: "SEARCH_IN_COLUMN", 109 | payload 110 | }); 111 | 112 | export const setColumnVisibilty = payload => ({ 113 | type: "SET_COLUMN_VISIBILITY", 114 | payload 115 | }); 116 | 117 | export const handlePresetDisplay = payload => ({ 118 | type: "HANDLE_PRESET_DISPLAY", 119 | payload 120 | }); 121 | 122 | export const notifyOnPresetCreation = payload => { 123 | return dispatch => { 124 | dispatch( 125 | enqueueSnackbar({ 126 | message: payload.message, 127 | options: { 128 | key: new Date().getTime() + Math.random(), 129 | variant: payload.variant 130 | } 131 | }) 132 | ); 133 | }; 134 | }; 135 | 136 | export const setUserConfiguration = payload => ({ 137 | type: "SET_USER_CONFIGURATION", 138 | payload 139 | }); 140 | 141 | export const refreshRowsSuccess = payload => ({ 142 | type: "REFRESH_ROWS_SUCCESS", 143 | payload 144 | }); 145 | 146 | export const refreshRowsError = () => ({ 147 | type: "REFRESH_ROWS_ERROR" 148 | }); 149 | 150 | export const refreshRowsStarted = () => ({ 151 | type: "REFRESH_ROWS_STARTED" 152 | }); 153 | 154 | export const refreshRows = payload => { 155 | const key = new Date().getTime() + Math.random(); 156 | return dispatch => { 157 | dispatch(refreshRowsStarted()); 158 | return Promise.resolve(payload()) 159 | .then(res => { 160 | dispatch( 161 | enqueueSnackbar({ 162 | message: "Rows have been refreshed.", 163 | options: { 164 | key, 165 | variant: "success" 166 | } 167 | }) 168 | ); 169 | dispatch(refreshRowsSuccess(res)); 170 | }) 171 | .catch(err => { 172 | dispatch( 173 | enqueueSnackbar({ 174 | message: "Rows couldn't be refreshed.", 175 | options: { 176 | key, 177 | variant: "error" 178 | } 179 | }) 180 | ); 181 | dispatch(refreshRowsError(err)); 182 | }); 183 | }; 184 | }; 185 | 186 | export const orderByColumns = payload => ({ 187 | type: "ORDER_BY_COLUMNS", 188 | payload 189 | }); 190 | 191 | export const updateOptions = payload => ({ 192 | type: "UPDATE", 193 | payload 194 | }); 195 | 196 | export const duplicateRow = payload => ({ 197 | type: "DUPLICATE_ROW", 198 | payload 199 | }); 200 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@o2xp/react-datatable", 3 | "description": "@o2xp/react-datatable is a modulable component to render data in a table with some nice features !", 4 | "keywords": [ 5 | "react", 6 | "component", 7 | "datatable", 8 | "data", 9 | "modulable", 10 | "table", 11 | "material-ui" 12 | ], 13 | "homepage": "https://github.com/o2xp/react-datatable", 14 | "bugs": "https://github.com/o2xp/react-datatable/issues", 15 | "license": "MIT", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/o2xp/react-datatable" 19 | }, 20 | "version": "1.1.75", 21 | "dependencies": { 22 | "@date-io/moment": "^1.3.13", 23 | "@material-ui/pickers": "^3.2.10", 24 | "array-move": "^2.2.1", 25 | "copy-to-clipboard": "^3.3.1", 26 | "deepmerge": "^3.3.0", 27 | "element-resize-event": "^3.0.3", 28 | "fast-deep-equal": "^2.0.1", 29 | "fuse.js": "^3.6.1", 30 | "moment": "^2.24.0", 31 | "notistack": "^0.8.9", 32 | "react-redux": "^6.0.1", 33 | "react-scroll-sync": "^0.7.1", 34 | "react-sortable-hoc": "^1.11.0", 35 | "react-spinners": "^0.10.6", 36 | "react-text-mask": "^5.4.3", 37 | "react-window": "^1.8.5", 38 | "redux": "^4.0.5", 39 | "redux-thunk": "^2.3.0", 40 | "styled-components": "^4.4.1", 41 | "text-width": "^1.2.0" 42 | }, 43 | "devDependencies": { 44 | "@babel/core": "^7.9.0", 45 | "@babel/plugin-proposal-class-properties": "^7.8.3", 46 | "@babel/preset-env": "^7.9.5", 47 | "@babel/preset-es2016": "^7.0.0-beta.53", 48 | "@babel/preset-react": "^7.9.4", 49 | "@dump247/storybook-state": "^1.6.1", 50 | "@material-ui/core": "^4.10.1", 51 | "@material-ui/icons": "^4.5.0", 52 | "@storybook/addon-actions": "^5.3.18", 53 | "@storybook/addon-knobs": "^5.3.18", 54 | "@storybook/addon-links": "^5.3.18", 55 | "@storybook/addon-notes": "^5.3.18", 56 | "@storybook/addons": "^5.3.18", 57 | "@storybook/react": "^6.5.10", 58 | "babel-eslint": "^10.1.0", 59 | "babel-loader": "^8.1.0", 60 | "enzyme": "^3.11.0", 61 | "enzyme-adapter-react-16": "^1.15.2", 62 | "eslint": "^5.15.1", 63 | "eslint-config-airbnb": "^17.1.1", 64 | "eslint-config-prettier": "^4.3.0", 65 | "eslint-import-resolver-webpack": "^0.11.1", 66 | "eslint-plugin-import": "^2.20.2", 67 | "eslint-plugin-jsx-a11y": "^6.2.3", 68 | "eslint-plugin-prettier": "^3.1.3", 69 | "eslint-plugin-react": "^7.19.0", 70 | "eslint-plugin-react-hooks": "^1.7.0", 71 | "husky": "^1.3.1", 72 | "jest": "^24.9.0", 73 | "jest-canvas-mock": "^2.2.0", 74 | "jest-css-modules": "^2.1.0", 75 | "jest-styled-components": "^6.3.4", 76 | "lint-staged": "^8.2.1", 77 | "prettier": "^1.19.1", 78 | "react": "^16.13.1", 79 | "react-virtualized": "^9.21.2", 80 | "redux-mock-store": "^1.5.4", 81 | "webpack": "^4.41.2", 82 | "webpack-cli": "^3.3.11", 83 | "webpack-dev-server": "^4.10.0", 84 | "webpack-node-externals": "^1.7.2" 85 | }, 86 | "peerDependencies": { 87 | "@material-ui/core": ">=4.10.0", 88 | "@material-ui/icons": ">=4.5.0", 89 | "react": ">=16.8.0", 90 | "react-dom": ">=16.8.0" 91 | }, 92 | "scripts": { 93 | "build": "webpack --mode=production", 94 | "lint": "eslint src/**/*.{js,jsx}", 95 | "lintfix": "eslint src/**/*.{js,jsx} --fix", 96 | "start": "webpack --watch", 97 | "test": "jest --verbose", 98 | "storybook": "start-storybook -p 3000", 99 | "prettier": "prettier --write src/**/*.{js,jsx}", 100 | "build-storybook": "build-storybook" 101 | }, 102 | "husky": { 103 | "hooks": { 104 | "pre-commit": "lint-staged" 105 | } 106 | }, 107 | "jest": { 108 | "coverageDirectory": "./coverage/", 109 | "collectCoverageFrom": [ 110 | "src/**/*.{js,jsx}" 111 | ], 112 | "coveragePathIgnorePatterns": [ 113 | "/redux/store/", 114 | "/redux/reducers/reducers.js", 115 | "components/Notifier.js" 116 | ], 117 | "collectCoverage": true, 118 | "moduleNameMapper": { 119 | "\\.(css|less|scss|sss|styl)$": "/node_modules/jest-css-modules" 120 | }, 121 | "testPathIgnorePatterns": [ 122 | "/node_modules/", 123 | "/storybook-static/" 124 | ], 125 | "setupFiles": [ 126 | "jest-canvas-mock" 127 | ], 128 | "setupFilesAfterEnv": [ 129 | "./test/enzyme.conf.js" 130 | ] 131 | }, 132 | "lint-staged": { 133 | "src/**/*.{js,jsx}": [ 134 | "prettier --write src/**/*.{js,jsx}", 135 | "eslint src/**/*.{js,jsx} --fix", 136 | "git add" 137 | ] 138 | }, 139 | "eslintConfig": { 140 | "extends": "react-app" 141 | }, 142 | "browserslist": [ 143 | ">0.2%", 144 | "not dead", 145 | "not ie <= 11", 146 | "not op_mini all" 147 | ] 148 | } 149 | -------------------------------------------------------------------------------- /src/components/DatatableContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from "react"; 2 | import { connect } from "react-redux"; 3 | import { ScrollSync, ScrollSyncPane } from "react-scroll-sync"; 4 | import Header from "./DatatableCore/Header/Header"; 5 | import Body from "./DatatableCore/Body/Body"; 6 | import DatatableHeader from "./DatatableHeader/DatatableHeader"; 7 | import DatatableFooter from "./DatatableFooter/DatatableFooter"; 8 | import Notifier from "./Notifier"; 9 | import Loader from "./Loader"; 10 | import { 11 | dataPropType, 12 | heightNumberPropType, 13 | widthNumberPropType, 14 | featuresPropType, 15 | titlePropType, 16 | isRefreshingPropType, 17 | columnSizeMultiplierPropType 18 | } from "../proptypes"; 19 | 20 | class DatatableContainer extends Component { 21 | render() { 22 | const { 23 | data, 24 | height, 25 | columnSizeMultiplier, 26 | width, 27 | features, 28 | title, 29 | totalWidthNumber, 30 | isRefreshing 31 | } = this.props; 32 | 33 | const { 34 | canGlobalEdit, 35 | canPrint, 36 | canDownload, 37 | canSearch, 38 | canFilter, 39 | canCreatePreset, 40 | canRefreshRows, 41 | canOrderColumns, 42 | canSaveUserConfiguration, 43 | additionalIcons, 44 | selectionIcons 45 | } = features; 46 | const hasHeader = 47 | canGlobalEdit || 48 | canPrint || 49 | canDownload || 50 | canSearch || 51 | canFilter || 52 | canCreatePreset || 53 | canRefreshRows || 54 | canOrderColumns || 55 | canSaveUserConfiguration || 56 | title.length > 0 || 57 | additionalIcons.length > 0 || 58 | selectionIcons.length > 0; 59 | 60 | return ( 61 | 62 | 63 |
64 | {hasHeader && } 65 | 66 |
67 | {data.columns.length > 0 && ( 68 | 69 |
70 | {data.rows.length > 0 && !isRefreshing && } 71 | 72 | )} 73 | {(data.columns.length === 0 || data.rows.length === 0) && 74 | !isRefreshing && ( 75 | 76 |
80 | 81 |
90 |
95 | . 96 |
97 |
98 |
99 | 100 | )} 101 | 102 | {isRefreshing && 103 | Loader({ 104 | height, 105 | width, 106 | columnSizeMultiplier, 107 | totalWidthNumber 108 | })} 109 |
110 | 111 |
112 | 113 | 114 | 115 | ); 116 | } 117 | } 118 | 119 | DatatableContainer.propTypes = { 120 | data: dataPropType.isRequired, 121 | height: heightNumberPropType.isRequired, 122 | width: widthNumberPropType.isRequired, 123 | isRefreshing: isRefreshingPropType.isRequired, 124 | totalWidthNumber: widthNumberPropType, 125 | features: featuresPropType, 126 | title: titlePropType, 127 | columnSizeMultiplier: columnSizeMultiplierPropType 128 | }; 129 | 130 | const mapStateToProps = state => { 131 | return { 132 | data: state.datatableReducer.data, 133 | height: state.datatableReducer.dimensions.body.heightNumber, 134 | width: state.datatableReducer.dimensions.datatable.widthNumber, 135 | features: state.datatableReducer.features, 136 | title: state.datatableReducer.title, 137 | isRefreshing: state.datatableReducer.isRefreshing, 138 | totalWidthNumber: 139 | state.datatableReducer.dimensions.datatable.totalWidthNumber, 140 | columnSizeMultiplier: state.datatableReducer.dimensions.columnSizeMultiplier 141 | }; 142 | }; 143 | 144 | export default connect(mapStateToProps)(DatatableContainer); 145 | -------------------------------------------------------------------------------- /src/components/DatatableCore/Body/BodyCell.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import twidth from "text-width"; 4 | import { Tooltip, Zoom } from "@material-ui/core"; 5 | import { setRowEdited as setRowEditedAction } from "../../../redux/actions/datatableActions"; 6 | import { 7 | columnPropType, 8 | cellValPropType, 9 | customDataTypesPropType, 10 | widthPropType, 11 | rowIdPropType, 12 | editingPropType, 13 | setRowEditedPropType, 14 | onClickPropType, 15 | isScrollingPropType, 16 | isLastLockedPropType, 17 | stylePropType, 18 | fontPropType 19 | } from "../../../proptypes"; 20 | import { 21 | NumberType, 22 | TextType, 23 | BooleanType, 24 | DateType, 25 | TimeType, 26 | DateTimeType 27 | } from "../CellTypes"; 28 | 29 | export class BodyCell extends Component { 30 | buildCell = () => { 31 | const { 32 | cellVal, 33 | column, 34 | customDataTypes, 35 | width, 36 | font, 37 | rowId, 38 | editing, 39 | setRowEdited, 40 | onClick, 41 | style, 42 | isLastLocked, 43 | isScrolling 44 | } = this.props; 45 | const customDatatype = customDataTypes.find( 46 | cd => cd.dataType === column.dataType 47 | ); 48 | const textWidth = twidth(cellVal, { 49 | family: font, 50 | size: 13 51 | }); 52 | const overlap = textWidth + 5 > Number(width.split("px")[0]); 53 | let cellContent; 54 | const { 55 | inputType, 56 | dataType, 57 | values, 58 | valueVerification, 59 | dateFormatIn, 60 | dateFormatOut, 61 | mask 62 | } = column; 63 | const columnId = column.id; 64 | const properties = { 65 | cellVal, 66 | editing, 67 | inputType, 68 | values, 69 | rowId, 70 | columnId, 71 | valueVerification, 72 | dateFormatIn, 73 | dateFormatOut, 74 | mask, 75 | setRowEdited 76 | }; 77 | 78 | if (customDatatype && !editing) { 79 | cellContent = customDatatype.component(cellVal, width); 80 | } else { 81 | switch (dataType) { 82 | case "number": 83 | cellContent = NumberType(properties); 84 | break; 85 | case "boolean": 86 | cellContent = BooleanType(properties); 87 | break; 88 | case "date": 89 | cellContent = DateType(properties); 90 | break; 91 | case "time": 92 | cellContent = TimeType(properties); 93 | break; 94 | case "dateTime": 95 | cellContent = DateTimeType(properties); 96 | break; 97 | case "text": 98 | default: 99 | cellContent = TextType(properties); 100 | break; 101 | } 102 | } 103 | 104 | let className = ""; 105 | switch (true) { 106 | case isLastLocked && isScrolling: 107 | className = `Table-Cell Table-Cell-${column.id} scrolling-shadow`; 108 | break; 109 | case isLastLocked && !isScrolling: 110 | className = `Table-Cell Table-Cell-${column.id} no-scrolling-shadow`; 111 | break; 112 | default: 113 | className = `Table-Cell Table-Cell-${column.id} `; 114 | break; 115 | } 116 | 117 | return ( 118 |
onClick(cellVal)} 121 | onKeyDown={this.handleKeyDown} 122 | role="presentation" 123 | style={style} 124 | > 125 | 131 |
{cellContent}
132 |
133 |
134 | ); 135 | }; 136 | 137 | render() { 138 | return this.buildCell(); 139 | } 140 | } 141 | 142 | BodyCell.propTypes = { 143 | cellVal: cellValPropType, 144 | column: columnPropType.isRequired, 145 | customDataTypes: customDataTypesPropType.isRequired, 146 | width: widthPropType.isRequired, 147 | rowId: rowIdPropType.isRequired, 148 | editing: editingPropType.isRequired, 149 | isScrolling: isScrollingPropType.isRequired, 150 | isLastLocked: isLastLockedPropType, 151 | style: stylePropType, 152 | setRowEdited: setRowEditedPropType, 153 | onClick: onClickPropType, 154 | font: fontPropType 155 | }; 156 | 157 | const mapStateToProps = state => { 158 | return { 159 | customDataTypes: state.customComponentsReducer.customDataTypes, 160 | isScrolling: state.datatableReducer.dimensions.isScrolling, 161 | font: state.datatableReducer.font 162 | }; 163 | }; 164 | 165 | const mapDispatchToProps = dispatch => { 166 | return { 167 | setRowEdited: ({ columnId, rowId, newValue, error }) => 168 | dispatch(setRowEditedAction({ columnId, rowId, newValue, error })) 169 | }; 170 | }; 171 | 172 | export default connect(mapStateToProps, mapDispatchToProps)(BodyCell); 173 | --------------------------------------------------------------------------------