modules_DataGrid_index.js

import { React, PropTypes, classnames, SimpleBar } from '@gravityforms/libraries';
import { ConditionalWrapper, IdProvider, useIdContext } from '@gravityforms/react-utils';
import { spacerClasses } 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 { getModules } from './utils';

const { forwardRef } = React;

/**
 * @module DataGridComponent
 * @description Renders a complex Data Grid component.
 *
 * @since 3.3.0
 *
 * @param {object}      props Component props.
 * @param {object|null} ref   Ref to the component.
 *
 * @return {JSX.Element} The DataGrid component.
 */
const DataGridComponent = forwardRef( ( props, ref ) => {
	const {
		afterGridHeading = null,
		columns = [],
		customAttributes = {},
		customClasses = [],
		data = [],
		dataPerPage = 20,
		equalGrid = false,
		gridControlWrapperClasses = [],
		highlightHover = true,
		highlightSelected = true,
		i18n = {},
		isLoading = false,
		maintainHeight = false,
		modules = [],
		scrollX = false,
		spacing = '',
		titleAttributes = {},
		titleClasses = [],
	} = props;
	const { gridHeadingI18n = '' } = i18n;
	const id = useIdContext();

	const {
		BulkActions,
		GridPagination,
		ActiveFilters,
	} = getModules( modules, [ 'BulkActions', 'GridPagination', 'ActiveFilters' ] );

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

	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': isLoading,
			'gform-data-grid--empty': data.length === 0,
			...spacerClasses( spacing ),
		}, customClasses ),
		ref,
		id,
		...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) { #${ id } .gform-data-grid__column-${ index } { display: none; } }`
			: ''
	).join( '\n' );

	return (
		<>
			<style>{ columnStyles }</style>
			<article { ...componentProps }>
				{ ( gridHeadingI18n || afterGridHeading ) && (
					<header className="gform-data-grid__header">
						{ gridHeadingI18n && <Heading { ...titleProps }>{ gridHeadingI18n }</Heading> }
						{ afterGridHeading }
					</header>
				) }
				<GridControls
					{ ...props }
					displayedData={ displayedData }
					wrapperClasses={ gridControlWrapperClasses }
				/>
				{ ActiveFilters && <ActiveFilters.ActiveFilters { ...props } /> }
				<ConditionalWrapper
					condition={ scrollX }
					wrapper={ ( ch ) => <SimpleBar>{ ch }</SimpleBar> }
				>
					<GridColumns
						{ ...props }
						location="header"
						displayedData={ displayedData }
						gridId={ id }
					/>
					{ BulkActions && <BulkActions.BulkSelectNotice { ...props } /> }
					<div className="gform-data-grid__data">
						{ displayedData.map( ( row, rowIndex ) => (
							<GridRow
								{ ...props }
								key={ `${ id }-gform-data-grid__data-row--${ rowIndex }` }
								row={ row }
								rowIndex={ rowIndex }
								displayedData={ displayedData }
								gridId={ id }
							/>
						) ) }
						{ ! isLoading && <GridEmpty { ...props } /> }
					</div>
					<GridColumns
						{ ...props }
						location="footer"
						displayedData={ displayedData }
						gridId={ id }
					/>
				</ConditionalWrapper>
				{ GridPagination && <GridPagination.GridPagination { ...props } /> }
			</article>
		</>
	);
} );

