modules_DataGrid_index.js

import { React, PropTypes, classnames } from '@gravityforms/libraries';
import { spacerClasses, uniqueId } from '@gravityforms/utils';
import Heading from '../../elements/Heading';
import GridColumns from './GridColumns';
import GridControls from './GridControls';
import GridEmpty from './GridEmpty';
import GridRow from './GridRow';
import useStore from './store';
import { getModules } from './utils';

const { forwardRef, useState } = React;

/**
 * @module DataGrid
 * @description Renders a complex Data Grid component.
 *
 * @since 3.3.0
 *
 * @param {object}                     props                           Component props.
 * @param {object}                     props.columns                   Array of column objects. Supply: component, key, props, sortable (optional), hideAt (optional). Key is used to match data keys for cells.
 * @param {object}                     props.columnRowAttributes       Custom attributes for the column row.
 * @param {string|Array|object}        props.columnRowClasses          Custom classes for the column row.
 * @param {object}                     props.columnStyleProps          Style props for the column.
 * @param {object}                     props.customAttributes          Custom attributes for the component
 * @param {string|Array|object}        props.customClasses             Custom classes for the component.
 * @param {object}                     props.data                      The data for the component.
 * @param {number}                     props.dataPerPage               The number of data rows to show per page.
 * @param {object}                     props.dataRowAttributes         Custom attributes for the data row.
 * @param {string|Array|object}        props.dataRowClasses            Custom classes for the data row.
 * @param {string}                     props.dataRowMinHeight          The minimum height for the data row.
 * @param {JSX.Element}                props.EmptyImage                The image to show when there is no data.
 * @param {boolean}                    props.emptyMessageAttributes    Custom attributes for the empty message.
 * @param {string|Array|object}        props.emptyMessageClasses       Custom classes for the empty message.
 * @param {boolean}                    props.equalGrid                 Whether the grid should be equal.
 * @param {string|Array|object}        props.gridControlWrapperClasses Custom classes for the grid control wrapper.
 * @param {Function}                   props.handleGridClicks          Passed handler for grid clicks if the field type supports it.
 * @param {boolean}                    props.highlightHover            Whether the grid should highlight a row on hover.
 * @param {boolean}                    props.highlightSelected         Whether the grid should highlight a selected row.
 * @param {object}                     props.i18n                      Language strings for the component.
 * @param {string}                     props.i18n.emptyMessageI18n     The empty message for the data grid.
 * @param {string}                     props.i18n.emptyTitleI18n       The empty title for the data grid.
 * @param {string}                     props.i18n.gridHeadingI18n      The heading for the data grid.
 * @param {string}                     props.id                        The ID for the component.
 * @param {boolean}                    props.isLoading                 Whether the data grid is loading.
 * @param {boolean}                    props.maintainHeight            Whether the grid should maintain height at all times.
 * @param {Array}                      props.modules                   The modules for the data grid.
 * @param {object}                     props.moduleAttributes          Custom attributes for the modules.
 * @param {Function}                   props.setGridData               Passed handler for field changes if the field type supports it.
 * @param {boolean}                    props.showColumns               Whether to show the column row(s).
 * @param {boolean}                    props.showColumnsInFooter       Whether the column row should be in the footer as well.
 * @param {boolean}                    props.sortable                  Whether the grid should be sortable.
 * @param {string|number|Array|object} props.spacing                   The spacing for the component, as a string, number, array, or object.
 * @param {object}                     props.titleAttributes           Custom attributes for the title.
 * @param {string|Array|object}        props.titleClasses              Custom classes for the title.
 * @param {boolean}                    props.useAjax                   Whether to use ajax for the data grid.
 * @param {object|null}                ref                             Ref to the component.
 *
 * @return {JSX.Element} The DataGrid component.
 *
 * @example
 * import DataGrid from '@gravityforms/components/react/admin/modules/DataGrid';
 *
 * return <DataGrid />;
 *
 */
