diff --git a/README.md b/README.md index dbbd7c9..b09f85a 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,11 @@ using REST API. - 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 +- Easily configurable layout - 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. +- Easily styleable with styled-components and/or external css. - Show or hide columns dynamically using the Columns item in the toolbar. @@ -49,146 +48,250 @@ import { reducer, epics } from '@flipbyte/redux-datatable'; ```javascript { name: 'your_table_name', // this is the key used to set your table data inside the table reducer - height: 500, + height: 400, rowHeight: 50, - pagination: { - // visible: true, // or an object { top: true, bottom: false } default visible - items: { - limiter: { - type: 'limiter', - visible: true, - position: 10, - options: [10, 20, 50, 200, 2000], - default: 200, - }, - pages: { - type: 'pages', - visible: true, - position: 20, - right: true, - }, - resultCount: { - type: 'resultCount', - visible: true, - position: 30, - right: true, - }, - } - }, + editing: false, + primaryKey: 'id', 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' - } - }, - ... + 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. - [{ + 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: { ... }, + spinner: { ... } + } + }, + ResultCount: { + styles: { ... } + }, + Pages: { + styles: { + first: { ... }, + last: { ... }, + previous: { ... }, + next: { ... }, + pageNumber: { ... }, + } + }, + 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: { ... }, + hide: { ... }, + save: { ... } + } + // renderer: ( props ) => { ... } + }, + MassActions: { + name: 'actions', + label: 'Actions', + id: 'dropdown', + styles: { + button: { ... }, + dropdownMenu: { ... }, + dropdownItem: { ... } + } + options: [{ + type: 'action', + name: 'delete', + label: 'Delete', + styles: { ... }, + 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 ) => { ... }, + styles: { ... } + }, + ResetFilters: { type: 'reset-filters', label: 'Reset Filters', - visible: true, state: false, - }, { + styles: { ... } + }, + Print: { + type: 'print', + label: 'Print Table', + state: false, + styles: { ... } + }, + Columns: { + name: 'columns', type: 'columns', label: 'Columns', visible: true, state: false, + styles: { + button: { ... }, + dropdownMenu: { ... }, + dropdownItem: { ... } + } }, - ... - ] - ... - ], - 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: "Status", - type: "options", - name: "entityData.data.status", - sortable: true, - filterable: true, - textAlign: "center", - options: { - "published": { - label: "Published" + Limiter: { + type: 'limiter', + options: [10, 20, 50, 200, 2000, 0], + default: 200, + styles: { ... } + }, + Table: { + styles: { + table: { ... }, + thead: { ... }, + tbody: { ... }, + filters: { ... }, + tr: { + header: { ... }, + filters: { ... }, + body: { ... } + }, + th: { ... }, + td: { + filters: { ... }, + body: { ... } + } }, - "draft": { - label: "Draft" + columns: [{ + name: 'ids', + label: '', + sortable: false, + type: 'selection', + width: 50, + }, { + label: 'ID', + type: 'number', + name: 'id', + sortable: true, + width: 150, + filterable: true, + sortable: true, + }, { + label: "Status", + type: "options", + name: "status", + sortable: true, + filterable: true, + textAlign: "center", + options: { + "published": { + label: "Published" + }, + "draft": { + label: "Draft" + }, + "archived": { + label: "Archived" + }, + ... + } + }, { + 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()); + }, + style: { ... } + }, { + 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); + }, + style: { ... } + }, + ... + ] }, - "archived": { - label: "Archived" - } + ... + ] } - }, { - 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); - } - }, - ... - ] - }, - ... - ] + } } ``` @@ -211,33 +314,11 @@ const YourComponent = () => | 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 | -| styles | object | false | {} | Custom styles for your table | -| editable | boolean | false | {} | Set whether the table is editable | -| editing | boolean | 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. | - -#### Pagination object - -| Key | Type | Required | Default | Description | -| ------- | -------------- | -------- | ------- | ---------------------------------------------------------------------------------- | -| items | object | false | {} | Items 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 object - -| Key | Type | Required | Default | Description | -| ---------------------------- | ------- | -------- | ------- | ------------------------------------------------- | -| type | string | true | - | One of the following: limiter, pages, resultCount | -| visible | boolean | false | true | Whether the item is visible | -| **Limiter specific options** | | | | | -| options | array | true | - | Array of integers with limiter options | -| default | array | true | - | One of the values in the limiter options key | +| components | object | true | - | All the components required for your table | +| layout | array | true | - | The layout of your table | +| editing | boolean | 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. | #### Routes object @@ -250,30 +331,169 @@ const YourComponent = () => | 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 +#### Components object + +Components can be defined within this object as key value pairs, where `key` is the id of the component and needs to be unique and `value` is a configuration object for the specific component. +All available components are listed below with their configuration. Custom components can be added and existing components can be overridden by using the key `renderer` in the configuration object of the component. +Please check the example table config object above. + +#### Layout array + +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. + +#### Available Components + +**_Common Properties_** + +| Key | Type | Required | Default | Description | +| -------- | -------- | -------- | ------- | ------------------------------ | +| styles | object | false | {} | styled-component styles object | +| renderer | function | false | - | returns a react component | +| type | string | true | - | the type of the object | + +##### Loader + +Note: This component cannot be added to the layout and does not have any other properties except styles. + +**_Styles object properties_** -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 | +| ------- | ------ | -------- | ------- | ------------------------------ | +| mask | object | false | {} | styled-component styles object | +| spinner | object | false | {} | styled-component styles object | -| Key | Type | Required | Default | Description | -| ---------------------- | -------- | -------- | ------------------------------------------------------------- | ------------------------------------------------------------------------- | -| type | string | false | actions | Available values resetFilters, print, editable 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 | -| **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. | -| **For type: editable** | | | | | -| labels | object | false | { show: 'Make editable', hide: 'Hide editable', save: 'Save'} | Labels for each of the buttons enabled when the table is editable | +##### ResultCount -Note: action of type "editable" is required when you set the table to be editable +No unique properties -#### Columns object +##### Pages + +**_Styles object properties_** + +| Key | Type | Required | Default | Description | +| ---------- | ------ | -------- | ------- | ------------------------------ | +| first | object | false | {} | styled-component styles object | +| last | object | false | {} | styled-component styles object | +| previous | object | false | {} | styled-component styles object | +| next | object | false | {} | styled-component styles object | +| pageNumber | object | false | {} | styled-component styles object | + +##### Editable (type: editable) + +Toggles the table between editable and non-editable and shows a save button when the content of the table is modified + +**_Properties_** + +| Key | Type | Required | Default | Description | +| ------ | -------- | -------- | ----------- | ----------------------------------------------- | +| labels | object | false | check below | check below | +| save | function | false | - | ( config ) => ( dispatch, getState ) => { ... } | + +**_Labels object properties_** + +| Key | Type | Required | Default | Description | +| ---- | ------ | -------- | ------------- | ----------------------------------------------- | +| show | string | false | Make editable | Label for the button to show editable table | +| hide | string | false | Hide editable | Label for the button to hide editable table | +| save | string | false | Save | Label for the button to save the modified table | + +**_Styles object properties_** + +| Key | Type | Required | Default | Description | +| ---- | ------ | -------- | ------- | ------------------------------ | +| show | object | false | {} | styled-component styles object | +| hide | object | false | {} | styled-component styles object | +| save | object | false | {} | styled-component styles object | + +##### Actions (type: mass-actions) + +**_Properties_** + +| Key | Type | Required | Default | Description | +| ------- | ------ | -------- | ------- | ------------------------------------- | +| options | array | required | \[] | array of actions objects | +| label | string | required | - | Label for the actions dropdown button | + +**_Actions object properties_** + +| Key | Type | Required | Default | Description | +| ----- | -------- | -------- | ------- | ----------------------------------------------- | +| type | string | true | - | action | +| name | string | true | - | Unique name | +| label | string | true | - | Label for the action item | +| thunk | function | true | - | ( config ) => ( dispatch, getState ) => { ... } | + +**_Styles object properties_** + +| Key | Type | Required | Default | Description | +| ------------ | ------ | -------- | ------- | ------------------------------ | +| button | object | false | {} | styled-component styles object | +| dropdownMenu | object | false | {} | styled-component styles object | +| dropdownItem | object | false | {} | styled-component styles object | + +##### Button (type: button) + +**_Properties_** + +| Key | Type | Required | Default | Description | +| ----- | ------- | -------- | ------- | ------------------------------------- | +| label | string | required | - | Label for the actions dropdown button | +| state | boolean | false | false | Whether to pass state to this button | + +##### ResetFilters (type: reset-filters) + +**_Properties_** + +| Key | Type | Required | Default | Description | +| ----- | ------- | -------- | ------- | ------------------------------------- | +| label | string | required | - | Label for the actions dropdown button | +| state | boolean | false | false | Whether to pass state to this button | + +##### Print (type: print) + +Makes the table printable. + +**_Properties_** + +| Key | Type | Required | Default | Description | +| ----- | ------- | -------- | ------- | ------------------------------------- | +| label | string | required | - | Label for the actions dropdown button | +| state | boolean | false | false | Whether to pass state to this button | + +##### Columns (type: columns) + +Shows the columns toggling dropdown. + +**_Properties_** + +| Key | Type | Required | Default | Description | +| ----- | ------- | -------- | ------- | ------------------------------------- | +| label | string | required | - | Label for the actions dropdown button | +| state | boolean | false | false | Whether to pass state to this button | + +**_Styles object properties_** + +| Key | Type | Required | Default | Description | +| ------------ | ------ | -------- | ------- | ------------------------------ | +| button | object | false | {} | styled-component styles object | +| dropdownMenu | object | false | {} | styled-component styles object | +| dropdownItem | object | false | {} | styled-component styles object | + +##### Limiter (type: limiter) + +| Key | Type | Required | Default | Description | +| ------- | ------- | -------- | ------- | --------------------------------------------------------------------- | +| options | array | required | \[] | array of limiter counts | +| default | integer | required | \[] | default limiter option (should be a value in the options array above) | + +##### Table (type: table) + +| Key | Type | Required | Default | Description | +| ------- | ----- | -------- | ------- | ----------------------------------------- | +| columns | array | required | \[] | array of object with column configuration | + +**_Columns object properties_** | Key | Type | Required | Default | Description | | ------------------------------- | ------------ | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -297,24 +517,32 @@ Note: action of type "editable" is required when you set the table to be editabl | label | string | true | - | Label for the action | | thunk | function | true | - | An action creator which is dispatched on action click. Check demo schema. | -#### Styles object - -Styles has the following properties available: - -| Key | Type | Required | Default | Description | -| -------------- | ------------------------------ | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| tableContainer | styled-components style object | false | - | Outer table container | -| table | styled-components style object | false | - | Table component | -| thead | styled-components style object | false | - | Table header component | -| tbody | styled-components style object | false | - | Table body component | -| tr | object | false | - | Table rows - the object can contain the following keys `header`, `filter`, `body`, each of whose values is a styled-components style object | -| th | styled-components style object | false | - | Table header columns | -| td | object | false | - | Table columns - the object contain the following keys `filter`, `body` whose value is a styled-components style object | -| toolbar | object | false | - | Keys `container` and `row` which are styled-components style object and `item` which is an object with keys that are the names of the respective items (as defined in the config) and the value is a styled-components style object | -| pagination | object | false | - | Keys `container` - a styled-components style object and `item` - same as above toolbar item | -| filter | object | false | - | Each key is the name of the column and the value is the styled-components style object | -| body | object | false | - | Same as `filter` (above) | -| loader | object | false | - | There are 2 style-able keys `mask` and `spinner` for the loader overlay and the loading spinner respectively | +**_Styles object properties_** + +| Key | Type | Required | Default | Description | +| ------- | ------ | -------- | ------- | ------------------------------ | +| table | object | false | {} | styled-component styles object | +| thead | object | false | {} | styled-component styles object | +| tbody | object | false | {} | styled-component styles object | +| filters | object | false | {} | styled-component styles object | +| tr | object | false | {} | check below | +| th | object | false | {} | styled-component styles object | +| td | object | false | {} | check below | + +**_tr Styles object properties_** + +| Key | Type | Required | Default | Description | +| ------- | ------ | -------- | ------- | ------------------------------ | +| header | object | false | {} | styled-component styles object | +| filters | object | false | {} | styled-component styles object | +| body | object | false | {} | styled-component styles object | + +**_tr Styles object properties_** + +| Key | Type | Required | Default | Description | +| ------- | ------ | -------- | ------- | ------------------------------ | +| filters | object | false | {} | styled-component styles object | +| body | object | false | {} | styled-component styles object | ## License diff --git a/demo/src/schema/basic.js b/demo/src/schema/basic.js index 2507824..0df46cc 100644 --- a/demo/src/schema/basic.js +++ b/demo/src/schema/basic.js @@ -1,132 +1,13 @@ import React from 'react'; import { MODIFY_DATA, REQUEST_DATA, IS_LOADING } from '../../../src/actions'; +import { getItemIds } from '../../../src/utils'; export default { name: 'posts', height: 400, rowHeight: 50, - filterable: true, - headers: true, - editable: true, editing: false, primaryKey: 'pageId', - // styles: { - // loader: { - // mask: { - // backgroundColor: 'red', - // }, - // spinner: { - // borderTopColor: 'black', - // } - // }, - // tableContainer: { - // fontSize: '16px', - // }, - // // table: { - // // background: '#000', - // // }, - // // thead: { - // // background: '#000' - // // }, - // // tbody: { - // // background: '#000' - // // }, - // // tr: { - // // header: { fontWeight: 'normal' }, - // // filter: { }, - // // body: { }, - // // }, - // // th: { textAlign: 'center', ':last-child': { - // // textAlign: 'right' - // // }}, - // // td: { - // // filter: {}, - // // body: { textAlign: 'center', ':last-child': { - // // textAlign: 'right' - // // }} - // // }, - // toolbar: { - // // item: { - // // backgroundColor: '#000', - // // }, - // // containr: - // // row: - // // item: - // item: { - // actions: { - // marginRight: '5px' - // }, - // columns: { - // float: 'right', - // 'button': { - // background: '#fff', - // color: '#6c757d' - // }, - // '> div > div': { - // right: 0, - // left: '' - // } - // } - // } - // }, - // pagination: { - // // container: - // // item: - // // items: { - // // [name]: - // // }, - // item: { - // pages: { - // float: 'right', - // } - // } - // }, - // filter: { - // // pageId: {' input': { - // // border: '1px solid #000' - // // }} - // // [name]: - // }, - // body: { - // actions: { - // ' button': { - // fontSize: '12px', - // ':last-child': { - // background: '#dc3545', - // color: '#fff', - // ':hover': { - // backgroundColor: '#c82333', - // } - // } - // } - // } - // // [name]: - // } - // }, - pagination: { - // visible: true, // or an object { top: true, bottom: false } default visible - items: { - limiter: { - type: 'limiter', - visible: true, - position: 10, - options: [10, 20, 50, 200, 2000, 0], - default: 200, - }, - pages: { - type: 'pages', - visible: true, - position: 20, - right: true, - }, - resultCount: { - type: 'resultCount', - visible: true, - position: 30, - right: true, - }, - } - }, routes: { get: { route: '/page', @@ -140,19 +21,93 @@ export default { route: '/users/:id' } }, - toolbar: [ - [{ + 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', - indexField: '@id', + styles: { + backgroundColor: 'red', + }, thunk: ( config ) => ( dispatch, getState ) => { // Get current table state. - const tableState = getState()[payload.reducerName][payload.name]; + 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); @@ -165,13 +120,11 @@ export default { type: 'action', name: 'edit', label: 'Edit this field', - indexField: '@id' - }], - visible: true - }, { + }] + }, + SimpleButton: { type: 'button', label: 'Simple Button', - visible: true, state: false, thunk: ( config ) => ( dispatch, getState ) => { const tableState = getState()[config.reducerName][config.name]; @@ -181,128 +134,216 @@ export default { setTimeout(function() { config.action(IS_LOADING)({ value: false }); }, 1000); - } - }, { - type: 'resetFilters', + }, + // styles: { + // backgroundColor: 'green', + // color: 'white' + // } + }, + ResetFilters: { + type: 'reset-filters', label: 'Reset Filters', - visible: true, state: false, - }, { + // styles: { + // backgroundColor: 'red', + // color: 'white' + // } + }, + Print: { type: 'print', label: 'Print Table', - visible: true, state: false, - }, { + // styles: { + // backgroundColor: 'yellow', + // } + }, + Columns: { name: 'columns', type: 'columns', label: 'Columns', visible: true, - state: false - }, { - name: '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. - } - }], - ], - 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" - } + state: false, + // styles: { + // button: { + // backgroundColor: '#aaa' + // }, + // dropdownMenu: { + // backgroundColor: 'magento' + // }, + // dropdownItem: { + // backgroundColor: 'pink' + // } + // } }, - 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' + 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' + // // } + // // } + // }, }, - thunk: ( config ) => ( dispatch, getState ) => { - confirm('Are your sure you want to delete this page?') - ? console.log('delete', getState()) - : console.log(false); + 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: '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: "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 c3fd29f..8bc2616 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@flipbyte/redux-datatable", - "version": "0.5.4", + "version": "0.6.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -748,9 +748,9 @@ } }, "axios-observable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/axios-observable/-/axios-observable-1.1.0.tgz", - "integrity": "sha512-+dmhX0BgbNt27DvsyA9lRmOJpk4yNXK8k2pCjeiFbbVdaiUR9A9g0x49z8O97SI25m5otadmTIrSPUG8bKhDdQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/axios-observable/-/axios-observable-1.1.1.tgz", + "integrity": "sha512-mjrRa8qJSLxTIKqThB7aWpMLZVDKrwYo/PwbbnnOzEo7u7s6n9sUjCYudCR9FVRZ8T3MexgnvXpSjwgdQTylXA==", "dev": true }, "babel-cli": { diff --git a/package.json b/package.json index cf9c74a..194f413 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@flipbyte/redux-datatable", - "version": "0.5.4", + "version": "0.6.0", "description": "React-Redux data table", "main": "lib/index.js", "module": "es/index.js", @@ -40,7 +40,7 @@ }, "devDependencies": { "axios": "^0.19.0", - "axios-observable": "^1.1.0", + "axios-observable": "^1.1.1", "bootstrap": "^4.3.1", "lodash": ">=4.17.11", "nwb": "0.23.x", diff --git a/src/Renderer.js b/src/Renderer.js deleted file mode 100644 index 858146c..0000000 --- a/src/Renderer.js +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; - -import Limiter from './Renderer/Pagination/Limiter'; -import Pages from './Renderer/Pagination/Pages'; -import ResultCount from './Renderer/Pagination/ResultCount'; - -import Button from './Renderer/Toolbar/Button'; -import Print from './Renderer/Toolbar/Print'; -import Columns from './Renderer/Toolbar/Columns'; -import MassActions from './Renderer/Toolbar/MassActions'; -import ResetFilters from './Renderer/Toolbar/ResetFilters'; -import EditableButtons from './Renderer/Toolbar/EditableButtons'; - -import DateFilter from './Renderer/Filter/Date'; -import FilterOptions from './Renderer/Filter/Options'; -import String from './Renderer/Filter/String'; -import Number from './Renderer/Filter/Number'; - -import Text from './Renderer/Body/Text'; -import Date from './Renderer/Body/Date'; -import Image from './Renderer/Body/Image'; -import Actions from './Renderer/Body/Actions'; -import Options from './Renderer/Body/Options'; -import Selection from './Renderer/Body/Selection'; - -import HeaderCol from './Renderer/Header/Column'; -import SelectAll from './Renderer/Header/Selection'; - -const renderers = { - pagination: { - limiter: Limiter, - pages: Pages, - resultCount: ResultCount - }, - toolbar: { - button: Button, - print: Print, - columns: Columns, - resetFilters: ResetFilters, - editable: EditableButtons, - default: MassActions, - }, - filter: { - number: Number, - date: DateFilter, - options: FilterOptions, - default: String - }, - body: { - date: Date, - actions: Actions, - selection: Selection, - options: Options, - image: Image, - default: Text - }, - header: { - selection: SelectAll, - default: HeaderCol - } -}; - -const getRenderers = ( ofType ) => renderers[ofType] || {}; - -const Renderer = ({ ofType, forItem, ...props }) => { - const renderers = getRenderers(ofType); - const Renderer = renderers[forItem] || renderers['default']; - return ; -}; - -export default Renderer; diff --git a/src/Renderer/Body/Selection.js b/src/Renderer/Body/Selection.js deleted file mode 100644 index 899e20f..0000000 --- a/src/Renderer/Body/Selection.js +++ /dev/null @@ -1,35 +0,0 @@ -import _ from 'lodash'; -import React from 'react'; -import { SET_SELECTION } from '../../actions'; -import { getParam, getConfigParam } from '../../utils'; -import { SELECT_ALL } from '../../constants'; -import Field from '../../components/Field'; - -const handleSelection = ({ data, indexField, action }, event ) => { - let paramKey = getConfigParam(indexField); - let key = getParam(indexField, data); - action(SET_SELECTION)({ paramKey, key, value: event.target.checked }); -}; - -const isChecked = ( selection, selected ) => selection.all === true && selected !== false ? true : !!selected; - -const Selection = ({ - action, - data, - extraData: { selection }, - colConfig: { indexField } -}) => { - const dataKey = getConfigParam(indexField); - const key = _.get(data, dataKey); - const value = isChecked(selection, _.get(selection.selected, [dataKey, key])); - return ( -
- -
- ); -}; - -export default Selection; diff --git a/src/Renderer/Header/Selection.js b/src/Renderer/Header/Selection.js deleted file mode 100644 index 09f940e..0000000 --- a/src/Renderer/Header/Selection.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import { SET_SELECTION } from '../../actions'; -import { SELECT_ALL } from '../../constants'; -import { getConfigParam } from '../../utils'; -import Field from '../../components/Field'; - -const handleSelection = ( selector, indexField, event ) => { - let paramKey = getConfigParam(indexField); - if (!paramKey) { - return false; - } - - selector({ paramKey, type: SELECT_ALL, value: event.target.checked }); -}; - -const Selection = ({ name, action, indexField }) => ( - handleSelection(action(SET_SELECTION), indexField, event)} - /> -); - -export default Selection; diff --git a/src/Renderer/Pagination/Pages.js b/src/Renderer/Pagination/Pages.js deleted file mode 100644 index 4934a79..0000000 --- a/src/Renderer/Pagination/Pages.js +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import { SET_PAGE } from '../../actions'; -import styled from 'styled-components'; -import Button from '../../components/Button'; - -export const List = styled.div ` - display: inline-flex; - vertical-align: middle; - background: ${props => props.background || '#fff'}; - border: ${props => props.border || '1px solid rgba(34,36,38,.15)'}; - border-radius: ${props => props.borderRadius || 'none'}; - height: ${props => props.height || '40px'}; -`; - -const NUM_LINKS = 5; - -const fillRange = ( start, end ) => { - return Array(end - start + 1).fill().map((item, index) => start + index); -}; - -const getPages = ( currentPage, total ) => { - var padding = Math.floor(NUM_LINKS / 2); - var left = (currentPage - padding < padding) ? 1 : currentPage - padding; - var right = (left + NUM_LINKS - 1 > total) ? total : left + NUM_LINKS - 1; - - left = (right === total) ? - (right - NUM_LINKS < 1) ? 1 : right - NUM_LINKS + 1 - : left; - - return fillRange(left, right); -}; - -const Pages = ({ page, total, action, style }) => { - const setPage = ( page ) => action(SET_PAGE)({ page }); - return ( - - - - { getPages(page, total).map( (link, index) => - - ) } - - - - ); -}; - -export default Pages; diff --git a/src/Renderer/Pagination/ResultCount.js b/src/Renderer/Pagination/ResultCount.js deleted file mode 100644 index 0b3ac5f..0000000 --- a/src/Renderer/Pagination/ResultCount.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -const lowerLimit = ( page, limit ) => ((page - 1) * limit) + 1; -const upperLimit = ( page, limit, count ) => (page * limit) > count || limit === 0 ? count : page * limit; - -const ResultCount = ({ page, limit, count }) => ( - !!count > 0 && - Showing { lowerLimit(page, limit) } to { upperLimit(page, limit, count) } of { count } entries -); - -export default ResultCount; diff --git a/src/Renderer/Toolbar/Button.js b/src/Renderer/Toolbar/Button.js deleted file mode 100644 index a196fb5..0000000 --- a/src/Renderer/Toolbar/Button.js +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import StyledButton from '../../components/Button'; - -const Button = ({ itemConfig, thunk }) => { - const { thunk: cb, label, name } = itemConfig; - return ( - - { label } - - ); -}; - -export default Button; diff --git a/src/Renderer/Toolbar/Columns.js b/src/Renderer/Toolbar/Columns.js deleted file mode 100644 index 6350a1c..0000000 --- a/src/Renderer/Toolbar/Columns.js +++ /dev/null @@ -1,102 +0,0 @@ -import ReactDOM from 'react-dom'; -import React, { Component } from 'react'; -import Button from '../../components/Button'; -import Dropdown from '../../components/Dropdown'; -import { ADD_COLUMN, REMOVE_COLUMN } from '../../constants'; - -class Columns extends Component { - constructor(props) { - super(props); - - this.state = { open: false }; - this.updateColumns = this.updateColumns.bind(this); - this.toggle = this.toggle.bind(this); - this.manageEvents = this.manageEvents.bind(this); - this.handleDocumentClick = this.handleDocumentClick.bind(this); - } - - updateColumns( value, event ) { - const { internalStateUpdater } = this.props; - - if (event.target.checked) { - internalStateUpdater({ type: ADD_COLUMN, value }); - } else { - internalStateUpdater({ type: REMOVE_COLUMN, value }); - } - } - - componentWillUnmount() { - this.manageEvents(true); - } - - componentDidMount() { - this.manageEvents(); - } - - toggle( e ) { - const { open } = this.state; - // if(isOpen) { - // this.manageEvents(); - // } else { - // this.manageEvents(true); - // } - // - this.setState({ open: !open }); - } - - manageEvents(remove = false) { - var eventUpdater = remove ? document.removeEventListener : document.addEventListener; - - ['click', 'touchstart', 'keyup'].forEach( event => - eventUpdater(event, this.handleDocumentClick, true) - ); - } - - handleDocumentClick(e) { - if (e && (e.which === 3 || (e.type === 'keyup' && e.which !== 9))) { - return; - } - - const container = ReactDOM.findDOMNode(this); - if (container.contains(e.target) && container !== e.target - && (e.type !== 'keyup' || e.which === 9) - ) { - return; - } - - if (this.state.open) { - this.toggle(e); - } - } - - render() { - const { - itemConfig: { style = {} }, - columns = [], - visibleColumns = [] - } = this.props; - const { open } = this.state; - - return ( - - - - - ); - } -} - -export default Columns; diff --git a/src/Renderer/Toolbar/EditableButtons.js b/src/Renderer/Toolbar/EditableButtons.js deleted file mode 100644 index 4aad242..0000000 --- a/src/Renderer/Toolbar/EditableButtons.js +++ /dev/null @@ -1,32 +0,0 @@ -import React, { Fragment } from 'react'; -import Button from '../../components/Button'; -import { TOGGLE_EDITABLE } from '../../constants'; - -const defaultLabels = { - show: 'Make editable', - hide: 'Hide editable', - save: 'Save' -}; - -const EditableButtons = ({ itemConfig, isModified, isEditable, isEditing, internalStateUpdater, thunk }) => { - const { save } = itemConfig; - const labels = _.merge(defaultLabels, itemConfig.labels); - const toggleEditable = () => internalStateUpdater({ type: TOGGLE_EDITABLE }); - const getEditableLabel = () => !isEditing ? labels.show : labels.hide; - return isEditable && ( - - { !isModified && ( - - )} - { isModified && ( - - )} - - ); -}; - -export default EditableButtons; diff --git a/src/Renderer/Toolbar/MassActions.js b/src/Renderer/Toolbar/MassActions.js deleted file mode 100644 index 672397f..0000000 --- a/src/Renderer/Toolbar/MassActions.js +++ /dev/null @@ -1,76 +0,0 @@ -import ReactDOM from 'react-dom'; -import React, { Component } from 'react'; -import Button from '../../components/Button'; -import Dropdown from '../../components/Dropdown'; - -class MassActions extends Component { - constructor( props ) { - super(props); - this.state = { open: false }; - - this.toggle = this.toggle.bind(this); - this.manageEvents = this.manageEvents.bind(this); - this.handleDocumentClick = this.handleDocumentClick.bind(this); - } - - componentWillUnmount() { - this.manageEvents(true); - } - - componentDidMount() { - this.manageEvents(); - } - - toggle( e ) { - const { open } = this.state; - this.setState({ open: !open }); - } - - manageEvents(remove = false) { - var eventUpdater = remove ? document.removeEventListener : document.addEventListener; - - ['click', 'touchstart', 'keyup'].forEach( event => - eventUpdater(event, this.handleDocumentClick, true) - ); - } - - handleDocumentClick(e) { - if (e && (e.which === 3 || (e.type === 'keyup' && e.which !== 9))) { - return; - } - - const container = ReactDOM.findDOMNode(this); - if (container.contains(e.target) && container !== e.target - && (e.type !== 'keyup' || e.which === 9) - ) { - return; - } - - if (this.state.open) { - this.toggle(e); - } - } - - render() { - const { itemConfig, thunk } = this.props; - const { label, name, options, style = {} } = itemConfig; - const { open } = this.state; - - return ( - - - - - ); - } -} - -export default MassActions; diff --git a/src/Renderer/Toolbar/Print.js b/src/Renderer/Toolbar/Print.js deleted file mode 100644 index 62948dc..0000000 --- a/src/Renderer/Toolbar/Print.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import { SET_IS_PRINTING } from '../../constants'; -import Button from '../../components/Button'; - -const Print = ({ itemConfig, internalStateUpdater }) => { - const printTable = () => internalStateUpdater({ type: SET_IS_PRINTING, value: true }); - return ( - - ); -}; - -export default Print; diff --git a/src/Renderer/Toolbar/ResetFilters.js b/src/Renderer/Toolbar/ResetFilters.js deleted file mode 100644 index 37830d5..0000000 --- a/src/Renderer/Toolbar/ResetFilters.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import { SET_FILTER } from '../../actions'; -import Button from '../../components/Button'; - -const ResetFilters = ({ itemConfig, action }) => { - const clearFilter = () => action(SET_FILTER)({ clear: true }); - return ( - - ); -}; - -export default ResetFilters; diff --git a/src/TableProvider.js b/src/TableProvider.js deleted file mode 100644 index 585136d..0000000 --- a/src/TableProvider.js +++ /dev/null @@ -1,42 +0,0 @@ -import React, { Component } from 'react'; -import _ from 'lodash'; - -const ConfigContext = React.createContext({}); - -const TableProvider = ({ config, children }) => ( - { children } -); - -export const TableConsumer = ({ children }) => ( - - { (config) => ( - children({ config }) - )} - -); - -export const withTableConfig = ( paths ) => ( WrappedComponent ) => { - const ComponentWithConfig = ( props ) => ( - - { (config) => { - var tableConfig = {}; - if(_.isObject(paths)) { - _.forEach(paths, ( value, key ) => { - tableConfig = { - ...tableConfig, - [key]: _.get(config.config, value) - }; - }); - } else { - tableConfig = paths ? _.get(config.config, paths, false) : config; - } - - return ; - } } - - ); - - return ComponentWithConfig; -}; - -export default TableProvider; diff --git a/src/components/Body.js b/src/components/Body.js new file mode 100644 index 0000000..8a7150e --- /dev/null +++ b/src/components/Body.js @@ -0,0 +1,206 @@ +import React, { useRef, useEffect } from 'react'; +import { Loader, Tbody, Tr, Td, Div } from '../styled-components'; +import { Body as Renderers } from './Renderer'; +import { getStyles, getRenderer, prepareData, getExtraBodyRowProps, calculateWidth } from '../utils'; +import { MODIFY_DATA } from '../actions'; + +const Body = ({ + columns, + data = [], + rowHeight, + height, + isFetching, + scrollData: { top: startTop }, + setScrollData, + overScanCount = 10, + isPrinting = false, + innerHeight, + visibleHeight = 500, + styles = {}, + schema, + state, + primaryKey, + modified, + tableWidth: { width = '100%', widthAdjustment = 1 }, + setTableWidth, + bodyExtraData = {}, + action, + thunk, + isEditing, + minWidth, + loaderStyles = {}, +}) => { + let slicedData = []; + let startIndex = 0; + if (isPrinting === false) { + const visibleLower = startTop - overScanCount * rowHeight; + const visibleUpper = startTop + visibleHeight + overScanCount * rowHeight; + + startIndex = Math.floor(visibleLower / rowHeight); + if(startIndex < 0) { + startIndex = 0; + } + + const endIndex = Math.ceil(visibleUpper / rowHeight); + slicedData = data.slice(startIndex, endIndex); + } else { + slicedData = data.slice(0) + } + + const ref = useRef(null); + const updateTableDimensions = () => { + const tableBodyEl = ref.current; + const computedTableWidth = minWidth > tableBodyEl.clientWidth || !tableBodyEl.clientWidth + ? minWidth + : tableBodyEl.clientWidth; + + const percentage = computedTableWidth / calculateWidth(columns); + setTableWidth({ + width: calculateWidth(columns, percentage), + widthAdjustment: percentage + }) + } + + const handleScroll = () => ( + setScrollData({ pointerEvents: 'none', top: ref.current.scrollTop, left: -ref.current.scrollLeft }) + ); + + useEffect(() => { + updateTableDimensions(); + ref.current.addEventListener('scroll', handleScroll.bind(this), true); + window.addEventListener('resize', updateTableDimensions); + return () => { + ref.current.addEventListener('scroll', handleScroll.bind(this), true); + window.removeEventListener('resize', updateTableDimensions); + }; + }, []); + + return ( + + { isFetching && } +
+ { slicedData.map((item, index) => { + const rowIndex = startIndex + index; + const top = rowIndex * rowHeight; + const data = prepareData(item, schema, state); + const primarKeyValue = _.get(data, primaryKey); + const modifiedData = modified[primarKeyValue] || {}; + return ( + + {(column, index) => { + const { width, textAlign, name, type } = column; + const ColRenderer = getRenderer(column, Renderers); + const modifiedValue = _.get(modifiedData, name); + const origValue = _.get(data, name); + const value = modifiedValue || origValue; + return ( + +
+ { + var newData = { ...modifiedData }; + _.set(newData, primaryKey, primarKeyValue); + _.set(newData, event.target.name, event.target.value); + action(MODIFY_DATA)({ key: primarKeyValue, value: newData }) + }} + /> +
+ + ); + }} + + ); + })} +
+ + ); +} + +Body.mapPropsToComponent = ({ + config: { + rowHeight, + height, + primaryKey, + components: { + Loader = {}, + Table = {} + } + }, + tableData, + printing: [ isPrinting ], + editing: [ isEditing ], + scroller: [ scrollData, setScrollData ], + entity: { schema } = {}, + state, + action, + thunk, + width: [ tableWidth, setTableWidth ], + minWidth, + visibleColumns +}) => { + const { items: data, isFetching, modified } = tableData; + const totalHeight = rowHeight * (data || []).length; + const visibleHeight = height || totalHeight; + + return ({ + bodyExtraData: getExtraBodyRowProps(tableData, visibleColumns), + columns: visibleColumns, + data, + rowHeight, + scrollData, + setScrollData, + innerHeight: totalHeight, + height: totalHeight > visibleHeight ? visibleHeight : totalHeight, + visibleHeight: visibleHeight, + isFetching, + isPrinting, + schema, + state, + primaryKey, + modified, + action, + thunk, + isEditing, + setScrollData, + setTableWidth, + tableWidth, + minWidth, + loaderStyles: Loader.styles, + styles: Table.styles + }); +} + +export default Body; diff --git a/src/components/Button.js b/src/components/Button.js index 59102ff..56b2426 100644 --- a/src/components/Button.js +++ b/src/components/Button.js @@ -1,72 +1,19 @@ import React from 'react'; -import styled, { css } from 'styled-components'; - -const Button = styled.button ` - display: inline-block; - transition: color 0.15s ease-in-out, - background-color 0.15s ease-in-out, - border-color 0.15s ease-in-out, - box-shadow 0.15s ease-in-out; - vertical-align: middle; - white-space: nowrap; - - ${({ - background = '#fff', - border = '1px solid rgba(34,36,38,.15)', - fontWeight = 400, - lineHeight = 1.5, - fontSize = '14px', - borderRadius = 0, - color = '#6c757d', - padding = '0.375rem 0.75rem', - dropdownToggle = false, - hover = {}, - active, - disabled = false, - }) => css ` - background: ${background}; - border: ${border}; - font-weight: ${fontWeight}; - line-height: ${lineHeight}; - font-size: ${fontSize}; - border-radius: ${borderRadius}; - color: ${color}; - padding: ${padding}; - - ${dropdownToggle && ` - &::after { - display: inline-block; - width: 0; - height: 0; - vertical-align: middle; - content: ''; - border-top: 0.3em solid; - border-right: 0.3rem solid transparent; - border-bottom: 0; - border-left: 0.3rem solid transparent; - margin-left: 0.255rem; - } - `}; - - ${!disabled && !active && css ` - cursor: pointer; - - &:hover { - color: ${hover.color || '#fff'}; - background-color: ${hover.backgroundColor || '#5a6268'}; - } - `}; - - ${active && css ` - background: ${props => props.activeBackground || '#007bff'}; - color: ${props => props.activeColor || '#fff'}; - font-weight: ${props => props.activeFontWeight || 'normal'}; - `}; - - ${disabled && css ` - opacity: 0.5; - `}; - `} -`; +import { Button as StyledButton } from '../styled-components'; + +const Button = ({ config, thunk }) => { + const { thunk: cb, label, name, styles = {} } = config; + return ( + + { label } + + ); +}; + +Button.mapPropsToComponent = ({ thunk }) => ({ thunk }); export default Button; diff --git a/src/components/Columns.js b/src/components/Columns.js new file mode 100644 index 0000000..19be3f7 --- /dev/null +++ b/src/components/Columns.js @@ -0,0 +1,70 @@ +import React, { useEffect } from 'react'; +import withDropdown from '../hoc/withDropdown'; +import { ADD_COLUMN, REMOVE_COLUMN } from '../constants'; +import { Button, Dropdown } from '../styled-components'; + +const updateState = (type, index) => (state) => { + let visibleColumnIds = [ ...state ]; + if (type === ADD_COLUMN) { + visibleColumnIds.push(index); + visibleColumnIds.sort(); + } else { + visibleColumnIds.splice(visibleColumnIds.indexOf(index), 1).sort(); + } + + return visibleColumnIds; +} + +const updateColumns = (setVisibleColumnIds, index, event) => { + if (event.target.checked) { + setVisibleColumnIds(updateState(ADD_COLUMN, index)) + } else { + setVisibleColumnIds(updateState(REMOVE_COLUMN, index)) + } +}; + +const Columns = withDropdown(({ + open, + toggle, + columns, + visibleColumnIds, + setVisibleColumnIds, + config: { + styles = {} + } +}) => ( + + + + +)); + +Columns.mapPropsToComponent = ({ + config: { + components: { + Table: { columns } + } + }, + columns: [ visibleColumnIds, setVisibleColumnIds ] +}) => ({ columns, visibleColumnIds, setVisibleColumnIds }); + +export default Columns; diff --git a/src/components/Editable.js b/src/components/Editable.js new file mode 100644 index 0000000..3dfbf6e --- /dev/null +++ b/src/components/Editable.js @@ -0,0 +1,48 @@ +import React, { Fragment } from 'react'; +import { Button } from '../styled-components'; +import { TOGGLE_EDITABLE } from '../constants'; + +const Editable = ({ config, isModified, isEditing, setIsEditing, thunk }) => { + const { + save, + labels: { + show = 'Make editable', + hide = 'Hide editable', + save: saveLabel = 'Save', + }, + styles = {} + } = config; + const toggleEditable = () => setIsEditing(state => !state); + const getEditableLabel = () => !isEditing ? show : hide; + const getEditableStyles = () => !isEditing ? styles.show : styles.hide; + return ( + + { !isModified && ( + + )} + { isModified && ( + + )} + + ); +}; + +Editable.mapPropsToComponent = ({ + thunk, + tableData, + editing: [ isEditing, setIsEditing ] +}) => ({ isEditing, setIsEditing, thunk, isModified: tableData && !_.isEmpty(tableData.modified) }); + +export default Editable; diff --git a/src/components/Filters.js b/src/components/Filters.js new file mode 100644 index 0000000..ef2b8c1 --- /dev/null +++ b/src/components/Filters.js @@ -0,0 +1,62 @@ +import React from 'react'; +import { Tr, Td, Thead, Div } from '../styled-components'; +import { getStyles, getRenderer } from '../utils'; +import { Filters as Renderers } from './Renderer'; +import { SET_FILTER } from '../actions'; + +const Filters = ({ + columns, + action, + query = {}, + children, + styles = {}, + scrollData: { left }, + tableWidth: { width = '100%', widthAdjustment = 1 } +}) => ( + +
+ + {(config, index) => { + const { filterable, type, width, ...rest } = config; + const { name } = rest; + const value = _.get(query, ['search', name, 'value']); + const Component = getRenderer(config, Renderers); + return ( + + { filterable && Component && ( +
+ action(SET_FILTER)({ key, filter })} + { ...rest } + /> +
+ )} + + ); + }} + +
+ +); + +Filters.mapPropsToComponent = ({ + visibleColumns, + action, + tableData: { query }, + width: [ tableWidth ], + scroller: [ scrollData ], + config: { + components: { + Table: { styles } + } + } +}) => ({ columns: visibleColumns, action, query, tableWidth, scrollData, styles }); + +export default Filters; diff --git a/src/components/Header.js b/src/components/Header.js new file mode 100644 index 0000000..35883b6 --- /dev/null +++ b/src/components/Header.js @@ -0,0 +1,87 @@ +import React from 'react'; +import { Tr, Th, Thead } from '../styled-components'; +import { getStyles, getRenderer } from '../utils'; +import { Header as Renderers } from './Renderer'; +import { SET_SORT } from '../actions'; + +const changeSortOrder = ( query, colName, sorter ) => { + let dir = null; + if ( query.sort !== colName ) { + dir = 'asc'; + } else { + if (query.dir === 'asc') { + dir = 'desc'; + } else if (query.dir === 'desc') { + colName = ''; + dir = ''; + } else { + dir = 'asc'; + } + } + + sorter({ sort: colName, dir }); +}; + +const Header = ({ + columns, + action, + query = {}, + selection, + children, + scrollData: { left }, + styles = {}, + primaryKey, + tableWidth: { width = '100%', widthAdjustment = 1 } +}) => ( + +
+ + {(config, index) => { + const { sortable, width, textAlign, name, type, ...rest } = config; + const { sort, dir } = query; + const Component = getRenderer(config, Renderers); + return ( + + { Component && ( + + )} + + ); + }} + +
+ +); + +Header.mapPropsToComponent = ({ + visibleColumns, + action, + tableData: { query, selection }, + width: [ tableWidth ], + scroller: [ scrollData ], + config: { + primaryKey, + components: { + Table: { styles } + } + } +}) => ({ columns: visibleColumns, action, query, tableWidth, scrollData, selection, styles, primaryKey }); + +export default Header; diff --git a/src/Renderer/Pagination/Limiter.js b/src/components/Limiter.js similarity index 58% rename from src/Renderer/Pagination/Limiter.js rename to src/components/Limiter.js index 99ff17d..f098cb6 100644 --- a/src/Renderer/Pagination/Limiter.js +++ b/src/components/Limiter.js @@ -1,13 +1,17 @@ import React from 'react'; -import { SET_LIMIT } from '../../actions'; -import Field from '../../components/Field'; -import Label from '../../components/Label'; -import { isUndefined } from '../../utils'; +import { SET_LIMIT } from '../actions'; +import { isUndefined } from '../utils'; +import { Field, Label } from '../styled-components'; -const Limiter = ({ options, limit, action, style }) => { +const Limiter = ({ + config: { options }, + limit, + action, + config: { styles } +}) => { const setLimit = ( limit ) => action(SET_LIMIT)({ limit }); return ( -