From 9f6dc45729f50c5bda1e19cb27f3fcabe41eb410 Mon Sep 17 00:00:00 2001 From: John Hooks Date: Fri, 10 May 2024 20:25:49 -0700 Subject: [PATCH 1/4] fix: improve dataview types 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. - Add an `Action` type for the dataview actions. --- .../src/filter-and-sort-data-view.ts | 16 +++-- packages/dataviews/src/normalize-fields.ts | 6 +- packages/dataviews/src/types.ts | 72 ++++++++++++++----- 3 files changed, 68 insertions(+), 26 deletions(-) diff --git a/packages/dataviews/src/filter-and-sort-data-view.ts b/packages/dataviews/src/filter-and-sort-data-view.ts index dcbff18045dc93..d69b38742c611b 100644 --- a/packages/dataviews/src/filter-and-sort-data-view.ts +++ b/packages/dataviews/src/filter-and-sort-data-view.ts @@ -15,7 +15,7 @@ import { OPERATOR_IS_NOT_ALL, } from './constants'; import { normalizeFields } from './normalize-fields'; -import type { Data, Field, View } from './types'; +import type { Data, Field, Item, View } from './types'; function normalizeSearchInput( input = '' ) { return removeAccents( input.trim().toLowerCase() ); @@ -32,14 +32,14 @@ const EMPTY_ARRAY: Data = []; * * @return Filtered, sorted and paginated data. */ -export function filterSortAndPaginate( - data: Data, +export function filterSortAndPaginate< T extends Item >( + data: T[], view: View, - fields: Field[] + fields: Field< T >[] ): { data: Data; paginationInfo: { totalItems: number; totalPages: number } } { if ( ! data ) { return { - data: EMPTY_ARRAY, + data: EMPTY_ARRAY as T[], paginationInfo: { totalItems: 0, totalPages: 0 }, }; } @@ -100,7 +100,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 +113,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/normalize-fields.ts b/packages/dataviews/src/normalize-fields.ts index 8745a1b3316bc5..2bcf775a1633d1 100644 --- a/packages/dataviews/src/normalize-fields.ts +++ b/packages/dataviews/src/normalize-fields.ts @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import type { Field, NormalizedField } from './types'; +import type { Field, Item, NormalizedField } from './types'; /** * Apply default values and normalize the fields config. @@ -9,7 +9,9 @@ import type { Field, NormalizedField } from './types'; * @param fields Fields config. * @return Normalized fields config. */ -export function normalizeFields( fields: Field[] ): NormalizedField[] { +export function normalizeFields< T extends Item >( + fields: Field< T >[] +): NormalizedField< T >[] { return fields.map( ( field ) => { const getValue = field.getValue || ( ( { item } ) => item[ field.id ] ); diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index 7c367aace07ba2..87d64e42f89434 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -3,13 +3,38 @@ */ import type { ReactElement, ReactNode } from 'react'; -interface Option { - value: any; +export type SortDirection = 'asc' | 'desc'; + +/** + * Type of view UI. + */ +export type ViewType = 'table' | 'grid' | 'list'; + +/** + * Type of field. + */ +export type FieldType = 'enumeration'; + +/** + * Generic option type. + */ +interface Option< Value extends any = any > { + value: Value; label: string; } -interface filterByConfig { +interface FilterByConfig { + /** + * The list of operators supported by the field. + */ operators?: Operator[]; + + /** + * Whether it is a primary filter. + * + * A primary filter is always visible and is not listed in the "Add filter" component, + * except for the list layout where it behaves like a secondary filter. + */ isPrimary?: boolean; } @@ -17,7 +42,10 @@ type Operator = 'is' | 'isNot' | 'isAny' | 'isNone' | 'isAll' | 'isNotAll'; export type Item = Record< string, any >; -export interface Field { +/** + * A dataview field for a specific property of a data type. + */ +export interface Field< T extends Item > { /** * The unique identifier of the field. */ @@ -32,58 +60,67 @@ export interface Field { * Callback used to retrieve the value of the field from the item. * Defaults to `item[ field.id ]`. */ - getValue?: ( { item }: { item: Item } ) => any; + getValue?: ( args: { item: T } ) => string | undefined; /** * Callback used to render the field. Defaults to `field.getValue`. */ - render?: ( { item }: { item: Item } ) => ReactNode; + render?: ( args: { item: T } ) => ReactNode; /** * The width of the field column. */ - width: string | number | undefined; + width?: string | number; /** * The minimum width of the field column. */ - maxWidth: string | number | undefined; + maxWidth?: string | number; /** * The maximum width of the field column. */ - minWidth: string | number | undefined; + minWidth?: string | number; /** * Whether the field is sortable. */ - enableSorting: boolean | undefined; + enableSorting?: boolean; /** * Whether the field is searchable. */ - enableGlobalSearch: boolean | undefined; + enableGlobalSearch?: boolean; /** * Whether the field is filterable. */ - enableHiding: boolean | undefined; + enableHiding?: boolean; /** * The list of options to pick from when using the field as a filter. */ - elements: Option[] | undefined; + elements?: Option[]; /** * Filter config for the field. */ - filterBy: filterByConfig | undefined; + filterBy?: FilterByConfig | undefined; } -export type NormalizedField = Required< Field >; +export type NormalizedField< T extends Item > = Field< T > & + Required< Pick< Field< T >, 'header' | 'getValue' | 'render' > >; + +/** + * A collection of dataview fields for a data type. + */ +export type Fields< T extends Item > = Field< T >[]; export type Data = Item[]; +/** + * The filters applied to the dataset. + */ export interface Filter { /** * The field to filter by. @@ -105,7 +142,7 @@ interface ViewBase { /** * The layout of the view. */ - type: string; + type: ViewType; /** * The global search term. @@ -129,7 +166,7 @@ interface ViewBase { /** * The direction to sort by. */ - direction: string; + direction: SortDirection; }; /** @@ -147,6 +184,7 @@ interface ViewBase { */ hiddenFields: string[]; } + export interface ViewList extends ViewBase { type: 'list'; From 1b6bce2e509ec6102725e3a50e0aea05a841ac6c Mon Sep 17 00:00:00 2001 From: John Hooks Date: Tue, 14 May 2024 15:49:38 -0700 Subject: [PATCH 2/4] feat: add generic types to DataView action type This commit expands the original PR to include generic types for `@wordpress/dataview` actions. This commit also does the following: - Applies several CR suggestions from @youknowriad and @sirreal. - Adds generic types to to all .ts/.tsx files in the dataviews. - Renames `Item` to `AnyItem` to match the pattern of `@wordpress/data`. - Name all generic types to `Item` which helps to make the code more readable, despite the fact TypeScript generics are in general cryptic. --- .../src/filter-and-sort-data-view.ts | 17 ++--- packages/dataviews/src/item-actions.tsx | 63 +++++++++++-------- packages/dataviews/src/normalize-fields.ts | 8 +-- .../src/single-selection-checkbox.tsx | 12 ++-- packages/dataviews/src/types.ts | 40 +++++------- packages/dataviews/src/view-list.tsx | 31 ++++----- 6 files changed, 90 insertions(+), 81 deletions(-) diff --git a/packages/dataviews/src/filter-and-sort-data-view.ts b/packages/dataviews/src/filter-and-sort-data-view.ts index d69b38742c611b..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, Item, 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,14 +32,17 @@ const EMPTY_ARRAY: Data = []; * * @return Filtered, sorted and paginated data. */ -export function filterSortAndPaginate< T extends Item >( - data: T[], +export function filterSortAndPaginate< Item extends AnyItem >( + data: Item[], view: View, - fields: Field< T >[] -): { data: Data; paginationInfo: { totalItems: number; totalPages: number } } { + fields: Field< Item >[] +): { + data: Item[]; + paginationInfo: { totalItems: number; totalPages: number }; +} { if ( ! data ) { return { - data: EMPTY_ARRAY as T[], + data: EMPTY_ARRAY, paginationInfo: { totalItems: 0, totalPages: 0 }, }; } 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 (