/**
* 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;