diff --git a/README.md b/README.md index 0221c5d..6c4fdb7 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,283 @@ # @flipbyte/redux-datatable -[![Travis][build-badge]][build] +[Developed by Flipbyte](https://www.flipbyte.com/) + [![npm package][npm-badge]][npm] -[![Coveralls][coveralls-badge]][coveralls] -Describe @flipbyte/redux-datatable here. +Datatable built using React and Redux to fetch JSON data asynchronously using REST API. + +- Filterable columns by date ranges, numeric ranges and text. +- Pagination +- Sortable columns +- Configurable column widths +- Built in windowing to handle large dataset with thousands of rows +- Customizable limiter options +- Customizable toolbar with the ability to add custom renderers +- Completely configurable headers, filters, toolbar and pagination with options to enable/disable them individual +- Custom row level actions +- Thunks to handle custom mass or row actions externally. +- Compatible with normalizr to handle externally managed states +- Easily stylable with styled-components. +- Show or hide columns dynamically using the Columns item in the toolbar. + +## Installation + +Run the following command to install using npm + +```sh +npm i @flipbyte/redux-datatable +``` + +## Usage + +#### Add the table reducer and epics to your store. + +```javascript +// Get the table reducer and epics as follows +import { reducer, epics } from '@flipbyte/redux-datatable'; + +// Add the above reducer and epics to your store. +``` + +#### Preparing your table config object + +```javascript +{ + name: 'your_table_name', // this is the key used to set your table data inside the table reducer + height: 500, + rowHeight: 50, + pagination: { + items: [{ + type: 'limiter', + visible: true, + position: 10, + options: [10, 20, 50, 200, 2000], + default: 200, + style: { + right: false, + } + }, { + type: 'pages', + visible: true, + position: 20, + style: { + right: true, + } + }, { + type: 'resultCount', + visible: true, + position: 30, + style: { + width: '350px', + textAlign: 'center', + } + }] + }, + routes: { // You can add other routes and handle them using custom actions. + get: { // The route used to fetch data and it's params + route: '/{your_route}', + sort: 'id', + dir: 'asc', + resultPath: { + data: 'data' + } + }, + ... + }, + toolbar: [ // Each toolbar array of objects below is a separate row in the toolbar section. You can add your own renderers and toolbar items or use some of the in-built ones. + [{ + type: 'reset-filters', + label: 'Reset Filters', + visible: true, + state: false, + }, { + type: 'columns', + label: 'Columns', + visible: true, + state: false, + style: { + right: true + } + }, + ... + ] + ... + ], + columns: [{ + name: 'ids', + label: '', + sortable: false, + type: 'selection', + indexField: '@pageId', + width: 50, + extraData: 'selection' + }, { + label: 'ID', + type: 'number', + name: 'pageId', + sortable: true, + width: 150, + filterable: true, + sortable: true, + }, { + label: 'Avatar', + type: 'image', + name: 'avatar', + sortable: false, + textAlign: 'center', + width: 200, + filterable: false, + imgHeight: 50 + }, { + label: 'First Name', + type: 'string', + name: 'first_name', + sortable: true, + textAlign: 'text-left', + width: 200, + filterable: true, + }, { + label: 'Actions', + type: 'actions', + name: 'actions', + width: 100, + items: [{ + type: 'action', + name: 'edit', + label: 'Edit', + btnClass: 'btn btn-secondary', + icon: 'edit', + params: { + id: '@id', + }, + thunk: ( payload ) => ( dispatch, getState ) => { + console.log('edit', payload, getState()); + } + }, { + type: 'action', + name: 'delete', + label: 'Delete', + icon: 'trash-alt', + params: { + id: '@id' + }, + thunk: ( payload ) => ( dispatch, getState ) => { + confirm("Are your sure you want to delete this page?") + ? console.log('delete', getState()) + : console.log(false); + } + }, + ... + ] + }, + ... + ] +} +``` + +#### Render table component +```javascript +import ReduxDatatable from '@flipbyte/redux-datatable'; + +const YourComponent = () => + +``` + +### Table config props + +| Key | Type | Required | Default | Description | +| ------------ | ------------ | ------------ | ------------ | ------------ | +| name | string | true | - | A unique key where the data for the table is saved in the table state object | +| height | integer | true | - | The maximum height of the table | +| rowHeight | integer | true | - | The maximum height of each table body row | +| filterable | boolean | false | true | Whether to show/hide filters row | +| headers | boolean | false | true | Whether to show/hide headers row | +| pagination | object | false | {} | Pagination bar configuration (Check below) | +| routes | object | true | - | Routes definition to fetch data and other custom routes config for custom handling (Check below) | +| toolbar | array | false | [] | Toolbar definition (Check below) | +| columns | array | true | - | Columns to display | + +#### Pagination object + +| Key | Type | Required | Default | Description | +| ------------ | ------------ | ------------ | ------------ | ------------ | +| items | array | false | [] | Array of item objects available for display in the pagination bar. Check below for items available | +| visible | boolean/object | false | true | Whether the pagination is visible or not | + +##### Pagination items array: + +| Key | Type | Required | Default | Description | +| ------------ | ------------ | ------------ | ------------ | ------------ | +| type | string | true | - | One of the following: limiter, pages, resultCount | +| visible | boolean | false | true | Whether the item is visible | +| style | object | false | {} | Stylable properties | +| **Limiter specific options** | | | | | +| options | array | true | - | Array of integers with limiter options | +| default | array | true | - | One of the values in the limiter options key | +| **- style** | | | | | +| right | boolean | false | false | Align the item to the right | +| width | integer | false | 200 | Width of the item | +| textAlign | string | false | left | Align the text of the item to the right, left or center| +| fontSize | string | false | 14 | The font size of the text in the item | + +#### Routes object + +| Key | Type | Required | Default | Description | +| ------------ | ------------ | ------------ | ------------ | ------------ | +| get | object | true | - | The configuration for fetching data | +| **- get** | | | | | +| route | string | true | - | Your data fetching route | +| sort | string | true | - | Your key to sort with | +| dir | string | true | - | Sort by 'asc' or 'desc' order | +| resultPath | object | true | - | The keys object to your data. Required { data: '{your data path in json response. Ex: result.data}'} | + +#### Toolbar + +Toolbar config is an array of array of object where objects are the toolbar items. Each inner array represents a different row. + +| Key | Type | Required | Default | Description | +| ------------ | ------------ | ------------ | ------------ | ------------ | +| type | string | false | actions | Available values reset-filters and columns | +| label | string | true | - | Label for the toolbar item | +| visible | boolean | false | true | Whether the item is visible | +| state | boolean | false | false | Whether to pass the state object as item prop | +| style | object | false | {} | Define toolbar item styles | +| **For type: actions** | | | | | +| options | array | true | - | Array of option objects | +| **-- options** | | | | | +| type | string | true | action | Available option: action | +| name | string | true | - | Unique name for the action | +| label | string | true | - | Label for the action | +| thunk | function | true | - | An action creator which is dispatched on action click. Check demo schema. | +| **- styles** | | | | | +| right | boolean | false | false | Align the item to the right | +| width | integer | false | 200 | Width of the item | +| fontSize | string | false | 14 | The font size of the text in the item | + +#### Columns object -[build-badge]: https://img.shields.io/travis/user/repo/master.png?style=flat-square -[build]: https://travis-ci.org/user/repo +| Key | Type | Required | Default | Description | +| ------------ | ------------ | ------------ | ------------ | ------------ | +| name | string | true | - | Unique name for the column | +| label | string | true | - | Label for the column | +| sortable | boolean | false | true | Whether the column is sortable | +| filterable | boolean | false | true | Whether the column is filterable | +| type | string | true | string | Available types: selection, number, date, string, image, actions | +| width | integer | true | - | Width of the column | +| extraData | string/array | fasle | - | properties from the state to pass as in the extra object | +| textAlign | string | false | left | Text alignment in the column | +| **type: actions** | | | | | +| items | array | true | - | array of item configuration object | +| **- item configuration object ** | | | | | +| name | string | true | - | Unique name for the action | +| label | string | true | - | Label for the action | +| thunk | function | true | - | An action creator which is dispatched on action click. Check demo schema. | -[npm-badge]: https://img.shields.io/npm/v/npm-package.png?style=flat-square -[npm]: https://www.npmjs.org/package/npm-package +## License +The MIT License (MIT) -[coveralls-badge]: https://img.shields.io/coveralls/user/repo/master.png?style=flat-square -[coveralls]: https://coveralls.io/github/user/repo +[npm-badge]: https://img.shields.io/npm/v/@flipbyte/redux-datatable.svg +[npm]: https://www.npmjs.com/package/@flipbyte/redux-datatable diff --git a/demo/src/schema/basic.js b/demo/src/schema/basic.js index 11d2ee8..d6dedb7 100644 --- a/demo/src/schema/basic.js +++ b/demo/src/schema/basic.js @@ -2,15 +2,16 @@ export default { name: 'posts', height: 400, rowHeight: 50, - filterable: false, + filterable: true, + headers: true, pagination: { // visible: true, // or an object { top: true, bottom: false } default visible items: [{ type: 'limiter', visible: true, position: 10, - options: [10, 20, 50], - default: 10, + options: [10, 20, 50, 200, 2000], + default: 200, style: { right: false, } @@ -26,19 +27,18 @@ export default { visible: true, position: 30, style: { - width: '400px', + width: '350px', textAlign: 'center', } }] }, routes: { get: { - route: '/users', + route: '/page', sort: 'id', dir: 'asc', resultPath: { - data: 'data', - total: 'total' + data: 'data' } }, delete: { @@ -51,14 +51,15 @@ export default { id: 'dropdown', options: [{ type: 'action', - onClick: (params, action) => ( - confirm("Are your sure you want to delete these item(s)?") - ? dispatch(action('DELETE_DATA')(params)) - : false - ), name: 'delete', label: 'Delete', - indexField: '@id' + indexField: '@id', + thunk: ( payload ) => ( dispatch, getState ) => { + confirm("Are your sure you want to delete the selected items?") + ? console.log('delete items', getState()) + : console.log(false); + + } }, { type: 'action', name: 'edit', @@ -176,18 +177,18 @@ export default { label: '', sortable: false, type: 'selection', - indexField: '@id', + indexField: '@pageId', width: 50, - selection: true, + extraData: 'selection' }, { label: 'ID', type: 'number', - name: 'id', + name: 'pageId', sortable: true, width: 150, filterable: true, sortable: true, - }, { + }, /*{ label: 'Avatar', type: 'image', name: 'avatar', @@ -212,34 +213,45 @@ export default { textAlign: 'text-left', width: 200, filterable: true, + },*/{ + label: 'Created at', + type: 'date', + name: 'createdAt', + sortable: true, + textAlign: 'left', + width: 200, + filterable: true, }, { label: 'Actions', type: 'actions', name: 'actions', width: 100, - children: { - edit: { - type: 'action', - name: 'route', - action: 'EDIT_PAGE', - label: 'Edit', - btnClass: 'btn btn-secondary', - icon: 'edit', - params: { - id: '@id', - } + items: [{ + type: 'action', + name: 'edit', + label: 'Edit', + btnClass: 'btn btn-secondary', + icon: 'edit', + params: { + id: '@id', }, - delete: { - type: 'action', - name: 'delete', - label: 'Delete', - action: 'DATA_DELETE', - btnClass: 'btn btn-danger', - icon: 'trash-alt', - params: { - id: '@id' - } + thunk: ( payload ) => ( dispatch, getState ) => { + console.log('edit', payload, getState()); } - } + }, { + type: 'action', + name: 'delete', + label: 'Delete', + icon: 'trash-alt', + params: { + id: '@id' + }, + thunk: ( payload ) => ( dispatch, getState ) => { + confirm("Are your sure you want to delete this page?") + ? console.log('delete', getState()) + : console.log(false); + + } + }] }] } diff --git a/demo/src/store.js b/demo/src/store.js index 1af01ee..a1891b8 100644 --- a/demo/src/store.js +++ b/demo/src/store.js @@ -6,6 +6,7 @@ import { api, request } from './request-handlers'; import { createEpicMiddleware, combineEpics } from 'redux-observable'; import { combineReducers, createStore, applyMiddleware, compose } from 'redux'; import { createLogger } from 'redux-logger'; +import thunk from 'redux-thunk'; const requestHandlers = { api, request }; @@ -35,7 +36,7 @@ const prepareEpics = ( epics ) => { } const configureStore = ( config ) => { - let middlewares = []; + let middlewares = [thunk]; const logger = createLogger({ diff: true, duration: true, collapsed: true }); middlewares.push(logger); diff --git a/src/Datatable/Pagination.js b/src/Datatable/Pagination.js index 7aeffe1..ab9fbd5 100644 --- a/src/Datatable/Pagination.js +++ b/src/Datatable/Pagination.js @@ -23,7 +23,7 @@ const Pagination = ({ position, margin, componentConfig: { - items, + items = [], visible = true } }) => { diff --git a/src/Datatable/Renderer/Body/Actions.js b/src/Datatable/Renderer/Body/Actions.js index 197e178..2b8873c 100644 --- a/src/Datatable/Renderer/Body/Actions.js +++ b/src/Datatable/Renderer/Body/Actions.js @@ -6,40 +6,23 @@ import { withTableConfig } from '../../../TableProvider'; import { paramsResolver, prepareActionPayload } from '../../../utils' import { Button } from 'styled-button-component'; -class Actions extends Component { - constructor( props ) { - super(props); - - this.handleAction = this.handleAction.bind(this); - } - - handleAction( action ) { - const { data, actions } = this.props; - const params = paramsResolver(action.params, data); - actions[action.name](params.get(), action.action); - } - - render() { - const { - colConfig: { items }, - extra, - thunk - } = this.props; - return ( -
- { items.map((item, index) => - - )} -
- ) - } -} +const Actions = ({ + extra, + thunk, + colConfig: { items } +}) => ( +
+ { items.map(({ thunk: cb, ...item }, index) => + + )} +
+); export default Actions; diff --git a/src/Datatable/Table.js b/src/Datatable/Table.js index 690a138..908b7f6 100644 --- a/src/Datatable/Table.js +++ b/src/Datatable/Table.js @@ -126,6 +126,8 @@ class Table extends Component { action, thunk, data, + filterable, + headers, config: { columns, height }, } = this.props; const { items = [], query = {} } = data; @@ -137,7 +139,7 @@ class Table extends Component { - { columns.map((column, index) => + { headers !== false && columns.map((column, index) => - { columns.map((column, index) => + { filterable !== false && columns.map((column, index) => renderers[type] || renderers['default'] export const toolbarItem = ( render, item, index ) => { - const { style, type } = item; + const { style, type, visible } = item; return ( - { render(getRenderer(item.type), item, index) } + { visible !== false && render(getRenderer(item.type), item, index) } ); } diff --git a/src/Datatable/Toolbar/MassActions.js b/src/Datatable/Toolbar/MassActions.js index 47bef43..9120d7d 100644 --- a/src/Datatable/Toolbar/MassActions.js +++ b/src/Datatable/Toolbar/MassActions.js @@ -56,42 +56,11 @@ class MassActions extends Component { } } - handleAction( action, event ) { - const { - data: { selection }, - massActions - } = this.props; - - let dataKey = getConfigParam(action.indexField); - let selectedItems = getSelectedKeys(selection, dataKey); - - if( !selectedItems || !selectedItems[dataKey] || selectedItems[dataKey].length == 0 ) { - alert("No item(s) selected"); - return false; - } - - switch( action.type ) { - // case 'route': - // context.router.history.push({ - // pathname: action.route, - // search: '?' + selectedItems.toString() - // }); - // break; - - case 'action': - massActions[action.name](selectedItems.get()); - break; - - default: - break; - } - } - render() { const { itemConfig, thunk } = this.props; const { label, options } = itemConfig; const { open } = this.state; - console.log('thunk', thunk); + return (