diff --git a/README.md b/README.md index 870408d..9d27561 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,11 @@ import { reducer, epics } from '@flipbyte/redux-datatable'; }, ... }, + entity: { // optional. Check example code in /demo. + state: '{your state path}', + responseSchema: // normalizr schema, + schema: // normal;izr schema + }, layout: [ ['Editable'], ['MassActions', 'SimpleButton', 'ResetFilters', 'Spacer', 'Print', 'Columns'], @@ -316,6 +321,7 @@ const YourComponent = () => | rowHeight | integer | true | - | The maximum height of each table body row | | routes | object | true | - | Routes definition to fetch data and other custom routes config for custom handling (Check below) | | components | object | true | - | All the components required for your table | +| entity | object | false | - | [Normalizr](https://github.com/paularmstrong/normalizr) specification. Check below for details. | | layout | array | true | - | The layout of your table | | editing | boolean | false | false | Set the default state of the table to be in editing mode | | primaryKey | string | true | - | Set the primary key column of the table for actions like editing. | @@ -342,6 +348,18 @@ Please check the example table config object above. An array of arrays where each inner array represents a row in the layout, within which components can be specified, which will be displayed in the frontend. Please check the example table config object above. +#### Entity array + +All the fields are required when entity is defined. However, entity key itself is optional in the table config. + +| Key | Type | Required | Default | Description | +| -------------- | ------ | -------- | ------- | --------------------------------------------------------------------------------------- | +| state | object | true | - | Path to sub state in your top level redux state where the normalized data will be saved | +| responseSchema | object | true | - | Define how the data is represented in your fetch data api response | +| schema | object | true | - | Define how the data is represented in each row item of the table fetch repsonse | + +Note: Check the [example](https://github.com/flipbyte/redux-datatable/blob/master/demo/src/schema/normalized.js) code. + #### Available Components **_Common Properties_** diff --git a/demo/src/ExampleTableContainer.js b/demo/src/ExampleTableContainer.js index c1583cb..77fb1b5 100644 --- a/demo/src/ExampleTableContainer.js +++ b/demo/src/ExampleTableContainer.js @@ -3,10 +3,11 @@ import { render } from 'react-dom'; import ReduxDatatable from '../../src'; import config from './config'; -const ExampleTableContainer = ({ title, className, id, ...tableProps }) => +const ExampleTableContainer = ({ title, className, id, ...tableProps }) => (
{ title }
+); export default ExampleTableContainer; diff --git a/demo/src/config.js b/demo/src/config.js index 0ea158e..08b6386 100644 --- a/demo/src/config.js +++ b/demo/src/config.js @@ -1,9 +1,11 @@ import { reducer, epics } from '../../src'; +import pagesReducer from './reducer'; export default { tableReducerName: 'reduxDatatable', reducers: { - reduxDatatable: reducer + reduxDatatable: reducer, + pages: pagesReducer, }, epics: { pageTableFetchDataEpic: epics.fetchDataEpic, diff --git a/demo/src/index.js b/demo/src/index.js index bf01e1c..866199d 100644 --- a/demo/src/index.js +++ b/demo/src/index.js @@ -6,7 +6,7 @@ import { Provider } from 'react-redux'; import configureStore from './store'; import config from './config'; -// import '../../node_modules/bootstrap/dist/css/bootstrap.css'; +import '../../node_modules/bootstrap/dist/css/bootstrap.css'; import './css/simple-sidebar.css'; import './css/styles.css'; @@ -17,22 +17,24 @@ const Demo = () => (
- { tables.map(({ id, title }, index) => - { title } ) } + { tables.map(({ id, title }, index) => ( + { title } + ))}
- { tables.map((table, index) => - )} + { tables.map((table, index) => ( + + ))}
diff --git a/demo/src/reducer.js b/demo/src/reducer.js new file mode 100644 index 0000000..2d76354 --- /dev/null +++ b/demo/src/reducer.js @@ -0,0 +1,22 @@ +import _ from 'lodash'; +import * as actions from '../../src/actions'; + +export default function reducer(state = {}, action) { + if (!action.meta) { + return state; + } + + const { payload, meta: { name } } = action; + const acceptedActions = { + [actions.RECEIVE_DATA]: () => ({ + ...state, + ..._.get(payload, ['entities', 'pages'], {}) + }) + }; + + if (acceptedActions.hasOwnProperty(action.type) && name === 'pages') { + return acceptedActions[action.type](); + } + + return state; +} diff --git a/demo/src/schema/basic.js b/demo/src/schema/basic.js index 0df46cc..51a1212 100644 --- a/demo/src/schema/basic.js +++ b/demo/src/schema/basic.js @@ -226,39 +226,7 @@ export default { // indexField: '@pageId', width: 50, extraData: 'selection', - }, { - label: 'ID', - type: 'number', - name: 'pageId', - width: 150, - filterable: true, - sortable: true, - // editable: true - }, { - label: 'ID', - type: 'number', - name: 'pageId', - width: 150, - filterable: true, - sortable: true, - // editable: true - }, { - label: 'ID', - type: 'number', - name: 'pageId', - width: 150, - filterable: true, - sortable: true, - // editable: true - }, { - label: 'ID', - type: 'number', - name: 'pageId', - width: 150, - filterable: true, - sortable: true, - // editable: true - }, { + }, { label: 'ID', type: 'number', name: 'pageId', diff --git a/demo/src/schema/index.js b/demo/src/schema/index.js index 63df699..928e110 100644 --- a/demo/src/schema/index.js +++ b/demo/src/schema/index.js @@ -1,6 +1,13 @@ import basic from './basic'; +import normalized from './normalized'; export default [ + { + title: 'Normalized Table', + id: 'normalized-table', + className: 'mb-4', + config: normalized + }, { title: 'Basic Table', id: 'basic-table', diff --git a/demo/src/schema/normalized.js b/demo/src/schema/normalized.js new file mode 100644 index 0000000..65921d8 --- /dev/null +++ b/demo/src/schema/normalized.js @@ -0,0 +1,336 @@ +import React from 'react'; +import { MODIFY_DATA, REQUEST_DATA, IS_LOADING } from '../../../src/actions'; +import { getItemIds } from '../../../src/utils'; +import { normalize, schema } from 'normalizr'; + +const tableSchema = ( entityName, idAttributeName, definition = {}) => { + const rowSchema = new schema.Entity(entityName, definition, { + idAttribute: idAttributeName, + }); + + const responseSchema = { data: [rowSchema] } + + return { + rowSchema: rowSchema, + responseSchema: responseSchema + } +}; + +export default { + name: 'pages', + height: 400, + rowHeight: 50, + editing: false, + primaryKey: 'pageId', + routes: { + get: { + route: '/page', + sort: 'id', + dir: 'asc', + resultPath: { + data: 'data' + } + }, + delete: { + route: '/users/:id' + } + }, + entity: { + state: 'pages', + responseSchema: tableSchema('pages', 'pageId').responseSchema, + schema: tableSchema('pages', 'pageId').rowSchema + }, + layout: [ + ['Editable'], + ['MassActions', 'SimpleButton', 'ResetFilters', 'Spacer', 'Print', 'Columns'], + ['Limiter', 'Spacer', 'ResultCount', 'Spacer', 'Pages'], + [{ id: 'Table', layout: [ + ['Header'], + ['Filters'], + ['Body'], + ['Header'] + ]}], + ['Limiter', 'Spacer', 'ResultCount', 'Spacer', 'Pages'], + ], + components: { + // Loader: { + // // styles: { + // // mask: { + // // backgroundColor: 'red', + // // }, + // // spinner: { + // // borderTopColor: 'black', + // // } + // // } + // }, + ResultCount: { + styles: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center' + } + }, + // Pages: { + // styles: { + // first: { backgroundColor: 'red' }, + // previous: { backgroundColor: 'green' }, + // pageNumber: { backgroundColor: 'yellow' }, + // next: { backgroundColor: 'pink' }, + // last: { backgroundColor: 'purple' }, + // } + // }, + Editable: { + type: 'editable', + labels: { + show: 'Make editable', + hide: 'Hide editable', + save: 'Save', + }, + save: ( config ) => ( dispatch, getState ) => { + const tableState = getState()[config.reducerName][config.name]; + console.log('toolbar save click with modified data', config, tableState.modified); + config.action(MODIFY_DATA)({ clear: true }); + // Dispatch MODIFY_DATA action with clear: true, to reset the modified data + // Dispatch REQUEST_DATA action "config.action(REQUEST_DATA)" to refresh data. + }, + // styles: { + // show: { backgroundColor: 'blue' }, + // hide: { backgroundColor: 'black', color: 'white'}, + // save: { backgroundColor: 'green' } + // } + // renderer: ( props ) => {} + }, + MassActions: { + name: 'actions', + label: 'Actions', + id: 'dropdown', + // styles: { + // button: { + // backgroundColor: '#aaa' + // }, + // dropdownMenu: { + // backgroundColor: 'magento' + // }, + // dropdownItem: { + // backgroundColor: 'pink' + // } + // }, + options: [{ + type: 'action', + name: 'delete', + label: 'Delete', + styles: { + backgroundColor: 'red', + }, + thunk: ( config ) => ( dispatch, getState ) => { + // Get current table state. + const tableState = getState()[config.reducerName][config.name]; + console.log(config, tableState); + console.log(getItemIds(tableState.selection, tableState.items, config.primaryKey/*, config.entity.schema*/)) + confirm('Are your sure you want to delete the selected items?') + ? console.log('delete items', config, getState(), tableState) + : console.log(false); + + // Filter your selected item ids here for deletion + // You can find the selection data in the selection key of the tableState. + // When all:true, exclude the ids in the selected object with value false and vice versa. + } + }, { + type: 'action', + name: 'edit', + label: 'Edit this field', + }] + }, + SimpleButton: { + type: 'button', + label: 'Simple Button', + state: false, + thunk: ( config ) => ( dispatch, getState ) => { + const tableState = getState()[config.reducerName][config.name]; + console.log('toolbar button click', config, tableState); + config.action(REQUEST_DATA)(); + config.action(IS_LOADING)({ value: true }); + setTimeout(function() { + config.action(IS_LOADING)({ value: false }); + }, 1000); + }, + // styles: { + // backgroundColor: 'green', + // color: 'white' + // } + }, + ResetFilters: { + type: 'reset-filters', + label: 'Reset Filters', + state: false, + // styles: { + // backgroundColor: 'red', + // color: 'white' + // } + }, + Print: { + type: 'print', + label: 'Print Table', + state: false, + // styles: { + // backgroundColor: 'yellow', + // } + }, + Columns: { + name: 'columns', + type: 'columns', + label: 'Columns', + visible: true, + state: false, + // styles: { + // button: { + // backgroundColor: '#aaa' + // }, + // dropdownMenu: { + // backgroundColor: 'magento' + // }, + // dropdownItem: { + // backgroundColor: 'pink' + // } + // } + }, + Limiter: { + type: 'limiter', + options: [10, 20, 50, 200, 2000, 0], + default: 200, + // styles: {} + }, + Table: { + styles: { + // table: { + // background: '#000', + // }, + // thead: { + // background: '#000' + // }, + // filters: { + // background: 'blue' + // }, + // // tbody: { + // // background: '#000' + // // }, + // tr: { + // header: { fontWeight: 'normal' }, + // filters: { background: 'green' }, + // body: { }, + // }, + // // th: { + // // background: 'red', + // // textAlign: 'center', + // // ':last-child': { + // // textAlign: 'right' + // // } + // // }, + // td: { + // filters: { backgroundColor: '#000' }, + // // body: { + // // color: '#fff', + // // textAlign: 'center', + // // ':last-child': { + // // textAlign: 'right' + // // } + // // } + // }, + }, + columns: [{ + name: 'ids', + label: '', + sortable: false, + type: 'selection', + // indexField: '@pageId', + width: 50, + extraData: 'selection', + }, { + label: 'ID', + type: 'number', + name: 'pageId', + width: 150, + filterable: true, + sortable: true, + // editable: true + }, { + label: "Status", + type: "options", + name: "entityData.data.status", + sortable: true, + filterable: true, + textAlign: "center", + width: 150, + options: { + "published": { + "label": "Published" + }, + "draft": { + "label": "Draft" + }, + "unpublished": { + "label": "Unpublished" + }, + "pending-review": { + "label": "Pending Review" + }, + "trashed": { + "label": "Trashed" + }, + "archived": { + "label": "Archived" + } + }, + editable: true + // renderer: ({ + // data, + // colConfig: { name, options } + // }) =>
Not specified
+ }, { + label: 'Created at', + type: 'date', + name: 'createdAt', + sortable: true, + textAlign: 'left', + width: 200, + editable: true, + filterable: true, + }, { + label: 'Actions', + type: 'actions', + name: 'actions', + width: 100, + items: [{ + type: 'action', + name: 'edit', + label: 'Edit', + htmlClass: 'btn btn-secondary', + params: { + id: '@id', + }, + thunk: ( config ) => ( dispatch, getState ) => { + console.log('edit', config, getState()); + } + }, { + type: 'action', + name: 'delete', + label: 'Delete', + icon: 'trash-alt', + params: { + id: '@id' + }, + styles: { + backgroundColor: 'red', + color: 'white' + }, + thunk: ( config ) => ( dispatch, getState ) => { + confirm('Are your sure you want to delete this page?') + ? console.log('delete', getState()) + : console.log(false); + + } + }] + }] + } + } +} diff --git a/package-lock.json b/package-lock.json index 186b89d..e536d1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@flipbyte/redux-datatable", - "version": "0.6.1", + "version": "0.6.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e3f2709..54d4d90 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@flipbyte/redux-datatable", - "version": "0.6.1", + "version": "0.6.2", "description": "React-Redux data table", "main": "lib/index.js", "module": "es/index.js", diff --git a/src/components/Body.js b/src/components/Body.js index f44c63b..64a696c 100644 --- a/src/components/Body.js +++ b/src/components/Body.js @@ -76,6 +76,10 @@ const Body = ({ }; }, []); + useEffect(() => { + updateTableDimensions(); + }, [ columns ]) + return ( resultPath export const setParamsEpic = ( action$, state$ ) => action$.pipe( ofType(SET_PAGE, SET_FILTER, SET_SORT, SET_LIMIT), - concatMap((action) => { + mergeMap((action) => { const { meta: { name, routes, reducerName, entity } } = action; return of( @@ -37,7 +37,7 @@ export const setParamsEpic = ( action$, state$ ) => action$.pipe( export const fetchDataEpic = ( action$, state$, { api }) => action$.pipe( ofType(REQUEST_DATA), - switchMap((action) => { + mergeMap((action) => { const { name, routes, entity, reducerName } = action.meta; const query = _.get(state$.value, [reducerName, name]).query; @@ -66,7 +66,7 @@ export const fetchDataEpic = ( action$, state$, { api }) => action$.pipe( )), takeUntil(action$.pipe( ofType(REQUEST_DATA_CANCEL), - filter((cancelAction) => cancelAction.name === name) + filter((cancelAction) => cancelAction.meta.name === name) )) ); }) @@ -74,14 +74,14 @@ export const fetchDataEpic = ( action$, state$, { api }) => action$.pipe( export const deleteDataEpic = ( action$, state$, { api }) => action$.pipe( ofType(DELETE_DATA), - switchMap((action) => { + mergeMap((action) => { const { meta: { name, routes, reducerName, entity }, payload } = action; return api.delete(routes.delete.route, { params: payload.params }).pipe( - concatMap((response) => { + map((response) => { if(!response.success) { return of(createNotification({ type: NOTIFICATION_TYPE_ERROR, message: response.result })); } diff --git a/src/index.js b/src/index.js index 42145bf..0f9659d 100755 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ export reducer from './reducer'; export * as epics from './epics'; export * as utils from './utils'; -export * as action from './actions'; +export * as actions from './actions'; export { default } from './createTable';