Manual Reference Source

basic/dataTable/DataTable.js

/**
 * Created by pawelposel on 17/11/2016.
 */

import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Form } from 'semantic-ui-react';
import Pagination from '../pagination/Pagination';
import TableAction from './TableAction';
import TableColumn from './TableColumn';
import TableDataCell from './TableDataCell';
import TableDataExpandable from './TableDataExpandable';
import TableFilter from './TableFilter';
import TableRow from './TableRow';
import TableRowExpandable from './TableRowExpandable';
import TableSearch from './TableSearch';

/**
 * DataTable component enables fetching data using predefined function and showing tabular data in a simple manner.
 *
 * ## Features
 * - data pagination
 * - selectable rows
 * - expandable rows
 * - data sorting by columns
 *
 * ## Access
 * `Stage.Basic.DataTable`
 *
 * ## Usage
 *
 * ### DataTable with pagination
 *
 * ![DataTable](manual/asset/dataTable/DataTable_0.png)
 * ```
 * this.props = {
 *      fetchData: ...
 *      data: {
 *          items: [
 *              {
 *                  id: ...,
 *                  blueprint_id: ...,
 *                  created_at: ...,
 *                  isSelected: ...
 *              }
 *              ...
 *          ],
 *          total: ...
 *      },
 *      onSelectDeployment: ...
 * }
 *
 * <DataTable fetchData={this.props.fetchData}
 *            totalSize={this.props.data.total}
 *            pageSize={this.props.widget.configuration.pageSize}
 *            selectable={true}
 *            className="deploymentTable">
 *
 *      <DataTable.Column label="Name" name="id" width="25%"/>
 *      <DataTable.Column label="Blueprint" name="blueprint_id" width="50%"/>
 *      <DataTable.Column label="Created" name="created_at" width="25%"/>
 *
 *      {
 *          this.props.data.items.map((item)=>{
 *              return (
 *                  <DataTable.Row key={item.id} selected={item.isSelected} onClick={()=>this.props.onSelectDeployment(item)}>
 *                      <DataTable.Data><a className='deploymentName' href="javascript:void(0)">{item.id}</a></DataTable.Data>
 *                      <DataTable.Data>{item.blueprint_id}</DataTable.Data>
 *                      <DataTable.Data>{item.created_at}</DataTable.Data>
 *                  </DataTable.Row>
 *              );
 *          })
 *      }
 *
 * </DataTable>
 * ```
 *
 * ### DataTable with action bar
 *
 * ![DataTable](manual/asset/dataTable/DataTable_1.png)
 * ```
 * <DataTable fetchData={this.props.fetchGridData}
 *      totalSize={this.props.data.total}
 *      pageSize={this.props.widget.configuration.pageSize}
 *      sortColumn={this.props.widget.configuration.sortColumn}
 *      sortAscending={this.props.widget.configuration.sortAscending}
 *      selectable={true}>
 *
 *      <DataTable.Column label="Name" name="id" width="30%"/>
 *      <DataTable.Column label="Created" name="created_at" width="15%"/>
 *      <DataTable.Column label="Updated" name="updated_at" width="15%"/>
 *      <DataTable.Column label="Creator" name='created_by' width="15%"/>
 *      <DataTable.Column label="# Deployments" width="15%"/>
 *      <DataTable.Column width="10%"/>
 *
 *      {
 *          this.props.data.items.map((item)=>{
 *              return (
 *                  <DataTable.Row key={item.id} selected={item.isSelected} onClick={()=>this.props.onSelectBlueprint(item)}>
 *                      <DataTable.Data>{item.created_at}</DataTable.Data>
 *                      <DataTable.Data>{item.updated_at}</DataTable.Data>
 *                      <DataTable.Data>{item.created_by}</DataTable.Data>
 *                      <DataTable.Data><div className="ui green horizontal label">{item.depCount}</div></DataTable.Data>
 *                      <DataTable.Data className="center aligned rowActions">
 *                          <i className="rocket icon link bordered" title="Create deployment" onClick={(event)=>{event.stopPropagation();this.props.onCreateDeployment(item)}}></i>
 *                          <i className="trash icon link bordered" title="Delete blueprint" onClick={(event)=>{event.stopPropagation();this.props.onDeleteBlueprint(item)}}></i>
 *                      </DataTable.Data>
 *                  </DataTable.Row>
 *              );
 *          })
 *      }
 *
 *      <DataTable.Action>
 *          <UploadModal widget={this.props.widget} data={this.props.data} toolbox={this.props.toolbox}/>
 *      </DataTable.Action>
 *
 *  </DataTable>
 * ```
 *
 * ### DataTable with expandable row and without pagination
 *
 * ![DataTable](manual/asset/dataTable/DataTable_2.png)
 * ```
 * <DataTable selectable={true}>
 *
 *      <DataTable.Column label="Name" name="id" width="40%"/>
 *      <DataTable.Column label="Date" name="date" width="30%"/>
 *      <DataTable.Column width="30%"/>
 *
 *      <DataTable.Row key="drupal" selected={false} onClick={()=>this.onRowClick(item)}>
 *          <DataTable.Data><a href="javascript:void(0)">Drupal application</a></DataTable.Data>
 *          <DataTable.Data>2016-03-04</DataTable.Data>
 *          <DataTable.Data>description for portal</DataTable.Data>
 *      </DataTable.Row>
 *
 *      <DataTable.Row key="wordpress" selected={false} onClick={()=>this.onRowClick(item)}>
 *          <DataTable.Data><a href="javascript:void(0)">Wordpress blog</a></DataTable.Data>
 *          <DataTable.Data>2016-01-05</DataTable.Data>
 *          <DataTable.Data>description for blog</DataTable.Data>
 *      </DataTable.Row>
 *
 *      <DataTable.Row key="joomla" selected={false} onClick={()=>this.onRowClick(item)}>
 *          <DataTable.Data><a href="javascript:void(0)">Joomla website</a></DataTable.Data>
 *          <DataTable.Data>2015-08-14</DataTable.Data>
 *          <DataTable.Data>description for website</DataTable.Data>
 *      </DataTable.Row>
 *
 *      <DataTable.RowExpandable key="prestashop" expanded={true}>
 *          <DataTable.Row key="prestashop" selected={true} onClick={()=>this.onRowClick(item)}>
 *              <DataTable.Data><a href="javascript:void(0)">Prestashop store</a></DataTable.Data>
 *              <DataTable.Data>2017-01-05</DataTable.Data>
 *              <DataTable.Data>description for e-commerce solution</DataTable.Data>
 *          </DataTable.Row>
 *          <DataTable.DataExpandable>
 *              additional info when row becomes expanded
 *          </DataTable.DataExpandable>
 *      </DataTable.RowExpandable>
 *
 * </DataTable>
 * ```
 * ### No data available if total size is 0 or noDataAvailable prop is set
 *
 * ![DataTable](manual/asset/dataTable/DataTable_3.png)
 * ```
 * <DataTable noDataAvailable={this.props.data.items.length == 0}>
 *      <DataTable.Column label="Name" name="id" width="25%"/>
 *      <DataTable.Column label="Blueprint" name="blueprint_id" width="50%"/>
 *      <DataTable.Column label="Created" name="created_at" width="25%"/>
 *
 *      {
 *          this.props.data.items.map((item)=>{
 *              return (
 *                  <DataTable.Row key={item.id} selected={item.isSelected} onClick={()=>this.props.onSelectDeployment(item)}>
 *                      <DataTable.Data><a className='deploymentName' href="javascript:void(0)">{item.id}</a></DataTable.Data>
 *                      <DataTable.Data>{item.blueprint_id}</DataTable.Data>
 *                      <DataTable.Data>{item.created_at}</DataTable.Data>
 *                  </DataTable.Row>
 *              );
 *          })
 *      }
 *
 * </DataTable>
 * ```
 * ### Show search field
 *
 * ![DataTable](manual/asset/dataTable/DataTable_4.png)
 * ```
 * <DataTable searchable>
 *      ...
 * </DataTable>
 * ```
 */
