From fe67010143b20d23349051e80110aa1424e79999 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Thu, 27 Jun 2024 17:43:28 +0300 Subject: [PATCH] DataViews: simplify selection setting (#62846) Co-authored-by: ellatrix Co-authored-by: youknowriad Co-authored-by: jorgefilipecosta Co-authored-by: sirreal Co-authored-by: jsnajdr Co-authored-by: luisherranz Co-authored-by: oandregal --- .../dataviews/src/bulk-actions-toolbar.tsx | 7 +-- packages/dataviews/src/bulk-actions.tsx | 9 +++- packages/dataviews/src/dataviews.tsx | 43 +++++++++---------- packages/dataviews/src/private-types.tsx | 2 + .../src/single-selection-checkbox.tsx | 37 +++++----------- packages/dataviews/src/types.ts | 7 ++- packages/dataviews/src/view-grid.tsx | 33 +++----------- packages/dataviews/src/view-list.tsx | 6 +-- packages/dataviews/src/view-table.tsx | 39 +++++------------ 9 files changed, 70 insertions(+), 113 deletions(-) create mode 100644 packages/dataviews/src/private-types.tsx diff --git a/packages/dataviews/src/bulk-actions-toolbar.tsx b/packages/dataviews/src/bulk-actions-toolbar.tsx index 50a1386aadec0..07edaa6cd394c 100644 --- a/packages/dataviews/src/bulk-actions-toolbar.tsx +++ b/packages/dataviews/src/bulk-actions-toolbar.tsx @@ -20,6 +20,7 @@ import { useRegistry } from '@wordpress/data'; import { ActionWithModal } from './item-actions'; import type { Action } from './types'; import type { ActionTriggerProps } from './item-actions'; +import type { SetSelection } from './private-types'; interface ActionButtonProps< Item > { action: Action< Item >; @@ -32,14 +33,14 @@ interface ToolbarContentProps< Item > { selection: string[]; actionsToShow: Action< Item >[]; selectedItems: Item[]; - onSelectionChange: ( selection: Item[] ) => void; + onSelectionChange: SetSelection; } interface BulkActionsToolbarProps< Item > { data: Item[]; selection: string[]; actions: Action< Item >[]; - onSelectionChange: ( selection: Item[] ) => void; + onSelectionChange: SetSelection; getItemId: ( item: Item ) => string; } @@ -131,7 +132,7 @@ function renderToolbarContent< Item >( selectedItems: Item[], actionInProgress: string | null, setActionInProgress: ( actionId: string | null ) => void, - onSelectionChange: ( selection: Item[] ) => void + onSelectionChange: SetSelection ) { return ( <> diff --git a/packages/dataviews/src/bulk-actions.tsx b/packages/dataviews/src/bulk-actions.tsx index 7f743bbeea1a2..adafbe5799ccb 100644 --- a/packages/dataviews/src/bulk-actions.tsx +++ b/packages/dataviews/src/bulk-actions.tsx @@ -15,6 +15,7 @@ import { useRegistry } from '@wordpress/data'; */ import { unlock } from './lock-unlock'; import type { Action, ActionModal } from './types'; +import type { SetSelection } from './private-types'; const { DropdownMenuV2: DropdownMenu, @@ -46,7 +47,7 @@ interface BulkActionsProps< Item > { data: Item[]; actions: Action< Item >[]; selection: string[]; - onSelectionChange: ( selection: Item[] ) => void; + onSelectionChange: SetSelection; getItemId: ( item: Item ) => string; } @@ -248,7 +249,11 @@ export default function BulkActions< Item >( { disabled={ areAllSelected } hideOnClick={ false } onClick={ () => { - onSelectionChange( selectableItems ); + onSelectionChange( + selectableItems.map( ( item ) => + getItemId( item ) + ) + ); } } suffix={ numberSelectableItems } > diff --git a/packages/dataviews/src/dataviews.tsx b/packages/dataviews/src/dataviews.tsx index 476ed895ed529..f179ee71dcf04 100644 --- a/packages/dataviews/src/dataviews.tsx +++ b/packages/dataviews/src/dataviews.tsx @@ -7,7 +7,7 @@ import type { ComponentType } from 'react'; * WordPress dependencies */ import { __experimentalHStack as HStack } from '@wordpress/components'; -import { useMemo, useState, useCallback } from '@wordpress/element'; +import { useMemo, useState } from '@wordpress/element'; /** * Internal dependencies @@ -22,6 +22,7 @@ import BulkActions from './bulk-actions'; import { normalizeFields } from './normalize-fields'; import BulkActionsToolbar from './bulk-actions-toolbar'; import type { Action, Field, View, ViewBaseProps } from './types'; +import type { SetSelection, SelectionOrUpdater } from './private-types'; type ItemWithId = { id: string }; @@ -40,7 +41,7 @@ type DataViewsProps< Item > = { }; supportedLayouts: string[]; selection?: string[]; - setSelection?: ( selection: string[] ) => void; + setSelection?: SetSelection; onSelectionChange?: ( items: Item[] ) => void; } & ( Item extends ItemWithId ? { getItemId?: ( item: Item ) => string } @@ -83,26 +84,22 @@ export default function DataViews< Item >( { onSelectionChange = defaultOnSelectionChange, }: DataViewsProps< Item > ) { const [ selectionState, setSelectionState ] = useState< string[] >( [] ); - let selection, setSelection; - if ( - selectionProperty !== undefined && - setSelectionProperty !== undefined - ) { - selection = selectionProperty; - setSelection = setSelectionProperty; - } else { - selection = selectionState; - setSelection = setSelectionState; - } + const isUncontrolled = + selectionProperty === undefined || setSelectionProperty === undefined; + const selection = isUncontrolled ? selectionState : selectionProperty; + const setSelection = isUncontrolled + ? setSelectionState + : setSelectionProperty; const [ openedFilter, setOpenedFilter ] = useState< string | null >( null ); - const onSetSelection = useCallback( - ( items: Item[] ) => { - setSelection( items.map( ( item ) => getItemId( item ) ) ); - onSelectionChange( items ); - }, - [ setSelection, getItemId, onSelectionChange ] - ); + function setSelectionWithChange( value: SelectionOrUpdater ) { + const newValue = + typeof value === 'function' ? value( selection ) : value; + onSelectionChange( + data.filter( ( item ) => newValue.includes( getItemId( item ) ) ) + ); + return setSelection( value ); + } const ViewComponent = VIEW_LAYOUTS.find( ( v ) => v.type === view.type ) ?.component as ComponentType< ViewBaseProps< Item > >; @@ -149,7 +146,7 @@ export default function DataViews< Item >( { @@ -168,7 +165,7 @@ export default function DataViews< Item >( { getItemId={ getItemId } isLoading={ isLoading } onChangeView={ onChangeView } - onSelectionChange={ onSetSelection } + onSelectionChange={ setSelectionWithChange } selection={ _selection } setOpenedFilter={ setOpenedFilter } view={ view } @@ -184,7 +181,7 @@ export default function DataViews< Item >( { data={ data } actions={ actions } selection={ _selection } - onSelectionChange={ onSetSelection } + onSelectionChange={ setSelectionWithChange } getItemId={ getItemId } /> ) } diff --git a/packages/dataviews/src/private-types.tsx b/packages/dataviews/src/private-types.tsx new file mode 100644 index 0000000000000..d2d453e63599c --- /dev/null +++ b/packages/dataviews/src/private-types.tsx @@ -0,0 +1,2 @@ +export type SelectionOrUpdater = string[] | ( ( prev: string[] ) => string[] ); +export type SetSelection = ( selection: SelectionOrUpdater ) => void; diff --git a/packages/dataviews/src/single-selection-checkbox.tsx b/packages/dataviews/src/single-selection-checkbox.tsx index 84b359508663b..c8490a8c71c32 100644 --- a/packages/dataviews/src/single-selection-checkbox.tsx +++ b/packages/dataviews/src/single-selection-checkbox.tsx @@ -8,12 +8,12 @@ import { CheckboxControl } from '@wordpress/components'; * Internal dependencies */ import type { Field } from './types'; +import type { SetSelection } from './private-types'; interface SingleSelectionCheckboxProps< Item > { selection: string[]; - onSelectionChange: ( selection: Item[] ) => void; + onSelectionChange: SetSelection; item: Item; - data: Item[]; getItemId: ( item: Item ) => string; primaryField?: Field< Item >; disabled: boolean; @@ -23,23 +23,22 @@ export default function SingleSelectionCheckbox< Item >( { selection, onSelectionChange, item, - data, getItemId, primaryField, disabled, }: SingleSelectionCheckboxProps< Item > ) { const id = getItemId( item ); - const isSelected = ! disabled && selection.includes( id ); + const checked = ! disabled && selection.includes( id ); let selectionLabel; if ( primaryField?.getValue && item ) { // eslint-disable-next-line @wordpress/valid-sprintf selectionLabel = sprintf( /* translators: %s: item title. */ - isSelected ? __( 'Deselect item: %s' ) : __( 'Select item: %s' ), + checked ? __( 'Deselect item: %s' ) : __( 'Select item: %s' ), primaryField.getValue( { item } ) ); } else { - selectionLabel = isSelected + selectionLabel = checked ? __( 'Select a new item' ) : __( 'Deselect item' ); } @@ -49,31 +48,17 @@ export default function SingleSelectionCheckbox< Item >( { __nextHasNoMarginBottom aria-label={ selectionLabel } aria-disabled={ disabled } - checked={ isSelected } + checked={ checked } onChange={ () => { if ( disabled ) { return; } - if ( ! isSelected ) { - onSelectionChange( - data.filter( ( _item ) => { - const itemId = getItemId?.( _item ); - return ( - itemId === id || selection.includes( itemId ) - ); - } ) - ); - } else { - onSelectionChange( - data.filter( ( _item ) => { - const itemId = getItemId?.( _item ); - return ( - itemId !== id && selection.includes( itemId ) - ); - } ) - ); - } + onSelectionChange( + selection.includes( id ) + ? selection.filter( ( itemId ) => id !== itemId ) + : [ ...selection, id ] + ); } } /> ); diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index 76b514755056a..964523c72f8a6 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -3,6 +3,11 @@ */ import type { ReactElement, ReactNode } from 'react'; +/** + * Internal dependencies + */ +import type { SetSelection } from './private-types'; + export type SortDirection = 'asc' | 'desc'; /** @@ -383,7 +388,7 @@ export interface ViewBaseProps< Item > { getItemId: ( item: Item ) => string; isLoading?: boolean; onChangeView( view: View ): void; - onSelectionChange: ( items: Item[] ) => void; + onSelectionChange: SetSelection; selection: string[]; setOpenedFilter: ( fieldId: string ) => void; view: View; diff --git a/packages/dataviews/src/view-grid.tsx b/packages/dataviews/src/view-grid.tsx index 8fa9d6413d851..56f856a5d82b2 100644 --- a/packages/dataviews/src/view-grid.tsx +++ b/packages/dataviews/src/view-grid.tsx @@ -23,11 +23,11 @@ import ItemActions from './item-actions'; import SingleSelectionCheckbox from './single-selection-checkbox'; import { useHasAPossibleBulkAction } from './bulk-actions'; import type { Action, NormalizedField, ViewGridProps } from './types'; +import type { SetSelection } from './private-types'; interface GridItemProps< Item > { selection: string[]; - data: Item[]; - onSelectionChange: ( items: Item[] ) => void; + onSelectionChange: SetSelection; getItemId: ( item: Item ) => string; item: Item; actions: Action< Item >[]; @@ -40,7 +40,6 @@ interface GridItemProps< Item > { function GridItem< Item >( { selection, - data, onSelectionChange, getItemId, item, @@ -68,27 +67,11 @@ function GridItem< Item >( { if ( ! hasBulkAction ) { return; } - if ( ! isSelected ) { - onSelectionChange( - data.filter( ( _item ) => { - const itemId = getItemId?.( _item ); - return ( - itemId === id || - selection.includes( itemId ) - ); - } ) - ); - } else { - onSelectionChange( - data.filter( ( _item ) => { - const itemId = getItemId?.( _item ); - return ( - itemId !== id && - selection.includes( itemId ) - ); - } ) - ); - } + onSelectionChange( + selection.includes( id ) + ? selection.filter( ( itemId ) => id !== itemId ) + : [ ...selection, id ] + ); } } } > @@ -104,7 +87,6 @@ function GridItem< Item >( { selection={ selection } onSelectionChange={ onSelectionChange } getItemId={ getItemId } - data={ data } primaryField={ primaryField } disabled={ ! hasBulkAction } /> @@ -239,7 +221,6 @@ export default function ViewGrid< Item >( { ( props: ViewListProps< Item > ) { ) ); - const onSelect = useCallback( - ( item: Item ) => onSelectionChange( [ item ] ), - [ onSelectionChange ] - ); + const onSelect = ( item: Item ) => + onSelectionChange( [ getItemId( item ) ] ); const getItemDomId = useCallback( ( item?: Item ) => diff --git a/packages/dataviews/src/view-table.tsx b/packages/dataviews/src/view-table.tsx index f09b46733e1a8..fbd1400a301a1 100644 --- a/packages/dataviews/src/view-table.tsx +++ b/packages/dataviews/src/view-table.tsx @@ -51,6 +51,7 @@ import type { ViewTable as ViewTableType, ViewTableProps, } from './types'; +import type { SetSelection } from './private-types'; const { DropdownMenuV2: DropdownMenu, @@ -71,7 +72,7 @@ interface HeaderMenuProps< Item > { interface BulkSelectionCheckboxProps< Item > { selection: string[]; - onSelectionChange: ( items: Item[] ) => void; + onSelectionChange: SetSelection; data: Item[]; actions: Action< Item >[]; getItemId: ( item: Item ) => string; @@ -86,8 +87,7 @@ interface TableRowProps< Item > { primaryField?: NormalizedField< Item >; selection: string[]; getItemId: ( item: Item ) => string; - onSelectionChange: ( items: Item[] ) => void; - data: Item[]; + onSelectionChange: SetSelection; } function WithDropDownMenuSeparators( { children }: { children: ReactNode } ) { @@ -276,7 +276,9 @@ function BulkSelectionCheckbox< Item >( { if ( areAllSelected ) { onSelectionChange( [] ); } else { - onSelectionChange( selectableItems ); + onSelectionChange( + selectableItems.map( ( item ) => getItemId( item ) ) + ); } } } aria-label={ @@ -296,7 +298,6 @@ function TableRow< Item >( { selection, getItemId, onSelectionChange, - data, }: TableRowProps< Item > ) { const hasPossibleBulkAction = useHasAPossibleBulkAction( actions, item ); const isSelected = hasPossibleBulkAction && selection.includes( id ); @@ -336,27 +337,11 @@ function TableRow< Item >( { ! isTouchDevice.current && document.getSelection()?.type !== 'Range' ) { - if ( ! isSelected ) { - onSelectionChange( - data.filter( ( _item ) => { - const itemId = getItemId?.( _item ); - return ( - itemId === id || - selection.includes( itemId ) - ); - } ) - ); - } else { - onSelectionChange( - data.filter( ( _item ) => { - const itemId = getItemId?.( _item ); - return ( - itemId !== id && - selection.includes( itemId ) - ); - } ) - ); - } + onSelectionChange( + selection.includes( id ) + ? selection.filter( ( itemId ) => id !== itemId ) + : [ ...selection, id ] + ); } } } > @@ -373,7 +358,6 @@ function TableRow< Item >( { selection={ selection } onSelectionChange={ onSelectionChange } getItemId={ getItemId } - data={ data } primaryField={ primaryField } disabled={ ! hasPossibleBulkAction } /> @@ -579,7 +563,6 @@ function ViewTable< Item >( { selection={ selection } getItemId={ getItemId } onSelectionChange={ onSelectionChange } - data={ data } /> ) ) }