Skip to content

Commit

Permalink
feat: add generic types to DataView action type
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
johnhooks committed May 14, 2024
1 parent 3136424 commit d876c49
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 82 deletions.
17 changes: 10 additions & 7 deletions packages/dataviews/src/filter-and-sort-data-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 },
};
}
Expand Down
61 changes: 34 additions & 27 deletions packages/dataviews/src/item-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -30,48 +30,52 @@ 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;
onActionStart?: ( items: Item[] ) => void;
onActionPerformed?: ( items: Item[] ) => 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 (
<Button
label={ action.label }
Expand All @@ -83,10 +87,10 @@ function ButtonTrigger( { action, onClick }: ButtonTriggerProps ) {
);
}

function DropdownMenuItemTrigger( {
function DropdownMenuItemTrigger< Item extends AnyItem >( {
action,
onClick,
}: DropdownMenuItemTriggerProps ) {
}: DropdownMenuItemTriggerProps< Item > ) {
return (
<DropdownMenuItem
onClick={ onClick }
Expand All @@ -97,13 +101,13 @@ function DropdownMenuItemTrigger( {
);
}

export function ActionModal( {
export function ActionModal< Item extends AnyItem >( {
action,
items,
closeModal,
onActionStart,
onActionPerformed,
}: ActionModalProps ) {
}: ActionModalProps< Item > ) {
return (
<Modal
title={ action.modalHeader || action.label }
Expand All @@ -123,14 +127,14 @@ export function ActionModal( {
);
}

export function ActionWithModal( {
export function ActionWithModal< Item extends AnyItem >( {
action,
items,
ActionTrigger,
onActionStart,
onActionPerformed,
isBusy,
}: ActionWithModalProps ) {
}: ActionWithModalProps< Item > ) {
const [ isModalOpen, setIsModalOpen ] = useState( false );
const actionTriggerProps = {
action,
Expand All @@ -156,10 +160,10 @@ export function ActionWithModal( {
);
}

export function ActionsDropdownMenuGroup( {
export function ActionsDropdownMenuGroup< Item extends AnyItem >( {
actions,
item,
}: ActionsDropdownMenuGroupProps ) {
}: ActionsDropdownMenuGroupProps< Item > ) {
return (
<DropdownMenuGroup>
{ actions.map( ( action ) => {
Expand All @@ -185,11 +189,11 @@ export function ActionsDropdownMenuGroup( {
);
}

export default function ItemActions( {
export default function ItemActions< Item extends AnyItem >( {
item,
actions,
isCompact,
}: ItemActionsProps ) {
}: ItemActionsProps< Item > ) {
const { primaryActions, eligibleActions } = useMemo( () => {
// If an action is eligible for all items, doesn't need
// to provide the `isEligible` function.
Expand Down Expand Up @@ -242,7 +246,10 @@ export default function ItemActions( {
);
}

function CompactItemActions( { item, actions }: CompactItemActionsProps ) {
function CompactItemActions< Item extends AnyItem >( {
item,
actions,
}: CompactItemActionsProps< Item > ) {
return (
<DropdownMenu
trigger={
Expand Down
8 changes: 4 additions & 4 deletions packages/dataviews/src/normalize-fields.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
/**
* Internal dependencies
*/
import type { Field, Item, NormalizedField } from './types';
import type { Field, AnyItem, NormalizedField } from './types';

/**
* Apply default values and normalize the fields config.
*
* @param fields Fields config.
* @return Normalized fields config.
*/
export function normalizeFields< T extends Item >(
fields: Field< T >[]
): NormalizedField< T >[] {
export function normalizeFields< Item extends AnyItem >(
fields: Field< Item >[]
): NormalizedField< Item >[] {
return fields.map( ( field ) => {
const getValue = field.getValue || ( ( { item } ) => item[ field.id ] );

Expand Down
12 changes: 6 additions & 6 deletions packages/dataviews/src/single-selection-checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,27 @@ import { CheckboxControl } from '@wordpress/components';
/**
* Internal dependencies
*/
import type { Data, Field, Item } from './types';
import type { Field, AnyItem } from './types';

interface SingleSelectionCheckboxProps {
interface SingleSelectionCheckboxProps< Item extends AnyItem > {
selection: string[];
onSelectionChange: ( selection: Item[] ) => void;
item: Item;
data: Data;
data: Item[];
getItemId: ( item: Item ) => string;
primaryField?: Field;
primaryField?: Field< Item >;
disabled: boolean;
}

export default function SingleSelectionCheckbox( {
export default function SingleSelectionCheckbox< Item extends AnyItem >( {
selection,
onSelectionChange,
item,
data,
getItemId,
primaryField,
disabled,
}: SingleSelectionCheckboxProps ) {
}: SingleSelectionCheckboxProps< Item > ) {
const id = getItemId( item );
const isSelected = selection.includes( id );
let selectionLabel;
Expand Down
40 changes: 17 additions & 23 deletions packages/dataviews/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,6 @@ import type { ReactElement, ReactNode } from 'react';

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.
*/
Expand All @@ -40,12 +30,12 @@ interface FilterByConfig {

type Operator = 'is' | 'isNot' | 'isAny' | 'isNone' | 'isAll' | 'isNotAll';

export type Item = Record< string, any >;
export type AnyItem = Record< string, any >;

/**
* A dataview field for a specific property of a data type.
*/
export interface Field< T extends Item > {
export interface Field< Item extends AnyItem > {
/**
* The unique identifier of the field.
*/
Expand All @@ -60,12 +50,12 @@ export interface Field< T extends Item > {
* Callback used to retrieve the value of the field from the item.
* Defaults to `item[ field.id ]`.
*/
getValue?: ( args: { item: T } ) => string | undefined;
getValue?: ( args: { item: Item } ) => string | undefined;

/**
* Callback used to render the field. Defaults to `field.getValue`.
*/
render?: ( args: { item: T } ) => ReactNode;
render?: ( args: { item: Item } ) => ReactNode;

/**
* The width of the field column.
Expand Down Expand Up @@ -108,15 +98,15 @@ export interface Field< T extends Item > {
filterBy?: FilterByConfig | undefined;
}

export type NormalizedField< T extends Item > = Field< T > &
Required< Pick< Field< T >, 'header' | 'getValue' | 'render' > >;
export type NormalizedField< Item extends AnyItem > = Field< Item > &
Required< Pick< Field< Item >, 'header' | 'getValue' | 'render' > >;

/**
* A collection of dataview fields for a data type.
*/
export type Fields< T extends Item > = Field< T >[];
export type Fields< Item extends AnyItem > = Field< Item >[];

export type Data = Item[];
export type Data< Item extends AnyItem > = Item[];

/**
* The filters applied to the dataset.
Expand All @@ -142,7 +132,7 @@ interface ViewBase {
/**
* The layout of the view.
*/
type: ViewType;
type: string;

/**
* The global search term.
Expand Down Expand Up @@ -203,7 +193,7 @@ export interface ViewList extends ViewBase {

export type View = ViewList | ViewBase;

interface ActionBase {
interface ActionBase< Item extends AnyItem > {
/**
* The unique identifier of the action.
*/
Expand Down Expand Up @@ -247,7 +237,8 @@ interface ActionBase {
supportsBulk?: boolean;
}

export interface ActionModal extends ActionBase {
export interface ActionModal< Item extends AnyItem >
extends ActionBase< Item > {
/**
* Modal to render when the action is triggered.
*/
Expand All @@ -274,11 +265,14 @@ export interface ActionModal extends ActionBase {
modalHeader?: string;
}

export interface ActionButton extends ActionBase {
export interface ActionButton< Item extends AnyItem >
extends ActionBase< AnyItem > {
/**
* The callback to execute when the action is triggered.
*/
callback: ( items: Item[] ) => void;
}

export type Action = ActionModal | ActionButton;
export type Action< Item extends AnyItem > =
| ActionModal< Item >
| ActionButton< Item >;
Loading

0 comments on commit d876c49

Please sign in to comment.