export default class DataTable extends Component {
    /**
     * Data row, see {@link TableRow}
     */
    static Row = TableRow;

    /**
     * Header column, see {@link TableColumn}
     */
    static Column = TableColumn;

    /**
     * Data column, see {@link TableDataCell}
     */
    static Data = TableDataCell;

    /**
     * Table action, see {@link TableAction}
     */
    static Action = TableAction;

    /**
     * Table filter, see {@link TableFilter}
     */
    static Filter = TableFilter;

    /**
     * Expandable row, see {@link TableRowExpandable}
     */
    static RowExpandable = TableRowExpandable;

    /**
     * Expandable data, see {@link TableDataExpandable}
     */
    static DataExpandable = TableDataExpandable;

    constructor(props, context) {
        super(props, context);

        this.paginationRef = React.createRef();

        this.state = {
            sortColumn: props.sortColumn,
            sortAscending: props.sortAscending,
            searchText: '',
            searching: false
        };

        this.debouncedSearch = _.debounce(
            () => {
                this.paginationRef.current.reset(() => {
                    return Promise.resolve(this._fetchData()).then(() => this.setState({ searching: false }));
                });
            },
            300,
            { maxWait: 2000 }
        );
    }

    /**
     * @property {object[]} children - table content
     * @property {Function} [fetchData] - used to fetch table data
     * @property {number} [totalSize=-1] - total number of rows in table, if not specified pagination will not be set. It is used to calculate pagination pages.
     * @property {Function} [fetchSize=-1] - if total number is unknown size of fetched data can be provided.
     * Pagination pages will be added dynamically until fetchSize is not equal to page size
     * @property {number} [pageSize=0] - number of displayed rows on page
     * @property {string} [sortColumn=] - column name used for data sorting
     * @property {string} [sortAscending=true] - true for ascending sort, false for descending sort
     * @property {boolean} [searchable=false] - if true filtering and searching input to be added
     * @property {boolean} [selectable=false] - if true row can be selected and highlighted
     * @property {string} [className=] - name of the style class to be added
     * @property {boolean} [noDataAvailable=false] - if true no data available message is shown
     * @property {number} [sizeMultiplier=5] - param related to pagination.
     * List of page sizes is generated as multiplication of basic fixed values [1, 2, 3, 5, 10] by this param
     */
    static propTypes = {
        children: PropTypes.any.isRequired,
        fetchData: PropTypes.func,
        totalSize: PropTypes.number,
        fetchSize: PropTypes.number,
        pageSize: PropTypes.number,
        sortColumn: PropTypes.string,
        sortAscending: PropTypes.bool,
        searchable: PropTypes.bool,
        selectable: PropTypes.bool,
        className: PropTypes.string,
        noDataAvailable: PropTypes.bool,
        sizeMultiplier: PropTypes.number,
        noDataMessage: PropTypes.string
    };