const DataGrid = forwardRef( ( props, ref ) => {
	const {
		columns = [],
		// columnRowAttributes = {},
		// columnRowClasses = [],
		// columnStyleProps = {},
		customAttributes = {},
		customClasses = [],
		data = [],
		dataPerPage = 20,
		// dataRowAttributes = {},
		// dataRowClasses = [],
		// dataRowMinHeight = '71px',
		// EmptyImage = null,
		// emptyMessageAttributes = {},
		// emptyMessageClasses = [],
		equalGrid = false,
		gridControlWrapperClasses = [],
		// handleGridClicks = () => {},
		highlightHover = true,
		highlightSelected = true,
		i18n = {},
		id = '',
		isLoading = false,
		maintainHeight = false,
		modules = [],
		// moduleAttributes = {},
		// setGridData = () => {},
		// showColumns = true,
		// showColumnsInFooter = true,
		sortable = false,
		spacing = '',
		titleAttributes = {},
		titleClasses = [],
		useAjax = false,
	} = props;

	const loadingData = useStore( ( state ) => state.loadingData );
	const moduleState = useStore( ( state ) => state.moduleState );
	const [ sortColumn, setSortColumn ] = useState( null ); /* eslint-disable-line no-unused-vars */
	const [ sortDirection, setSortDirection ] = useState( null ); /* eslint-disable-line no-unused-vars */

	const state = {
		sortColumn,
		setSortColumn,
		sortDirection,
		setSortDirection,
	};

	const propsWithState = {
		...props,
		...state,
	};

	const { gridHeadingI18n = '' } = i18n;

	const gridId = id || uniqueId( 'data-grid-' );

	const {
		BulkActions,
		Filters,
		Pagination,
	} = getModules( modules, [ 'BulkActions', 'Filters', 'Pagination' ] );

	const componentProps = {
		className: classnames( {
			'gform-data-grid': true,
			'gform-data-grid--highlight-hover': highlightHover,
			'gform-data-grid--highlight-selected': highlightSelected,
			'gform-data-grid--equal-grid': equalGrid,
			'gform-data-grid--loading': loadingData,
			'gform-data-grid--empty': data.length === 0,
			...spacerClasses( spacing ),
		}, customClasses ),
		ref,
		id: gridId,
		...customAttributes,
	};

	const titleProps = {
		customClasses: classnames( {
			'gform-data-grid__title': true,
		}, titleClasses ),
		size: 'text-lg',
		tagName: 'h3',
		weight: 'medium',
		...titleAttributes,
	};

	const columnStyles = columns.map( ( column, index ) =>
		column.hideAt
			? `@media (max-width: ${ column.hideAt }px) { #${ gridId } .gform-data-grid__column-${ index } { display: none; } }`
			: ''
	).join( '\n' );

	let displayedData = [];
	const sortedData = [ ...data ];

	if ( data.length === 0 ) {
		displayedData = Array.from( { length: dataPerPage } ).map( () => ( {} ) );
	} else {
		if ( sortable && sortColumn ) {
			sortedData.sort( ( a, b ) => {
				if ( a[ sortColumn ] < b[ sortColumn ] ) {
					return sortDirection === 'asc' ? -1 : 1;
				}
				if ( a[ sortColumn ] > b[ sortColumn ] ) {
					return sortDirection === 'asc' ? 1 : -1;
				}
				return 0;
			} );
		}

		if ( ! useAjax ) {
			const currentPage = moduleState?.currentPage || 0;
			displayedData = sortedData.slice( currentPage * dataPerPage, ( currentPage + 1 ) * dataPerPage );
		} else {
			displayedData = data;
		}

		if ( maintainHeight && displayedData.length < dataPerPage ) {
			const fillerRows = Array.from( { length: dataPerPage - displayedData.length } ).map( () => ( {} ) );
			displayedData = [ ...displayedData, ...fillerRows ];
		}
	}

	const gridLocked = Boolean( data.length === 0 || loadingData || isLoading );

	return (
		<>
			<style>{ columnStyles }</style>
			<div { ...componentProps }>
				{ gridHeadingI18n && <Heading { ...titleProps }>{ gridHeadingI18n }</Heading> }
				<GridControls
					displayedData={ displayedData }
					gridLocked={ gridLocked }
					wrapperClasses={ gridControlWrapperClasses }
					{ ...propsWithState }
				/>
				{ Filters && <Filters.ActiveFilters { ...propsWithState } /> }
				<GridColumns
					location="header"
					displayedData={ displayedData }
					gridLocked={ gridLocked }
					{ ...propsWithState }
					id={ gridId }
				/>
				{ BulkActions && <BulkActions.BulkSelectNotice gridLocked={ gridLocked } { ...propsWithState } /> }
				<div className="gform-data-grid__data">
					{ displayedData.map( ( row, rowIndex ) => (
						<GridRow
							key={ `${ gridId }-gform-data-grid__data-row--${ rowIndex }` }
							row={ row }
							rowIndex={ rowIndex }
							displayedData={ displayedData }
							gridLocked={ gridLocked }
							{ ...propsWithState }
							id={ gridId }
						/>
					) ) }
					<GridEmpty { ...propsWithState } />
				</div>
				<GridColumns
					location="footer"
					displayedData={ displayedData }
					gridLocked={ gridLocked }
					{ ...propsWithState }
					id={ gridId }
				/>
				{ Pagination && <Pagination.Pagination { ...propsWithState } /> }
			</div>
		</>
	);
} );

DataGrid.propTypes = {
	columns: PropTypes.arrayOf(
		PropTypes.shape( {
			key: PropTypes.string.isRequired,
			component: PropTypes.string.isRequired,
			props: PropTypes.object,
			sortable: PropTypes.bool,
			hideAt: PropTypes.number,
		} )
	),
	columnRowAttributes: PropTypes.object,
	columnRowClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	columnStyleProps: PropTypes.object,
	customAttributes: PropTypes.object,
	customClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	data: PropTypes.arrayOf( PropTypes.object ),
	dataPerPage: PropTypes.number,
	dataRowAttributes: PropTypes.object,
	dataRowClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	dataRowMinHeight: PropTypes.string,
	EmptyImage: PropTypes.oneOfType( [
		PropTypes.node,
		PropTypes.func,
		PropTypes.object,
	] ),
	emptyMessageAttributes: PropTypes.object,
	emptyMessageClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	equalGrid: PropTypes.bool,
	gridControlWrapperClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	handleGridClicks: PropTypes.func,
	highlightHover: PropTypes.bool,
	highlightSelected: PropTypes.bool,
	i18n: PropTypes.object,
	id: PropTypes.string,
	isLoading: PropTypes.bool,
	maintainHeight: PropTypes.bool,
	modules: PropTypes.arrayOf( PropTypes.object ),
	moduleAttributes: PropTypes.object,
	setGridData: PropTypes.func,
	showColumns: PropTypes.bool,
	showColumnsInFooter: PropTypes.bool,
	sortable: PropTypes.bool,
	spacing: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.number,
		PropTypes.array,
		PropTypes.object,
	] ),
	titleAttributes: PropTypes.object,
	titleClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	useAjax: PropTypes.bool,
};

DataGrid.displayName = 'DataGrid';

export default DataGrid;