/**
* External dependencies
*/
import type { ReactNode } 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 { DataViewsPickerFooter } from '../components/dataviews-picker-footer';
import DataViewsSearch from '../components/dataviews-search';
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 { ActionButton, Field, View, SupportedLayouts } from '../types';
import type { SelectionOrUpdater } from '../types/private';
type ItemWithId = { id: string };
const isItemClickable = () => false;
const dataViewsPickerLayouts = VIEW_LAYOUTS.filter(
( viewLayout ) => viewLayout.isPicker
);
type DataViewsPickerProps< Item > = {
view: View;
onChangeView: ( view: View ) => void;
fields: Field< Item >[];
actions?: ActionButton< Item >[];
search?: boolean;
searchLabel?: string;
data: Item[];
isLoading?: boolean;
paginationInfo: {
totalItems: number;
totalPages: number;
};
defaultLayouts: SupportedLayouts;
selection: string[];
onChangeSelection: ( items: string[] ) => void;
children?: ReactNode;
config?: {
perPageSizes: number[];
};
itemListLabel?: string;
empty?: ReactNode;
} & ( Item extends ItemWithId
? { getItemId?: ( item: Item ) => string }
: { getItemId: ( item: Item ) => string } );
const defaultGetItemId = ( item: ItemWithId ) => item.id;
const EMPTY_ARRAY: any[] = [];
type DefaultUIProps = Pick<
DataViewsPickerProps< any >,
'search' | 'searchLabel'
>;
function DefaultUI( {
search = true,
searchLabel = undefined,
}: DefaultUIProps ) {
const { view } = useContext( DataViewsContext );
const isInfiniteScroll = view.infiniteScrollEnabled;
return (
<>
{ search && }
>
);
}
function DataViewsPicker< Item >( {
view,
onChangeView,
fields,
search = true,
searchLabel = undefined,
actions = EMPTY_ARRAY,
data,
getItemId = defaultGetItemId,
isLoading = false,
paginationInfo,
defaultLayouts: defaultLayoutsProperty,
selection,
onChangeSelection,
children,
config = { perPageSizes: [ 10, 20, 50, 100 ] },
itemListLabel,
empty,
}: DataViewsPickerProps< Item > ) {
// useData ensures data loading is correct whether infinite scroll is enabled or pagination is used.
const { data: displayData, setVisibleEntries } = useData( {
view,
data: data as any,
getItemId: getItemId as any,
selection,
paginationInfo,
} ) as {
data: ( Item & { position?: number } )[];
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 ( onChangeSelection ) {
onChangeSelection( newValue );
}
}
const _fields = useMemo( () => normalizeFields( fields ), [ fields ] );
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 dataViewsPickerLayouts.some(
( viewLayout ) => viewLayout.type === layoutType
);
}
)
),
[ defaultLayoutsProperty ]
);
if ( ! defaultLayouts[ view.type ] ) {
return null;
}
return (
{ children ?? (
) }
);
}
// Populate the DataViews sub components
const DataViewsPickerSubComponents =
DataViewsPicker as typeof DataViewsPicker & {
BulkActionToolbar: typeof DataViewsPickerFooter;
Filters: typeof Filters;
FiltersToggled: typeof FiltersToggled;
FiltersToggle: typeof FiltersToggle;
Layout: typeof DataViewsLayout;
LayoutSwitcher: typeof ViewTypeMenu;
Pagination: typeof DataViewsPagination;
Search: typeof DataViewsSearch;
ViewConfig: typeof DataviewsViewConfigDropdown;
};
DataViewsPickerSubComponents.BulkActionToolbar = DataViewsPickerFooter;
DataViewsPickerSubComponents.Filters = Filters;
DataViewsPickerSubComponents.FiltersToggled = FiltersToggled;
DataViewsPickerSubComponents.FiltersToggle = FiltersToggle;
DataViewsPickerSubComponents.Layout = DataViewsLayout;
DataViewsPickerSubComponents.LayoutSwitcher = ViewTypeMenu;
DataViewsPickerSubComponents.Pagination = DataViewsPagination;
DataViewsPickerSubComponents.Search = DataViewsSearch;
DataViewsPickerSubComponents.ViewConfig = DataviewsViewConfigDropdown;
export default DataViewsPickerSubComponents;