    static defaultProps = {
        fetchData: () => Promise.resolve(),
        totalSize: -1,
        fetchSize: -1,
        pageSize: 0,
        sortColumn: '',
        sortAscending: true,
        searchable: false,
        selectable: false,
        className: '',
        noDataAvailable: false,
        sizeMultiplier: 5,
        noDataMessage: 'No data available'
    };

    static childContextTypes = {
        getSortColumn: PropTypes.func,
        isSortAscending: PropTypes.func,
        sortColumn: PropTypes.func
    };

    getChildContext() {
        return {
            getSortColumn: () => this.state.sortColumn,
            isSortAscending: () => this.state.sortAscending,
            sortColumn: name => this._sortColumn(name)
        };
    }

    _sortColumn(name) {
        let ascending = this.state.sortAscending;

        if (this.state.sortColumn === name) {
            ascending = !ascending;
        } else {
            ascending = true;
        }

        const fetchData = { sortColumn: name, sortAscending: ascending, currentPage: 1 };
        this.setState(fetchData, () => {
            this.paginationRef.current.reset(this._fetchData.bind(this));
        });
    }

    componentDidUpdate(prevProps) {
        const changedProps = {};
        if (prevProps.sortColumn !== this.props.sortColumn) {
            changedProps.sortColumn = this.props.sortColumn;
        }
        if (prevProps.sortAscending !== this.props.sortAscending) {
            changedProps.sortAscending = this.props.sortAscending;
        }

        if (!_.isEmpty(changedProps)) {
            this.setState(changedProps);
        }
    }

