From 44bca551553b808526ff112b295328457a076b44 Mon Sep 17 00:00:00 2001 From: John Hooks Date: Wed, 15 May 2024 02:53:09 -0700 Subject: [PATCH] fix: improve dataview types (#61586) This commit improves the types in the dataviews package. This is helpful by typing the `item` or `items` provided as arguments to the functions of the fields and actions of the `DataViews`. This commit does not implement requiring these types in the `DataView` component, but helps to improve the types of the functions that are used in the `DataView` package. It also allows exporting the types to be used in consumers of the package. - Add generic item type to all dataview types that operate on `item` or lists of `items`. - Update the usage in the dataviews package to incorporate the use of the generic item type. Co-authored-by: johnhooks Co-authored-by: youknowriad Co-authored-by: sirreal --- packages/dataviews/src/bulk-actions.tsx | 55 +++++++------- .../src/filter-and-sort-data-view.ts | 21 +++-- packages/dataviews/src/item-actions.tsx | 63 ++++++++------- packages/dataviews/src/normalize-fields.ts | 6 +- .../src/single-selection-checkbox.tsx | 12 +-- packages/dataviews/src/types.ts | 76 +++++++++++++------ packages/dataviews/src/view-list.tsx | 31 ++++---- 7 files changed, 159 insertions(+), 105 deletions(-) diff --git a/packages/dataviews/src/bulk-actions.tsx b/packages/dataviews/src/bulk-actions.tsx index 0e9d6f3134d389..f2bb80dc3292e4 100644 --- a/packages/dataviews/src/bulk-actions.tsx +++ b/packages/dataviews/src/bulk-actions.tsx @@ -13,7 +13,7 @@ import { useMemo, useState, useCallback, useEffect } from '@wordpress/element'; * Internal dependencies */ import { unlock } from './lock-unlock'; -import type { Action, ActionModal, Data, Item } from './types'; +import type { Action, ActionModal, AnyItem } from './types'; const { DropdownMenuV2: DropdownMenu, @@ -22,34 +22,37 @@ const { DropdownMenuSeparatorV2: DropdownMenuSeparator, } = unlock( componentsPrivateApis ); -interface ActionWithModalProps { - action: ActionModal; +interface ActionWithModalProps< Item extends AnyItem > { + action: ActionModal< Item >; selectedItems: Item[]; - setActionWithModal: ( action?: ActionModal ) => void; + setActionWithModal: ( action?: ActionModal< Item > ) => void; onMenuOpenChange: ( isOpen: boolean ) => void; } -interface BulkActionsItemProps { - action: Action; +interface BulkActionsItemProps< Item extends AnyItem > { + action: Action< Item >; selectedItems: Item[]; - setActionWithModal: ( action?: ActionModal ) => void; + setActionWithModal: ( action?: ActionModal< Item > ) => void; } -interface ActionsMenuGroupProps { - actions: Action[]; +interface ActionsMenuGroupProps< Item extends AnyItem > { + actions: Action< Item >[]; selectedItems: Item[]; - setActionWithModal: ( action?: ActionModal ) => void; + setActionWithModal: ( action?: ActionModal< Item > ) => void; } -interface BulkActionsProps { - data: Data; - actions: Action[]; +interface BulkActionsProps< Item extends AnyItem > { + data: Item[]; + actions: Action< Item >[]; selection: string[]; onSelectionChange: ( selection: Item[] ) => void; getItemId: ( item: Item ) => string; } -export function useHasAPossibleBulkAction( actions: Action[], item: Item ) { +export function useHasAPossibleBulkAction< Item extends AnyItem >( + actions: Action< Item >[], + item: Item +) { return useMemo( () => { return actions.some( ( action ) => { return ( @@ -60,9 +63,9 @@ export function useHasAPossibleBulkAction( actions: Action[], item: Item ) { }, [ actions, item ] ); } -export function useSomeItemHasAPossibleBulkAction( - actions: Action[], - data: Data +export function useSomeItemHasAPossibleBulkAction< Item extends AnyItem >( + actions: Action< Item >[], + data: Item[] ) { return useMemo( () => { return data.some( ( item ) => { @@ -76,12 +79,12 @@ export function useSomeItemHasAPossibleBulkAction( }, [ actions, data ] ); } -function ActionWithModal( { +function ActionWithModal< Item extends AnyItem >( { action, selectedItems, setActionWithModal, onMenuOpenChange, -}: ActionWithModalProps ) { +}: ActionWithModalProps< Item > ) { const eligibleItems = useMemo( () => { return selectedItems.filter( ( item ) => ! action.isEligible || action.isEligible( item ) @@ -107,11 +110,11 @@ function ActionWithModal( { ); } -function BulkActionItem( { +function BulkActionItem< Item extends AnyItem >( { action, selectedItems, setActionWithModal, -}: BulkActionsItemProps ) { +}: BulkActionsItemProps< Item > ) { const eligibleItems = useMemo( () => { return selectedItems.filter( ( item ) => ! action.isEligible || action.isEligible( item ) @@ -141,11 +144,11 @@ function BulkActionItem( { ); } -function ActionsMenuGroup( { +function ActionsMenuGroup< Item extends AnyItem >( { actions, selectedItems, setActionWithModal, -}: ActionsMenuGroupProps ) { +}: ActionsMenuGroupProps< Item > ) { return ( <> @@ -163,20 +166,20 @@ function ActionsMenuGroup( { ); } -export default function BulkActions( { +export default function BulkActions< Item extends AnyItem >( { data, actions, selection, onSelectionChange, getItemId, -}: BulkActionsProps ) { +}: BulkActionsProps< Item > ) { const bulkActions = useMemo( () => actions.filter( ( action ) => action.supportsBulk ), [ actions ] ); const [ isMenuOpen, onMenuOpenChange ] = useState( false ); const [ actionWithModal, setActionWithModal ] = useState< - ActionModal | undefined + ActionModal< Item > | undefined >(); const selectableItems = useMemo( () => { return data.filter( ( item ) => { diff --git a/packages/dataviews/src/filter-and-sort-data-view.ts b/packages/dataviews/src/filter-and-sort-data-view.ts index dcbff18045dc93..a2906fdc4869e3 100644 --- a/packages/dataviews/src/filter-and-sort-data-view.ts +++ b/packages/dataviews/src/filter-and-sort-data-view.ts @@ -15,13 +15,13 @@ import { OPERATOR_IS_NOT_ALL, } from './constants'; import { normalizeFields } from './normalize-fields'; -import type { Data, Field, View } from './types'; +import type { Field, AnyItem, View } from './types'; function normalizeSearchInput( input = '' ) { return removeAccents( input.trim().toLowerCase() ); } -const EMPTY_ARRAY: Data = []; +const EMPTY_ARRAY: [] = []; /** * Applies the filtering, sorting and pagination to the raw data based on the view configuration. @@ -32,11 +32,14 @@ const EMPTY_ARRAY: Data = []; * * @return Filtered, sorted and paginated data. */ -export function filterSortAndPaginate( - data: Data, +export function filterSortAndPaginate< Item extends AnyItem >( + data: Item[], view: View, - fields: Field[] -): { data: Data; paginationInfo: { totalItems: number; totalPages: number } } { + fields: Field< Item >[] +): { + data: Item[]; + paginationInfo: { totalItems: number; totalPages: number }; +} { if ( ! data ) { return { data: EMPTY_ARRAY, @@ -100,7 +103,9 @@ export function filterSortAndPaginate( ) { filteredData = filteredData.filter( ( item ) => { return filter.value.every( ( value: any ) => { - return field.getValue( { item } ).includes( value ); + return field + .getValue( { item } ) + ?.includes( value ); } ); } ); } else if ( @@ -111,7 +116,7 @@ export function filterSortAndPaginate( return filter.value.every( ( value: any ) => { return ! field .getValue( { item } ) - .includes( value ); + ?.includes( value ); } ); } ); } else if ( filter.operator === OPERATOR_IS ) { diff --git a/packages/dataviews/src/item-actions.tsx b/packages/dataviews/src/item-actions.tsx index cbf4cdc479d437..3b45561defdd68 100644 --- a/packages/dataviews/src/item-actions.tsx +++ b/packages/dataviews/src/item-actions.tsx @@ -20,7 +20,7 @@ import { moreVertical } from '@wordpress/icons'; * Internal dependencies */ import { unlock } from './lock-unlock'; -import type { Action, ActionModal as ActionModalType, Item } from './types'; +import type { Action, ActionModal as ActionModalType, AnyItem } from './types'; const { DropdownMenuV2: DropdownMenu, @@ -30,46 +30,50 @@ const { kebabCase, } = unlock( componentsPrivateApis ); -interface ButtonTriggerProps { - action: Action; +interface ButtonTriggerProps< Item extends AnyItem > { + action: Action< Item >; onClick: MouseEventHandler; } -interface DropdownMenuItemTriggerProps { - action: Action; +interface DropdownMenuItemTriggerProps< Item extends AnyItem > { + action: Action< Item >; onClick: MouseEventHandler; } -interface ActionModalProps { - action: ActionModalType; +interface ActionModalProps< Item extends AnyItem > { + action: ActionModalType< Item >; items: Item[]; closeModal?: () => void; } -interface ActionWithModalProps extends ActionModalProps { +interface ActionWithModalProps< Item extends AnyItem > + extends ActionModalProps< Item > { ActionTrigger: ( - props: ButtonTriggerProps | DropdownMenuItemTriggerProps + props: ButtonTriggerProps< Item > | DropdownMenuItemTriggerProps< Item > ) => ReactElement; isBusy?: boolean; } -interface ActionsDropdownMenuGroupProps { - actions: Action[]; +interface ActionsDropdownMenuGroupProps< Item extends AnyItem > { + actions: Action< Item >[]; item: Item; } -interface ItemActionsProps { +interface ItemActionsProps< Item extends AnyItem > { item: Item; - actions: Action[]; + actions: Action< Item >[]; isCompact: boolean; } -interface CompactItemActionsProps { +interface CompactItemActionsProps< Item extends AnyItem > { item: Item; - actions: Action[]; + actions: Action< Item >[]; } -function ButtonTrigger( { action, onClick }: ButtonTriggerProps ) { +function ButtonTrigger< Item extends AnyItem >( { + action, + onClick, +}: ButtonTriggerProps< Item > ) { return (