├── .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 |
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 |
18 |
19 |
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 |
19 |
20 |
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 |
53 |
54 |
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 |
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 |
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 |
--------------------------------------------------------------------------------