/** * External dependencies */ import type { ReactNode, ComponentProps, ReactElement } from 'react'; import clsx from 'clsx'; /** * WordPress dependencies */ import { useContext, useEffect, useMemo, useRef, useState, } from '@wordpress/element'; import { useResizeObserver } from '@wordpress/compose'; import { Stack } from '@wordpress/ui'; /** * Internal dependencies */ import DataViewsContext from '../components/dataviews-context'; import { VIEW_LAYOUTS } from '../components/dataviews-layouts'; import { Filters, FiltersToggled, useFilters, FiltersToggle, } from '../components/dataviews-filters'; import DataViewsLayout from '../components/dataviews-layout'; import DataViewsFooter from '../components/dataviews-footer'; import DataViewsSearch from '../components/dataviews-search'; import { BulkActionsFooter } from '../components/dataviews-bulk-actions'; import { DataViewsPagination } from '../components/dataviews-pagination'; import DataViewsViewConfig, { DataviewsViewConfigDropdown, ViewTypeMenu, } from '../components/dataviews-view-config'; import normalizeFields from '../field-types'; import useData from '../hooks/use-data'; import { useInfiniteScroll } from '../hooks/use-infinite-scroll'; import type { Action, Field, View, SupportedLayouts } from '../types'; import type { SelectionOrUpdater } from '../types/private'; type ItemWithId = { id: string }; type DataViewsProps< Item > = { view: View; onChangeView: ( view: View ) => void; fields: Field< Item >[]; search?: boolean; searchLabel?: string; actions?: Action< Item >[]; data: Item[]; isLoading?: boolean; paginationInfo: { totalItems: number; totalPages: number; }; defaultLayouts: SupportedLayouts; selection?: string[]; onChangeSelection?: ( items: string[] ) => void; onClickItem?: ( item: Item ) => void; renderItemLink?: ( props: { item: Item; } & ComponentProps< 'a' > ) => ReactElement; isItemClickable?: ( item: Item ) => boolean; header?: ReactNode; getItemLevel?: ( item: Item ) => number; children?: ReactNode; config?: { perPageSizes: number[]; }; empty?: ReactNode; onReset?: ( () => void ) | false; } & ( Item extends ItemWithId ? { getItemId?: ( item: Item ) => string } : { getItemId: ( item: Item ) => string } ); const defaultGetItemId = ( item: ItemWithId ) => item.id; const defaultIsItemClickable = () => true; const EMPTY_ARRAY: any[] = []; const dataViewsLayouts = VIEW_LAYOUTS.filter( ( viewLayout ) => ! viewLayout.isPicker ); type DefaultUIProps = Pick< DataViewsProps< any >, 'header' | 'search' | 'searchLabel' >; function DefaultUI( { header, search = true, searchLabel = undefined, }: DefaultUIProps ) { const { view } = useContext( DataViewsContext ); const isInfiniteScroll = view.infiniteScrollEnabled; return ( <> { search && } { header } ); } function DataViews< Item >( { view, onChangeView, fields, search = true, searchLabel = undefined, actions = EMPTY_ARRAY, data, getItemId = defaultGetItemId, getItemLevel, isLoading = false, paginationInfo, defaultLayouts: defaultLayoutsProperty, selection: selectionProperty, onChangeSelection, onClickItem, renderItemLink, isItemClickable = defaultIsItemClickable, header, children, config = { perPageSizes: [ 10, 20, 50, 100 ] }, empty, onReset, }: DataViewsProps< Item > ) { const [ selectionState, setSelectionState ] = useState< string[] >( [] ); const isUncontrolled = selectionProperty === undefined || onChangeSelection === undefined; const selection = isUncontrolled ? selectionState : selectionProperty; // useData handles both infinite scroll and standard pagination paths, // preserving previous data while loading and tracking initial load state. const { data: displayData, paginationInfo: displayPaginationInfo, hasInitiallyLoaded, setVisibleEntries, } = useData( { view, data: data as any, getItemId: getItemId as any, isLoading, selection, paginationInfo, } ) as { data: ( Item & { position?: number } )[]; paginationInfo: { totalItems: number; totalPages: number }; hasInitiallyLoaded: boolean; setVisibleEntries?: React.Dispatch< React.SetStateAction< number[] > >; }; const containerRef = useRef< HTMLDivElement >( null ); const [ containerWidth, setContainerWidth ] = useState( 0 ); const resizeObserverRef = useResizeObserver( ( resizeObserverEntries: any ) => { setContainerWidth( resizeObserverEntries[ 0 ].borderBoxSize[ 0 ].inlineSize ); }, { box: 'border-box' } ); const [ openedFilter, setOpenedFilter ] = useState< string | null >( null ); function setSelectionWithChange( value: SelectionOrUpdater ) { const newValue = typeof value === 'function' ? value( selection ) : value; if ( isUncontrolled ) { setSelectionState( newValue ); } if ( onChangeSelection ) { onChangeSelection( newValue ); } } const _fields = useMemo( () => normalizeFields( fields ), [ fields ] ); // When infinite scroll is enabled, don't filter selection by current data // because items may be scrolled out of view but still selected. const _selection = useMemo( () => { if ( view.infiniteScrollEnabled ) { return selection; } return selection.filter( ( id ) => data.some( ( item ) => getItemId( item ) === id ) ); }, [ selection, data, getItemId, view.infiniteScrollEnabled ] ); const filters = useFilters( _fields, view ); const hasPrimaryOrLockedFilters = useMemo( () => ( filters || [] ).some( ( filter ) => filter.isPrimary || filter.isLocked ), [ filters ] ); const [ isShowingFilter, setIsShowingFilter ] = useState< boolean >( hasPrimaryOrLockedFilters ); const { intersectionObserver } = useInfiniteScroll( { view, onChangeView, isLoading, paginationInfo, containerRef, setVisibleEntries, } ); useEffect( () => { if ( hasPrimaryOrLockedFilters && ! isShowingFilter ) { setIsShowingFilter( true ); } }, [ hasPrimaryOrLockedFilters, isShowingFilter ] ); // Filter out DataViewsPicker layouts. const defaultLayouts = useMemo( () => Object.fromEntries( Object.entries( defaultLayoutsProperty ).filter( ( [ layoutType ] ) => { return dataViewsLayouts.some( ( viewLayout ) => viewLayout.type === layoutType ); } ) ), [ defaultLayoutsProperty ] ); if ( ! defaultLayouts[ view.type ] ) { return null; } return (
{ children ?? ( ) }
); } // Populate the DataViews sub components const DataViewsSubComponents = DataViews as typeof DataViews & { BulkActionToolbar: typeof BulkActionsFooter; Filters: typeof Filters; FiltersToggle: typeof FiltersToggle; FiltersToggled: typeof FiltersToggled; Layout: typeof DataViewsLayout; LayoutSwitcher: typeof ViewTypeMenu; Pagination: typeof DataViewsPagination; Search: typeof DataViewsSearch; ViewConfig: typeof DataviewsViewConfigDropdown; Footer: typeof DataViewsFooter; }; DataViewsSubComponents.BulkActionToolbar = BulkActionsFooter; DataViewsSubComponents.Filters = Filters; DataViewsSubComponents.FiltersToggled = FiltersToggled; DataViewsSubComponents.FiltersToggle = FiltersToggle; DataViewsSubComponents.Layout = DataViewsLayout; DataViewsSubComponents.LayoutSwitcher = ViewTypeMenu; DataViewsSubComponents.Pagination = DataViewsPagination; DataViewsSubComponents.Search = DataViewsSearch; DataViewsSubComponents.ViewConfig = DataviewsViewConfigDropdown; DataViewsSubComponents.Footer = DataViewsFooter; export default DataViewsSubComponents;