/** @format */ /** * External dependencies */ import { __ } from '@wordpress/i18n'; import classnames from 'classnames'; import { Component } from '@wordpress/element'; import { find, findIndex, first, isEqual, noop, partial, uniq } from 'lodash'; import { IconButton, ToggleControl } from '@wordpress/components'; import PropTypes from 'prop-types'; /** * Internal dependencies */ import './style.scss'; import Card from 'components/card'; import CompareButton from 'components/filters/compare/button'; import DowloadIcon from './download-icon'; import EllipsisMenu from 'components/ellipsis-menu'; import { downloadCSVFile, generateCSVDataFromTable, generateCSVFileName } from 'lib/csv'; import { getIdsFromQuery } from 'lib/nav-utils'; import MenuItem from 'components/ellipsis-menu/menu-item'; import MenuTitle from 'components/ellipsis-menu/menu-title'; import Pagination from 'components/pagination'; import Search from 'components/search'; import Table from './table'; import TableSummary from './summary'; /** * This is an accessible, sortable, and scrollable table for displaying tabular data (like revenue and other analytics data). * It accepts `headers` for column headers, and `rows` for the table content. * `rowHeader` can be used to define the index of the row header (or false if no header). * * `TableCard` serves as Card wrapper & contains a card header, ``, ``, and ``. * This includes filtering and comparison functionality for report pages. */ class TableCard extends Component { constructor( props ) { super( props ); const { compareBy, query } = props; this.state = { showCols: props.headers.map( ( { hiddenByDefault } ) => ! hiddenByDefault ), selectedRows: getIdsFromQuery( query[ compareBy ] ), }; this.toggleCols = this.toggleCols.bind( this ); this.onClickDownload = this.onClickDownload.bind( this ); this.onCompare = this.onCompare.bind( this ); this.onSearch = this.onSearch.bind( this ); this.selectRow = this.selectRow.bind( this ); this.selectAllRows = this.selectAllRows.bind( this ); } componentDidUpdate( { query: prevQuery } ) { const { compareBy, query } = this.props; const prevIds = getIdsFromQuery( prevQuery[ compareBy ] ); const currentIds = getIdsFromQuery( query[ compareBy ] ); if ( ! isEqual( prevIds.sort(), currentIds.sort() ) ) { /* eslint-disable react/no-did-update-set-state */ this.setState( { selectedRows: currentIds } ); /* eslint-enable react/no-did-update-set-state */ } } toggleCols( selected ) { const { headers, query, onQueryChange } = this.props; return () => { // Handle hiding a sorted column if ( query.orderby ) { const sortBy = findIndex( headers, { key: query.orderby } ); if ( sortBy === selected ) { const defaultSort = find( headers, { defaultSort: true } ) || first( headers ) || {}; onQueryChange( 'sort' )( defaultSort.key, 'desc' ); } } this.setState( prevState => ( { showCols: prevState.showCols.map( ( toggled, i ) => ( selected === i ? ! toggled : toggled ) ), } ) ); }; } onClickDownload() { const { headers, query, onClickDownload, rows, title } = this.props; const { showCols } = this.state; const visibleHeaders = headers.filter( ( header, i ) => showCols[ i ] ); const visibleRows = rows.map( row => row.filter( ( cell, i ) => showCols[ i ] ) ); // @TODO The current implementation only downloads the contents displayed in the table. // Another solution is required when the data set is larger (see #311). downloadCSVFile( generateCSVFileName( title, query ), generateCSVDataFromTable( visibleHeaders, visibleRows ) ); if ( onClickDownload ) { onClickDownload(); } } onCompare() { const { compareBy, onQueryChange } = this.props; const { selectedRows } = this.state; if ( compareBy ) { onQueryChange( 'compare' )( compareBy, selectedRows.join( ',' ) ); } } onSearch( value ) { const { compareBy, onQueryChange } = this.props; const { selectedRows } = this.state; if ( compareBy ) { const ids = value.map( v => v.id ); onQueryChange( 'compare' )( compareBy, [ ...selectedRows, ...ids ].join( ',' ) ); } } filterCols( rows = [] ) { const { showCols } = this.state; // Header is a 1d array if ( ! Array.isArray( first( rows ) ) ) { return rows.filter( ( col, i ) => showCols[ i ] ); } // Rows is a 2d array return rows.map( row => row.filter( ( col, i ) => showCols[ i ] ) ); } selectAllRows( event ) { const { ids } = this.props; if ( event.target.checked ) { this.setState( { selectedRows: ids, } ); } else { this.setState( { selectedRows: [], } ); } } selectRow( i, event ) { const { ids } = this.props; if ( event.target.checked ) { this.setState( ( { selectedRows } ) => ( { selectedRows: uniq( [ ids[ i ], ...selectedRows ] ), } ) ); } else { this.setState( ( { selectedRows } ) => { const index = selectedRows.indexOf( ids[ i ] ); return { selectedRows: [ ...selectedRows.slice( 0, index ), ...selectedRows.slice( index + 1 ) ], }; } ); } } getCheckbox( i ) { const { ids = [] } = this.props; const { selectedRows } = this.state; const isChecked = -1 !== selectedRows.indexOf( ids[ i ] ); return { display: ( ), value: false, }; } getAllCheckbox() { const { ids = [] } = this.props; const { selectedRows } = this.state; const isAllChecked = ids.length === selectedRows.length; return { cellClassName: 'is-checkbox-column', label: ( ), required: true, }; } render() { const { compareBy, downloadable, labels = {}, onClickDownload, onQueryChange, query, rowHeader, rowsPerPage, summary, title, totalRows, } = this.props; const { selectedRows, showCols } = this.state; const allHeaders = this.props.headers; let headers = this.filterCols( this.props.headers ); let rows = this.filterCols( this.props.rows ); if ( compareBy ) { rows = rows.map( ( row, i ) => { return [ this.getCheckbox( i ), ...row ]; } ); headers = [ this.getAllCheckbox(), ...headers ]; } const className = classnames( { 'woocommerce-table': true, 'has-compare': !! compareBy, } ); return ( { labels.compareButton || __( 'Compare', 'wc-admin' ) } ), compareBy && ( ), ( downloadable || onClickDownload ) && ( { labels.downloadButton || __( 'Download', 'wc-admin' ) } ), ] } menu={ { __( 'Columns:', 'wc-admin' ) } { allHeaders.map( ( { label, required }, i ) => { if ( required ) { return null; } return ( ); } ) } } >
{ summary && } ); } } TableCard.propTypes = { /** * The string to use as a query parameter when comparing row items. */ compareBy: PropTypes.string, /** * An array of column headers (see `Table` props). */ headers: PropTypes.arrayOf( PropTypes.shape( { hiddenByDefault: PropTypes.bool, defaultSort: PropTypes.bool, isSortable: PropTypes.bool, key: PropTypes.string, label: PropTypes.string, required: PropTypes.bool, } ) ), /** * Custom labels for table header actions. */ labels: PropTypes.shape( { compareButton: PropTypes.string, downloadButton: PropTypes.string, helpText: PropTypes.string, placeholder: PropTypes.string, } ), /** * A list of IDs, matching to the row list so that ids[ 0 ] contains the object ID for the object displayed in row[ 0 ]. */ ids: PropTypes.arrayOf( PropTypes.number ), /** * A function which returns a callback function to update the query string for a given `param`. */ onQueryChange: PropTypes.func, /** * Whether the table must be downloadable. If true, the download button will appear. */ downloadable: PropTypes.bool, /** * A callback function called when the "download" button is pressed. Optional, if used, the download button will appear. */ onClickDownload: PropTypes.func, /** * An object of the query parameters passed to the page, ex `{ page: 2, per_page: 5 }`. */ query: PropTypes.object, /** * An array of arrays of display/value object pairs (see `Table` props). */ rowHeader: PropTypes.oneOfType( [ PropTypes.number, PropTypes.bool ] ), /** * Which column should be the row header, defaults to the first item (`0`) (see `Table` props). */ rows: PropTypes.arrayOf( PropTypes.arrayOf( PropTypes.shape( { display: PropTypes.node, value: PropTypes.oneOfType( [ PropTypes.string, PropTypes.number, PropTypes.bool ] ), } ) ) ).isRequired, /** * The total number of rows to display per page. */ rowsPerPage: PropTypes.number.isRequired, /** * An array of objects with `label` & `value` properties, which display in a line under the table. * Optional, can be left off to show no summary. */ summary: PropTypes.arrayOf( PropTypes.shape( { label: PropTypes.node, value: PropTypes.oneOfType( [ PropTypes.string, PropTypes.number ] ), } ) ), /** * The title used in the card header, also used as the caption for the content in this table. */ title: PropTypes.string.isRequired, /** * The total number of rows (across all pages). */ totalRows: PropTypes.number.isRequired, }; TableCard.defaultProps = { downloadable: false, onQueryChange: noop, query: {}, rowHeader: 0, rows: [], }; export default TableCard;