diff --git a/packages/dataviews/src/constants.ts b/packages/dataviews/src/constants.ts index 9c045aef753262..b9ed8caff9e940 100644 --- a/packages/dataviews/src/constants.ts +++ b/packages/dataviews/src/constants.ts @@ -46,10 +46,12 @@ export const OPERATORS = { }, }; -// Sorting -export const SORTING_DIRECTIONS = { - asc: { label: __( 'Sort ascending' ) }, - desc: { label: __( 'Sort descending' ) }, +export const SORTING_DIRECTIONS = [ 'asc', 'desc' ] as const; +export const sortArrows = { asc: '↑', desc: '↓' }; +export const sortValues = { asc: 'ascending', desc: 'descending' } as const; +export const sortLabels = { + asc: __( 'Sort ascending' ), + desc: __( 'Sort descending' ), }; // View layouts. diff --git a/packages/dataviews/src/item-actions.tsx b/packages/dataviews/src/item-actions.tsx index 3b45561defdd68..a1cbafa56978ff 100644 --- a/packages/dataviews/src/item-actions.tsx +++ b/packages/dataviews/src/item-actions.tsx @@ -62,7 +62,7 @@ interface ActionsDropdownMenuGroupProps< Item extends AnyItem > { interface ItemActionsProps< Item extends AnyItem > { item: Item; actions: Action< Item >[]; - isCompact: boolean; + isCompact?: boolean; } interface CompactItemActionsProps< Item extends AnyItem > { diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index b02c4f7b9d0202..3735eaf0dd2033 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -28,7 +28,15 @@ interface FilterByConfig { isPrimary?: boolean; } -type Operator = 'is' | 'isNot' | 'isAny' | 'isNone' | 'isAll' | 'isNotAll'; +type DeprecatedOperator = 'in' | 'notIn'; +type Operator = + | 'is' + | 'isNot' + | 'isAny' + | 'isNone' + | 'isAll' + | 'isNotAll' + | DeprecatedOperator; export type AnyItem = Record< string, any >; @@ -175,6 +183,22 @@ interface ViewBase { hiddenFields: string[]; } +export interface ViewTable extends ViewBase { + type: 'table'; + + layout: { + /** + * The field to use as the primary field. + */ + primaryField: string; + + /** + * The field to use as the media field. + */ + mediaField: string; + }; +} + export interface ViewList extends ViewBase { type: 'list'; @@ -217,7 +241,7 @@ export interface ViewGrid extends ViewBase { }; } -export type View = ViewList | ViewGrid | ViewBase; +export type View = ViewList | ViewGrid | ViewTable; interface ActionBase< Item extends AnyItem > { /** @@ -312,3 +336,16 @@ export interface ActionButton< Item extends AnyItem > export type Action< Item extends AnyItem > = | ActionModal< Item > | ActionButton< Item >; + +export interface ViewProps< Item extends AnyItem, ViewType extends ViewBase > { + actions: Action< Item >[]; + data: Item[]; + fields: NormalizedField< Item >[]; + getItemId: ( item: Item ) => string; + isLoading?: boolean; + onChangeView( view: ViewType ): void; + onSelectionChange: ( items: Item[] ) => void; + selection: string[]; + setOpenedFilter: ( fieldId: string ) => void; + view: ViewType; +} diff --git a/packages/dataviews/src/utils.js b/packages/dataviews/src/utils.ts similarity index 88% rename from packages/dataviews/src/utils.js rename to packages/dataviews/src/utils.ts index cf861f36e06ace..8321b96b9d07b1 100644 --- a/packages/dataviews/src/utils.js +++ b/packages/dataviews/src/utils.ts @@ -8,8 +8,11 @@ import { OPERATOR_IS_ANY, OPERATOR_IS_NONE, } from './constants'; +import type { AnyItem, NormalizedField } from './types'; -export const sanitizeOperators = ( field ) => { +export function sanitizeOperators< Item extends AnyItem >( + field: NormalizedField< Item > +) { let operators = field.filterBy?.operators; // Assign default values. @@ -45,4 +48,4 @@ export const sanitizeOperators = ( field ) => { } return operators; -}; +} diff --git a/packages/dataviews/src/view-actions.js b/packages/dataviews/src/view-actions.js index 6385936b940867..ee65b60c37b075 100644 --- a/packages/dataviews/src/view-actions.js +++ b/packages/dataviews/src/view-actions.js @@ -13,7 +13,7 @@ import { settings } from '@wordpress/icons'; * Internal dependencies */ import { unlock } from './lock-unlock'; -import { SORTING_DIRECTIONS } from './constants'; +import { SORTING_DIRECTIONS, sortLabels } from './constants'; import { VIEW_LAYOUTS } from './layouts'; const { @@ -206,43 +206,41 @@ function SortMenu( { fields, view, onChangeView } ) { minWidth: '220px', } } > - { Object.entries( SORTING_DIRECTIONS ).map( - ( [ direction, info ] ) => { - const isChecked = - currentSortedField !== undefined && - sortedDirection === direction && - field.id === currentSortedField.id; + { SORTING_DIRECTIONS.map( ( direction ) => { + const isChecked = + currentSortedField !== undefined && + sortedDirection === direction && + field.id === currentSortedField.id; - const value = `${ field.id }-${ direction }`; + const value = `${ field.id }-${ direction }`; - return ( - { - onChangeView( { - ...view, - sort: { - field: field.id, - direction, - }, - } ); - } } - > - - { info.label } - - - ); - } - ) } + return ( + { + onChangeView( { + ...view, + sort: { + field: field.id, + direction, + }, + } ); + } } + > + + { sortLabels[ direction ] } + + + ); + } ) } ); } ) } diff --git a/packages/dataviews/src/view-grid.tsx b/packages/dataviews/src/view-grid.tsx index 331c0e1200bcf3..e3bb2920e82ba3 100644 --- a/packages/dataviews/src/view-grid.tsx +++ b/packages/dataviews/src/view-grid.tsx @@ -27,8 +27,12 @@ import type { AnyItem, NormalizedField, ViewGrid as ViewGridType, + ViewProps, } from './types'; +interface ViewGridProps< Item extends AnyItem > + extends ViewProps< Item, ViewGridType > {} + interface GridItemProps< Item extends AnyItem > { selection: string[]; data: Item[]; @@ -43,17 +47,6 @@ interface GridItemProps< Item extends AnyItem > { columnFields: string[]; } -interface ViewGridProps< Item extends AnyItem > { - actions: Action< Item >[]; - data: Item[]; - fields: NormalizedField< Item >[]; - getItemId: ( item: Item ) => string; - isLoading: boolean; - onSelectionChange: ( items: Item[] ) => void; - selection: string[]; - view: ViewGridType; -} - function GridItem< Item extends AnyItem >( { selection, data, diff --git a/packages/dataviews/src/view-list.tsx b/packages/dataviews/src/view-list.tsx index 12d9a7f86008d7..8ba12c03213fb4 100644 --- a/packages/dataviews/src/view-list.tsx +++ b/packages/dataviews/src/view-list.tsx @@ -37,21 +37,13 @@ import type { AnyItem, NormalizedField, ViewList as ViewListType, + ViewProps, } from './types'; import { ActionsDropdownMenuGroup, ActionModal } from './item-actions'; -interface ListViewProps< Item extends AnyItem > { - actions: Action< Item >[]; - data: Item[]; - fields: NormalizedField< Item >[]; - getItemId: ( item: Item ) => string; - id: string; - isLoading: boolean; - onSelectionChange: ( selection: Item[] ) => void; - selection: string[]; - view: ViewListType; -} +interface ViewListProps< Item extends AnyItem > + extends ViewProps< Item, ViewListType > {} interface ListViewItemProps< Item extends AnyItem > { actions: Action< Item >[]; @@ -311,7 +303,7 @@ function ListItem< Item extends AnyItem >( { } export default function ViewList< Item extends AnyItem >( - props: ListViewProps< Item > + props: ViewListProps< Item > ) { const { actions, diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.tsx similarity index 80% rename from packages/dataviews/src/view-table.js rename to packages/dataviews/src/view-table.tsx index f06b364a5c8e8d..566d098216d43f 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.tsx @@ -2,6 +2,7 @@ * External dependencies */ import clsx from 'clsx'; +import type { ReactNode, Ref, PropsWithoutRef, RefAttributes } from 'react'; /** * WordPress dependencies @@ -33,11 +34,24 @@ import SingleSelectionCheckbox from './single-selection-checkbox'; import { unlock } from './lock-unlock'; import ItemActions from './item-actions'; import { sanitizeOperators } from './utils'; -import { SORTING_DIRECTIONS } from './constants'; +import { + SORTING_DIRECTIONS, + sortArrows, + sortLabels, + sortValues, +} from './constants'; import { useSomeItemHasAPossibleBulkAction, useHasAPossibleBulkAction, } from './bulk-actions'; +import type { + Action, + AnyItem, + NormalizedField, + SortDirection, + ViewProps, + ViewTable as ViewTableType, +} from './types'; const { DropdownMenuV2: DropdownMenu, @@ -48,7 +62,38 @@ const { DropdownMenuSeparatorV2: DropdownMenuSeparator, } = unlock( componentsPrivateApis ); -function WithDropDownMenuSeparators( { children } ) { +interface HeaderMenuProps< Item extends AnyItem > { + field: NormalizedField< Item >; + view: ViewTableType; + onChangeView: ( view: ViewTableType ) => void; + onHide: ( field: NormalizedField< Item > ) => void; + setOpenedFilter: ( fieldId: string ) => void; +} + +interface BulkSelectionCheckboxProps< Item extends AnyItem > { + selection: string[]; + onSelectionChange: ( items: Item[] ) => void; + data: Item[]; + actions: Action< Item >[]; +} + +interface TableRowProps< Item extends AnyItem > { + hasBulkActions: boolean; + item: Item; + actions: Action< Item >[]; + id: string; + visibleFields: NormalizedField< Item >[]; + primaryField?: NormalizedField< Item >; + selection: string[]; + getItemId: ( item: Item ) => string; + onSelectionChange: ( items: Item[] ) => void; + data: Item[]; +} + +interface ViewTableProps< Item extends AnyItem > + extends ViewProps< Item, ViewTableType > {} + +function WithDropDownMenuSeparators( { children }: { children: ReactNode } ) { return Children.toArray( children ) .filter( Boolean ) .map( ( child, i ) => ( @@ -59,11 +104,15 @@ function WithDropDownMenuSeparators( { children } ) { ) ); } -const sortArrows = { asc: '↑', desc: '↓' }; - -const HeaderMenu = forwardRef( function HeaderMenu( - { field, view, onChangeView, onHide, setOpenedFilter }, - ref +const _HeaderMenu = forwardRef( function HeaderMenu< Item extends AnyItem >( + { + field, + view, + onChangeView, + onHide, + setOpenedFilter, + }: HeaderMenuProps< Item >, + ref: Ref< HTMLButtonElement > ) { const isHidable = field.enableHiding !== false; const isSortable = field.enableSorting !== false; @@ -92,9 +141,9 @@ const HeaderMenu = forwardRef( function HeaderMenu( variant="tertiary" > { field.header } - { isSorted && ( + { view.sort && isSorted && ( ) } @@ -104,9 +153,10 @@ const HeaderMenu = forwardRef( function HeaderMenu( { isSortable && ( - { Object.entries( SORTING_DIRECTIONS ).map( - ( [ direction, info ] ) => { + { SORTING_DIRECTIONS.map( + ( direction: SortDirection ) => { const isChecked = + view.sort && isSorted && view.sort.direction === direction; @@ -134,7 +184,7 @@ const HeaderMenu = forwardRef( function HeaderMenu( } } > - { info.label } + { sortLabels[ direction ] } ); @@ -191,16 +241,24 @@ const HeaderMenu = forwardRef( function HeaderMenu( ); } ); -function BulkSelectionCheckbox( { +// @ts-expect-error Lift the `Item` type argument through the forwardRef. +const HeaderMenu: < Item extends AnyItem >( + props: PropsWithoutRef< HeaderMenuProps< Item > > & + RefAttributes< HTMLButtonElement > +) => ReturnType< typeof _HeaderMenu > = _HeaderMenu; + +function BulkSelectionCheckbox< Item extends AnyItem >( { selection, onSelectionChange, data, actions, -} ) { +}: BulkSelectionCheckboxProps< Item > ) { const selectableItems = useMemo( () => { return data.filter( ( item ) => { return actions.some( - ( action ) => action.supportsBulk && action.isEligible( item ) + ( action ) => + action.supportsBulk && + ( ! action.isEligible || action.isEligible( item ) ) ); } ); }, [ data, actions ] ); @@ -210,7 +268,7 @@ function BulkSelectionCheckbox( { className="dataviews-view-table-selection-checkbox" __nextHasNoMarginBottom checked={ areAllSelected } - indeterminate={ ! areAllSelected && selection.length } + indeterminate={ ! areAllSelected && !! selection.length } onChange={ () => { if ( areAllSelected ) { onSelectionChange( [] ); @@ -225,7 +283,7 @@ function BulkSelectionCheckbox( { ); } -function TableRow( { +function TableRow< Item extends AnyItem >( { hasBulkActions, item, actions, @@ -236,7 +294,7 @@ function TableRow( { getItemId, onSelectionChange, data, -} ) { +}: TableRowProps< Item > ) { const hasPossibleBulkAction = useHasAPossibleBulkAction( actions, item ); const isSelected = selection.includes( id ); @@ -270,7 +328,7 @@ function TableRow( { onClick={ () => { if ( ! isTouchDevice.current && - document.getSelection().type !== 'Range' + document.getSelection()?.type !== 'Range' ) { if ( ! isSelected ) { onSelectionChange( @@ -305,7 +363,6 @@ function TableRow( { >
( { actions, data, fields, @@ -372,10 +429,13 @@ function ViewTable( { selection, setOpenedFilter, view, -} ) { - const headerMenuRefs = useRef( new Map() ); - const headerMenuToFocusRef = useRef(); - const [ nextHeaderMenuToFocus, setNextHeaderMenuToFocus ] = useState(); +}: ViewTableProps< Item > ) { + const headerMenuRefs = useRef< + Map< string, { node: HTMLButtonElement; fallback: string } > + >( new Map() ); + const headerMenuToFocusRef = useRef< HTMLButtonElement >(); + const [ nextHeaderMenuToFocus, setNextHeaderMenuToFocus ] = + useState< HTMLButtonElement >(); const hasBulkActions = useSomeItemHasAPossibleBulkAction( actions, data ); useEffect( () => { @@ -393,13 +453,15 @@ function ViewTable( { // Clearing out the focus directive is necessary to make sure // future renders don't cause unexpected focus jumps. headerMenuToFocusRef.current = nextHeaderMenuToFocus; - setNextHeaderMenuToFocus(); + setNextHeaderMenuToFocus( undefined ); return; } - const onHide = ( field ) => { + const onHide = ( field: NormalizedField< Item > ) => { const hidden = headerMenuRefs.current.get( field.id ); - const fallback = headerMenuRefs.current.get( hidden.fallback ); + const fallback = hidden + ? headerMenuRefs.current.get( hidden.fallback ) + : undefined; setNextHeaderMenuToFocus( fallback?.node ); }; const visibleFields = fields.filter( @@ -408,7 +470,6 @@ function ViewTable( { ! [ view.layout.mediaField ].includes( field.id ) ); const hasData = !! data?.length; - const sortValues = { asc: 'ascending', desc: 'descending' }; const primaryField = fields.find( ( field ) => field.id === view.layout.primaryField @@ -450,8 +511,9 @@ function ViewTable( { } } data-field-id={ field.id } aria-sort={ - view.sort?.field === field.id && - sortValues[ view.sort.direction ] + view.sort?.field === field.id + ? sortValues[ view.sort.direction ] + : undefined } scope="col" > @@ -504,7 +566,7 @@ function ViewTable( { item={ item } hasBulkActions={ hasBulkActions } actions={ actions } - id={ getItemId( item ) || index } + id={ getItemId( item ) || index.toString() } visibleFields={ visibleFields } primaryField={ primaryField } selection={ selection }