/**
 * @module DataGrid
 * @description Renders a complex Data Grid component with id wrapper. See modules for information on how to use them.
 *
 * @since 3.3.0
 *
 * @param {object}                     props                           Component props.
 * @param {JSX.Element}                props.afterGridHeading          Element to render after the grid heading.
 * @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 {boolean}                    props.gridLocked                Whether the grid should be locked.
 * @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 {object}                     props.moduleState               The state for the modules.
 * @param {boolean}                    props.scrollX                   Whether the grid should have horizontal scrolling.
 * @param {Function}                   props.setGridData               Passed handler for field changes if the field type supports it.
 * @param {Function}                   props.setIsLoading              Handler to update the loading state.
 * @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 {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 {Function}                   props.updateModuleState         Handler to update the module state.
 * @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';
 * import {
 *   ActiveFilters,
 *   BulkActions,
 *   ColumnSort,
 *   DateFilters,
 *   GridPagination,
 *   Search,
 *   SimpleFilters,
 * } from '@gravityforms/components/react/admin/modules/DataGrid/modules';
 * import useDataGridState from '@gravityforms/components/react/admin/modules/DataGrid/use-data-grid-state';
 * import StatusIndicator from '@gravityforms/components/react/admin/elements/StatusIndicator';
 * import Text from '@gravityforms/components/react/admin/elements/Text';
 *
 * const activeFiltersAttributes = {
 *   pill: {
 *     icon: 'x-circle',
 *     iconPrefix: 'gravity-component-icon',
 *     onClick: ( event ) => {},
 *     // ...restPillAttributes (see Pill component)
 *   },
 *   reset: {
 *     label: 'Reset Filters',
 *     onClick: ( event ) => {},
 *     // ...restButtonAttributes (see Button component)
 *   },
 *   i18n: {
 *     activeFiltersLabel: 'Filters:',
 *   },
 *   onFilterChange: ( state ) => {},
 * };
 *
 * const bulkActionsAttributes = {
 *   button: {
 *     label: 'Apply',
 *     onClick: ( state ) => {},
 *     // ...restButtonAttributes (see Button component)
 *   },
 *   select: {
 *     blankValue: -1,
 *     onChange: ( value ) => {},
 *     wrapperClasses: [],
 *     labelAttributes: {
 *       label: 'Select an action',
 *       isVisible: false,
 *     },
 *     options: [
 *       {
 *         label: 'Bulk Actions',
 *         value: '-1',
 *       },
 *       {
 *         label: 'Delete',
 *         value: 'delete',
 *       },
 *     },
 *     // ...restSelectAttributes (see Select component)
 *   },
 *   i18n: {
 *     bulkSelectI18n: 'Select all rows',
 *     selectNoticeSelectedNumberEntriesI18n: 'All %1$s rows on this page are selected',
 *     selectNoticeSelectedAllNumberEntriesI18n: 'All %1$s rows in this table are selected',
 *     selectNoticeSelectAllNumberEntriesI18n: 'Select All %1$s Rows',
 *     selectNoticeClearSelectionI18n: 'Clear Selection',
 *     selectRowI18n: 'Select row',
 *   },
 *   notice: {
 *     rowCount: 315, // Total number of rows in the data grid.
 *   },
 * };
 *
 * const columnSortAttributes = {
 *   sortButton: {
 *     customClasses: [],
 *     iconAsc: 'chevron-up',
 *     iconDesc: 'chevron-down',
 *     iconPrefix: 'gravity-component-icon',
 *     initialOrder: 'DESC',
 *     onClick: ( state ) => {},
 *   },
 * };
 *
 * const dateFiltersAttributes = {
 *   calendarAttributes: {
 *     // ...restCalendarAttributes (see Calendar component)
 *   },
 *   customClasses: [],
 *   dateFormatOptions: {
 *     day: 'numeric',
 *     month: 'short',
 *     year: 'numeric',
 *   },
 *   triggerAttributes: {
 *     ariaText: 'Calendar',
 *     icon: 'calendar',
 *     iconPrefix: 'gravity-component-icon',
 *     // ...restButtonAttributes (see Button component)
 *   },
 *   onChange: ( state ) => {},
 *   resetAttributes: {
 *     label: 'Reset',
 *     // ...restResetAttributes (see Button component)
 *   },
 *   todayAttributes: {
 *     label: 'Today',
 *     // ...restTodayAttributes (see Button component)
 *   },
 *   i18n: {
 *     pillLabel: 'Date: %s',
 *   },
 *   // ...restDateFiltersAttributes (see Calendar component)
 * };
 *
 * const paginationAttributes = {
 *   nextLabel: 'Next',
 *   nextAriaLabel: 'Next page',
 *   onClick: ( state ) => {},
 *   onPageChange: ( event ) => {},
 *   pageCount: 15,
 *   previousLabel: 'Prev',
 *   previousAriaLabel: 'Previous page',
 *   // ...restPaginationAttributes (see Pagination component)
 * };
 *
 * const searchAttributes = {
 *   input: {
 *     onChange: ( value ) => {},
 *     onClear: () => {},
 *     onKeyDown: ( event ) => {},
 *     wrapperClasses: [],
 *     // ...restInputAttributes (see Input component)
 *   },
 *   button: {
 *     customClasses: [],
 *     // ...restButtonAttributes (see Button component)
 *   },
 *   onSearch: ( state ) => {},
 *   i18n: {
 *     noResultsTitle: 'No results found',
 *     noResultsMessage: 'Try adjusting your search criteria.',
 *     pillLabel: 'Search: %s',
 *   },
 *   noResults: {
 *     customClasses: [],
 *     Image: NoResultsImage,
 *     // ...restAttributes (for NoResults component)
 *   },
 * };
 *
 * const simpleFiltersAttributes = {
 *   droplist: {
 *     customClasses: [],
 *     listItems: [
 *       {
 *         key: 'status',
 *         triggerAttributes: {
 *           id: 'status',
 *           label: 'Status',
 *         },
 *         listItems: [
 *           {
 *             key: 'status-sent',
 *             props: {
 *               customAttributes: {
 *                 'data-key': 'status',
 *                 'data-value': 'sent',
 *                 'data-pill-label': 'Status: Sent',
 *                 id: 'status-sent',
 *               },
 *               element: 'button',
 *               label: 'Sent',
 *             },
 *           },
 *           {
 *             key: 'status-failed',
 *             props: {
 *               customAttributes: {
 *                 'data-key': 'status',
 *                 'data-value': 'failed',
 *                 'data-pill-label': 'Status: Failed',
 *                 id: 'status-failed',
 *               },
 *               element: 'button',
 *               label: 'Failed',
 *             },
 *           },
 *         ],
 *       },
 *       {
 *         key: 'service',
 *         triggerAttributes: {
 *           id: 'service',
 *           label: 'Service',
 *         },
 *         listItems: [
 *           {
 *             key: 'service-mailgun',
 *             props: {
 *               customAttributes: {
 *                 'data-key': 'service',
 *                 'data-value': 'mailgun',
 *                 'data-pill-label': 'Service: Mailgun',
 *                 id: 'service-mailgun',
 *               },
 *               element: 'button',
 *               label: 'Mailgun',
 *             },
 *           },
 *           {
 *             key: 'service-sendgrid',
 *             props: {
 *               customAttributes: {
 *                 'data-key': 'service',
 *                 'data-value': 'sendgrid',
 *                 'data-pill-label': 'Service: SendGrid',
 *                 id: 'service-sendgrid',
 *               },
 *               element: 'button',
 *               label: 'SendGrid',
 *             },
 *           },
 *         ],
 *       },
 *     ],
 *     listItemAttributes: {
 *       iconAfter: 'check-alt',
 *       groupIcon: 'chevron-right',
 *       iconPrefix: 'gravity-component-icon',
 *     },
 *     reset: {
 *       hasReset: true,
 *       label: 'Reset Filters',
 *     },
 *     triggerAttributes: {
 *       ariaId: 'aria-text',
 *       ariaText: 'Filter by',
 *       icon: 'filter',
 *       iconPrefix: 'gravity-component-icon',
 *       size: 'size-height-m',
 *       // ...restTriggerAttributes (see Droplist component)
 *     },
 *     // ...restDroplistAttributes (see Droplist component)
 *   },
 * };
 *
 * const modules = [ ActiveFilters, BulkActions, ColumnSort, DateFilters, GridPagination, Search, SimpleFilters ];
 *
 * const moduleAttributes = {
 *   activeFilters: activeFiltersAttributes,
 *   bulkActions: bulkActionsAttributes,
 *   columnSort: columnSortAttributes,
 *   dateFilters: dateFiltersAttributes,
 *   pagination: paginationAttributes,
 *   search: searchAttributes,
 *   simpleFilters: simpleFiltersAttributes,
 * };
 *
 * const columns = [
 *   {
 *     component: Text,
 *     hideWhenLoading: true,
 *     key: 'status',
 *     props: {
 *       content: 'Status',
 *       size: 'text-sm',
 *       weight: 'medium',
 *     },
 *     sortable: true,
 *     variableLoader: true,
 *   },
 *   {
 *     component: Text,
 *     hideAt: 960,
 *     hideWhenLoading: true,
 *     key: 'service',
 *     props: {
 *       content: 'Service',
 *       size: 'text-sm',
 *       weight: 'medium',
 *     },
 *     sortable: true,
 *   },
 * ];
 *
 * const columnStyleProps = {
 *   status: { flex: '0 0 120px' },
 *   service: { flexBasis: '100px' },
 * };
 *
 * const initialData = [
 *   {
 *     status: {
 *       component: StatusIndicator,
 *       props: {
 *         label: 'Sent',
 *       },
 *     },
 *     service: {
 *       component: Text,
 *       props: {
 *         content: 'Mailgun',
 *         size: 'text-sm',
 *       },
 *     },
 *   },
 *   {
 *     status: {
 *       component: StatusIndicator,
 *       props: {
 *         label: 'Failed',
 *       },
 *     },
 *     service: {
 *       component: Text,
 *       props: {
 *         content: 'SendGrid',
 *         size: 'text-sm',
 *       },
 *     },
 *   }
 * ];
 *
 * const useAjax = true;
 *
 * const {
 *   moduleState,
 *   updateModuleState,
 *   isLoading,
 *   setIsLoading,
 *   gridLocked,
 *   organizedData,
 * } = useDataGridState( {
 *   data: initialData,
 *   dataPerPage: 10,
 *   initialState: {
 *     isLoading: false,
 *     moduleState: {
 *       currentPage: 0,
 *       searchTerm: '',
 *       searchActive: false,
 *     },
 *   },
 *   modules,
 *   moduleAttributes,
 *   useAjax,
 * } );
 *
 * return <DataGrid
 *   columns={ columns }
 *   columnStyleProps={ columnStyleProps }
 *   data={ organizedData }
 *   dataPerPage={ 10 }
 *   EmptyImage={ EmptyImageSvg }
 *   emptyMessageAttributes={ { style: { maxWidth: '360px' } } },
 *   gridLocked={ gridLocked }
 *   i18n={ {
 *     emptyMessageI18n: 'No data available',
 *     emptyTitleI18n: 'No Data',
 *     gridHeadingI18n: 'Data Grid Title',
 *   } }
 *   isLoading={ isLoading }
 *   modules={ modules }
 *   moduleAttributes={ moduleAttributes }
 *   moduleState={ moduleState }
 *   setIsLoading={ setIsLoading }
 *   updateModuleState={ updateModuleState }
 *   useAjax={ useAjax }
 * />;
 *
 */
