diff --git a/packages/dataviews/src/add-filter.js b/packages/dataviews/src/add-filter.tsx similarity index 79% rename from packages/dataviews/src/add-filter.js rename to packages/dataviews/src/add-filter.tsx index 37c057201a616..b9fc6e9c504c8 100644 --- a/packages/dataviews/src/add-filter.js +++ b/packages/dataviews/src/add-filter.tsx @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import type { Ref } from 'react'; + /** * WordPress dependencies */ @@ -12,6 +17,7 @@ import { forwardRef } from '@wordpress/element'; * Internal dependencies */ import { unlock } from './lock-unlock'; +import type { NormalizedFilter, View } from './types'; const { DropdownMenuV2: DropdownMenu, @@ -19,7 +25,17 @@ const { DropdownMenuItemLabelV2: DropdownMenuItemLabel, } = unlock( componentsPrivateApis ); -function AddFilter( { filters, view, onChangeView, setOpenedFilter }, ref ) { +interface AddFilterProps { + filters: NormalizedFilter[]; + view: View; + onChangeView: ( view: View ) => void; + setOpenedFilter: ( filter: string | null ) => void; +} + +function AddFilter( + { filters, view, onChangeView, setOpenedFilter }: AddFilterProps, + ref: Ref< HTMLButtonElement > +) { if ( ! filters.length || filters.every( ( { isPrimary } ) => isPrimary ) ) { return null; } diff --git a/packages/dataviews/src/constants.ts b/packages/dataviews/src/constants.ts index b9ed8caff9e94..48753470280d0 100644 --- a/packages/dataviews/src/constants.ts +++ b/packages/dataviews/src/constants.ts @@ -3,6 +3,11 @@ */ import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import type { Operator } from './types'; + // Filter operators. export const OPERATOR_IS = 'is'; export const OPERATOR_IS_NOT = 'isNot'; @@ -19,7 +24,7 @@ export const ALL_OPERATORS = [ OPERATOR_IS_ALL, OPERATOR_IS_NOT_ALL, ]; -export const OPERATORS = { +export const OPERATORS: Record< Operator, { key: string; label: string } > = { [ OPERATOR_IS ]: { key: 'is-filter', label: __( 'Is' ), diff --git a/packages/dataviews/src/filter-summary.js b/packages/dataviews/src/filter-summary.tsx similarity index 89% rename from packages/dataviews/src/filter-summary.js rename to packages/dataviews/src/filter-summary.tsx index f8251c7d1e15e..da4b2e840a297 100644 --- a/packages/dataviews/src/filter-summary.js +++ b/packages/dataviews/src/filter-summary.tsx @@ -2,6 +2,7 @@ * External dependencies */ import clsx from 'clsx'; +import type { RefObject } from 'react'; /** * WordPress dependencies @@ -35,8 +36,30 @@ import { OPERATOR_IS_ALL, OPERATOR_IS_NOT_ALL, } from './constants'; +import type { Filter, NormalizedFilter, Operator, Option, View } from './types'; -const FilterText = ( { activeElements, filterInView, filter } ) => { +interface FilterTextProps { + activeElements: Option[]; + filterInView?: Filter; + filter: NormalizedFilter; +} + +interface OperatorSelectorProps { + filter: NormalizedFilter; + view: View; + onChangeView: ( view: View ) => void; +} + +interface FilterSummaryProps extends OperatorSelectorProps { + addFilterRef: RefObject< HTMLButtonElement >; + openedFilter: string | null; +} + +const FilterText = ( { + activeElements, + filterInView, + filter, +}: FilterTextProps ) => { if ( activeElements === undefined || activeElements.length === 0 ) { return filter.name; } @@ -125,7 +148,11 @@ const FilterText = ( { activeElements, filterInView, filter } ) => { ); }; -function OperatorSelector( { filter, view, onChangeView } ) { +function OperatorSelector( { + filter, + view, + onChangeView, +}: OperatorSelectorProps ) { const operatorOptions = filter.operators?.map( ( operator ) => ( { value: operator, label: OPERATORS[ operator ]?.label, @@ -150,13 +177,14 @@ function OperatorSelector( { filter, view, onChangeView } ) { value={ value } options={ operatorOptions } onChange={ ( newValue ) => { + const operator = newValue as Operator; const newFilters = currentFilter ? [ ...view.filters.map( ( _filter ) => { if ( _filter.field === filter.field ) { return { ..._filter, - operator: newValue, + operator, }; } return _filter; @@ -166,7 +194,8 @@ function OperatorSelector( { filter, view, onChangeView } ) { ...view.filters, { field: filter.field, - operator: newValue, + operator, + value: undefined, }, ]; onChangeView( { @@ -188,8 +217,8 @@ export default function FilterSummary( { addFilterRef, openedFilter, ...commonProps -} ) { - const toggleRef = useRef(); +}: FilterSummaryProps ) { + const toggleRef = useRef< HTMLDivElement >( null ); const { filter, view, onChangeView } = commonProps; const filterInView = view.filters.find( ( f ) => f.field === filter.field ); const activeElements = filter.elements.filter( ( element ) => { diff --git a/packages/dataviews/src/filters.js b/packages/dataviews/src/filters.tsx similarity index 82% rename from packages/dataviews/src/filters.js rename to packages/dataviews/src/filters.tsx index aae4d3871ab1e..df7a13b2953f1 100644 --- a/packages/dataviews/src/filters.js +++ b/packages/dataviews/src/filters.tsx @@ -2,6 +2,7 @@ * WordPress dependencies */ import { memo, useRef } from '@wordpress/element'; +import { __experimentalHStack as HStack } from '@wordpress/components'; /** * Internal dependencies @@ -11,17 +12,25 @@ import AddFilter from './add-filter'; import ResetFilters from './reset-filters'; import { sanitizeOperators } from './utils'; import { ALL_OPERATORS, OPERATOR_IS, OPERATOR_IS_NOT } from './constants'; -import { __experimentalHStack as HStack } from '@wordpress/components'; +import type { AnyItem, NormalizedField, NormalizedFilter, View } from './types'; + +interface FiltersProps< Item extends AnyItem > { + fields: NormalizedField< Item >[]; + view: View; + onChangeView: ( view: View ) => void; + openedFilter: string | null; + setOpenedFilter: ( openedFilter: string | null ) => void; +} -const Filters = memo( function Filters( { +const Filters = memo( function Filters< Item extends AnyItem >( { fields, view, onChangeView, openedFilter, setOpenedFilter, -} ) { - const addFilterRef = useRef(); - const filters = []; +}: FiltersProps< Item > ) { + const addFilterRef = useRef< HTMLButtonElement >( null ); + const filters: NormalizedFilter[] = []; fields.forEach( ( field ) => { if ( ! field.elements?.length ) { return; diff --git a/packages/dataviews/src/reset-filters.js b/packages/dataviews/src/reset-filters.tsx similarity index 66% rename from packages/dataviews/src/reset-filters.js rename to packages/dataviews/src/reset-filters.tsx index d7143599c443b..74570b49d9548 100644 --- a/packages/dataviews/src/reset-filters.js +++ b/packages/dataviews/src/reset-filters.tsx @@ -4,8 +4,23 @@ import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -export default function ResetFilter( { filters, view, onChangeView } ) { - const isPrimary = ( field ) => +/** + * Internal dependencies + */ +import type { NormalizedFilter, View } from './types'; + +interface ResetFilterProps { + filters: NormalizedFilter[]; + view: View; + onChangeView: ( view: View ) => void; +} + +export default function ResetFilter( { + filters, + view, + onChangeView, +}: ResetFilterProps ) { + const isPrimary = ( field: string ) => filters.some( ( _filter ) => _filter.field === field && _filter.isPrimary ); diff --git a/packages/dataviews/src/search-widget.js b/packages/dataviews/src/search-widget.tsx similarity index 91% rename from packages/dataviews/src/search-widget.js rename to packages/dataviews/src/search-widget.tsx index 080859b15ff8e..6a5f4b6644f67 100644 --- a/packages/dataviews/src/search-widget.js +++ b/packages/dataviews/src/search-widget.tsx @@ -22,6 +22,7 @@ import { SVG, Circle } from '@wordpress/primitives'; * Internal dependencies */ import { unlock } from './lock-unlock'; +import type { Filter, NormalizedFilter, View } from './types'; const { CompositeV2: Composite, @@ -29,6 +30,12 @@ const { useCompositeStoreV2: useCompositeStore, } = unlock( componentsPrivateApis ); +interface SearchWidgetProps { + view: View; + filter: NormalizedFilter; + onChangeView: ( view: View ) => void; +} + const radioCheck = ( @@ -39,8 +46,11 @@ function normalizeSearchInput( input = '' ) { return removeAccents( input.trim().toLowerCase() ); } -const EMPTY_ARRAY = []; -const getCurrentValue = ( filterDefinition, currentFilter ) => { +const EMPTY_ARRAY: [] = []; +const getCurrentValue = ( + filterDefinition: NormalizedFilter, + currentFilter?: Filter +) => { if ( filterDefinition.singleSelection ) { return currentFilter?.value; } @@ -56,7 +66,11 @@ const getCurrentValue = ( filterDefinition, currentFilter ) => { return EMPTY_ARRAY; }; -const getNewValue = ( filterDefinition, currentFilter, value ) => { +const getNewValue = ( + filterDefinition: NormalizedFilter, + currentFilter: Filter | undefined, + value: any +) => { if ( filterDefinition.singleSelection ) { return value; } @@ -70,7 +84,7 @@ const getNewValue = ( filterDefinition, currentFilter, value ) => { return [ value ]; }; -function ListBox( { view, filter, onChangeView } ) { +function ListBox( { view, filter, onChangeView }: SearchWidgetProps ) { const compositeStore = useCompositeStore( { virtualFocus: true, focusLoop: true, @@ -184,7 +198,7 @@ function ListBox( { view, filter, onChangeView } ) { ); } -function ComboboxList( { view, filter, onChangeView } ) { +function ComboboxList( { view, filter, onChangeView }: SearchWidgetProps ) { const [ searchValue, setSearchValue ] = useState( '' ); const deferredSearchValue = useDeferredValue( searchValue ); const currentFilter = view.filters.find( @@ -234,7 +248,13 @@ function ComboboxList( { view, filter, onChangeView } ) { setValue={ setSearchValue } >
- }> + + { __( 'Search items' ) } + + } + > { __( 'Search items' ) } 10 ? ComboboxList : ListBox; return ; } diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index e4bcb3440f585..58b0bffc92296 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -8,9 +8,10 @@ export type SortDirection = 'asc' | 'desc'; /** * Generic option type. */ -interface Option< Value extends any = any > { +export interface Option< Value extends any = any > { value: Value; label: string; + description?: string; } interface FilterByConfig { @@ -28,15 +29,13 @@ interface FilterByConfig { isPrimary?: boolean; } -type DeprecatedOperator = 'in' | 'notIn'; -type Operator = +export type Operator = | 'is' | 'isNot' | 'isAny' | 'isNone' | 'isAll' - | 'isNotAll' - | DeprecatedOperator; + | 'isNotAll'; export type AnyItem = Record< string, any >; @@ -136,6 +135,43 @@ export interface Filter { value: any; } +export interface NormalizedFilter { + /** + * The field to filter by. + */ + field: string; + + /** + * The field name. + */ + name: string; + + /** + * The list of options to pick from when using the field as a filter. + */ + elements: Option[]; + + /** + * Is a single selection filter. + */ + singleSelection: boolean; + + /** + * The list of operators supported by the field. + */ + operators: Operator[]; + + /** + * Whether the filter is visible. + */ + isVisible: boolean; + + /** + * Whether it is a primary filter. + */ + isPrimary: boolean; +} + interface ViewBase { /** * The layout of the view. diff --git a/packages/dataviews/src/utils.ts b/packages/dataviews/src/utils.ts index 8321b96b9d07b..d895289318da0 100644 --- a/packages/dataviews/src/utils.ts +++ b/packages/dataviews/src/utils.ts @@ -20,17 +20,6 @@ export function sanitizeOperators< Item extends AnyItem >( operators = [ OPERATOR_IS_ANY, OPERATOR_IS_NONE ]; } - // Transform legacy in, notIn operators to is, isNot. - // To be removed in the future. - if ( operators.includes( 'in' ) ) { - operators = operators.filter( ( operator ) => operator !== 'is' ); - operators.push( 'is' ); - } - if ( operators.includes( 'notIn' ) ) { - operators = operators.filter( ( operator ) => operator !== 'notIn' ); - operators.push( 'isNot' ); - } - // Make sure only valid operators are used. operators = operators.filter( ( operator ) => ALL_OPERATORS.includes( operator )