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;