const DataGrid = forwardRef( ( props, ref ) => {
	const defaultProps = {
		afterGridHeading: null,
		columns: [],
		columnRowAttributes: {},
		columnRowClasses: [],
		columnStyleProps: {},
		customAttributes: {},
		customClasses: [],
		data: [],
		dataPerPage: 20,
		dataRowAttributes: {},
		dataRowClasses: [],
		dataRowMinHeight: '71px',
		EmptyImage: null,
		emptyMessageAttributes: {},
		emptyMessageClasses: [],
		equalGrid: false,
		gridControlWrapperClasses: [],
		gridLocked: false,
		handleGridClicks: () => {},
		highlightHover: true,
		highlightSelected: true,
		i18n: {},
		id: '',
		isLoading: false,
		maintainHeight: false,
		modules: [],
		moduleAttributes: {},
		moduleState: {},
		scrollX: false,
		setGridData: () => {},
		setIsLoading: () => {},
		showColumns: true,
		showColumnsInFooter: true,
		spacing: '',
		titleAttributes: {},
		titleClasses: [],
		updateModuleState: () => {},
		useAjax: false,
	};
	const combinedProps = { ...defaultProps, ...props };
	const { id: idProp = '' } = combinedProps;
	const idProviderProps = { id: idProp };

	return (
		<IdProvider { ...idProviderProps }>
			<DataGridComponent { ...combinedProps } ref={ ref } />
		</IdProvider>
	);
} );

DataGrid.propTypes = {
	afterGridHeading: PropTypes.node,
	columns: PropTypes.arrayOf(
		PropTypes.shape( {
			key: PropTypes.string.isRequired,
			component: PropTypes.object.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,
	] ),
	gridLocked: PropTypes.bool,
	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,
	moduleState: PropTypes.object,
	scrollX: PropTypes.bool,
	setGridData: PropTypes.func,
	setIsLoading: PropTypes.func,
	showColumns: PropTypes.bool,
	showColumnsInFooter: 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,
	] ),
	updateModuleState: PropTypes.func,
	useAjax: PropTypes.bool,
};

DataGrid.displayName = 'DataGrid';

export default DataGrid;