    _fetchData() {
        return this.props.fetchData({
            gridParams: {
                _search: this.state.searchText,
                currentPage: this.paginationRef.current.state.currentPage,
                pageSize: this.paginationRef.current.state.pageSize,
                sortColumn: this.state.sortColumn,
                sortAscending: this.state.sortAscending
            }
        });
    }

    render() {
        const headerColumns = [];
        const bodyRows = [];
        let gridAction = null;
        const gridFilters = [];

        const showCols = [];
        React.Children.forEach(this.props.children, function(child) {
            if (child) {
                if (child.type === TableColumn) {
                    showCols.push(child.props.show);
                    headerColumns.push(child);
                } else if (child.type === TableRow) {
                    bodyRows.push(React.cloneElement(child, { showCols }));
                } else if (child.type === TableRowExpandable) {
                    const expandableContent = [];
                    React.Children.forEach(child.props.children, function(expChild) {
                        if (expChild) {
                            if (expChild.type === TableRow) {
                                bodyRows.push(React.cloneElement(expChild, { showCols }));
                            } else if (expChild.type === TableDataExpandable && child.props.expanded) {
                                expandableContent.push(
                                    React.cloneElement(expChild, { numberOfColumns: showCols.length })
                                );
                            }
                        }
                    });
                    bodyRows.push(expandableContent);
                } else if (child.type === TableAction) {
                    gridAction = child;
                } else if (child.type === TableFilter) {
                    gridFilters.push(child);
                }
            }
        });

        return (
            <div className={`gridTable ${this.props.className}`}>
                {(this.props.searchable || !_.isEmpty(gridFilters) || gridAction) && (
                    <Form size="small" as="div">
                        <Form.Group inline>
                            {this.props.searchable && (
                                <TableSearch
                                    search={this.state.searchText}
                                    searching={this.state.searching}
                                    onSearch={searchText =>
                                        this.setState({ searchText, searching: true }, this.debouncedSearch)
                                    }
                                />
                            )}
                            {gridFilters}
                            {gridAction}
                        </Form.Group>
                    </Form>
                )}

                <Pagination
                    totalSize={this.props.totalSize}
                    pageSize={this.props.pageSize}
                    sizeMultiplier={this.props.sizeMultiplier}
                    fetchSize={this.props.fetchSize}
                    fetchData={this._fetchData.bind(this)}
                    ref={this.paginationRef}
                >
                    <table
                        className={`ui very compact table sortable ${this.props.selectable ? 'selectable' : ''} ${
                            this.props.className
                        }`}
                        cellSpacing="0"
                        cellPadding="0"
                    >
                        <thead>
                            <tr>{headerColumns}</tr>
                        </thead>
                        {this.props.noDataAvailable ||
                        (this.props.totalSize <= 0 &&
                            this.props.fetchSize <= 0 &&
                            (this.props.totalSize === 0 || this.props.fetchSize === 0)) ? (
                            <tbody>
                                <tr className="noDataRow">
                                    <td colSpan={headerColumns.length} className="center aligned">
                                        {this.props.fetchSize === 0 &&
                                        this.paginationRef.current &&
                                        this.paginationRef.current.state.currentPage > 1 ? (
                                            <span>No more data available</span>
                                        ) : (
                                            <span>{this.props.noDataMessage}</span>
                                        )}
                                    </td>
                                </tr>
                            </tbody>
                        ) : (
                            <tbody>{bodyRows}</tbody>
                        )}
                    </table>
                </Pagination>
            </div>
        );
    }
}