/** * External dependencies */ import clsx from 'clsx'; import type { RefObject } from 'react'; /** * WordPress dependencies */ import { Dropdown, FlexItem, SelectControl, Tooltip, Icon, } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { useMemo, useRef } from '@wordpress/element'; import { closeSmall } from '@wordpress/icons'; import { Stack } from '@wordpress/ui'; /** * Internal dependencies */ import SearchWidget from './search-widget'; import InputWidget from './input-widget'; import { getOperatorByName } from '../../utils/operators'; import type { Filter, NormalizedField, NormalizedFilter, Operator, Option, View, } from '../../types'; import useElements from '../../hooks/use-elements'; const ENTER = 'Enter'; const SPACE = ' '; interface FilterTextProps { activeElements: Option[]; filterInView?: Filter; filter: NormalizedFilter; } interface OperatorSelectorProps { filter: NormalizedFilter; view: View; onChangeView: ( view: View ) => void; } interface FilterProps extends OperatorSelectorProps { addFilterRef: RefObject< HTMLButtonElement | null >; openedFilter: string | null; fields: NormalizedField< any >[]; } const FilterText = ( { activeElements, filterInView, filter, }: FilterTextProps ) => { if ( activeElements === undefined || activeElements.length === 0 ) { return filter.name; } const operator = getOperatorByName( filterInView?.operator ); if ( operator !== undefined ) { return operator.filterText( filter, activeElements ); } return sprintf( /* translators: 1: Filter name e.g.: "Unknown status for Author". */ __( 'Unknown status for %1$s' ), filter.name ); }; function OperatorSelector( { filter, view, onChangeView, }: OperatorSelectorProps ) { const operatorOptions = filter.operators?.map( ( operator ) => ( { value: operator, label: getOperatorByName( operator )?.label || operator, } ) ); const currentFilter = view.filters?.find( ( _filter ) => _filter.field === filter.field ); const value = currentFilter?.operator || filter.operators[ 0 ]; return ( operatorOptions.length > 1 && ( { filter.name } { const newOperator = newValue as Operator; const currentOperator = currentFilter?.operator; const newFilters = currentFilter ? [ ...( view.filters ?? [] ).map( ( _filter ) => { if ( _filter.field === filter.field ) { const currentOpSelectionModel = getOperatorByName( currentOperator )?.selection; const newOpSelectionModel = getOperatorByName( newOperator )?.selection; const shouldResetValue = currentOpSelectionModel !== newOpSelectionModel || [ currentOpSelectionModel, newOpSelectionModel, ].includes( 'custom' ); return { ..._filter, value: shouldResetValue ? undefined : _filter.value, operator: newOperator, }; } return _filter; } ), ] : [ ...( view.filters ?? [] ), { field: filter.field, operator: newOperator, value: undefined, }, ]; onChangeView( { ...view, page: 1, filters: newFilters, } ); } } size="small" variant="minimal" hideLabelFromVision /> ) ); } export default function Filter( { addFilterRef, openedFilter, fields, ...commonProps }: FilterProps ) { const toggleRef = useRef< HTMLDivElement >( null ); const { filter, view, onChangeView } = commonProps; const filterInView = view.filters?.find( ( f ) => f.field === filter.field ); let activeElements: Option[] = []; const field = useMemo( () => { const currentField = fields.find( ( f ) => f.id === filter.field ); if ( currentField ) { return { ...currentField, // Configure getValue as if Item was a plain object. // See related input-widget.tsx getValue: ( { item }: { item: any } ) => item[ currentField.id ], }; } return currentField; }, [ fields, filter.field ] ); const { elements } = useElements( { elements: filter.elements, getElements: filter.getElements, } ); if ( elements.length > 0 ) { // When there are elements, we favor those activeElements = elements.filter( ( element ) => { if ( filter.singleSelection ) { return element.value === filterInView?.value; } return filterInView?.value?.includes( element.value ); } ); } else if ( Array.isArray( filterInView?.value ) ) { // or, filterInView.value can also be array // for the between operator, as in [ 1, 2 ] const label = filterInView.value.map( ( v ) => { const formattedValue = field?.getValueFormatted( { item: { [ field.id ]: v }, field, } ); return formattedValue || String( v ); } ); activeElements = [ { value: filterInView.value, // @ts-ignore label, }, ]; } else if ( typeof filterInView?.value === 'object' ) { // or, it can also be object for the inThePast/over operators, // as in { value: '1', units: 'days' } activeElements = [ { value: filterInView.value, label: filterInView.value }, ]; } else if ( filterInView?.value !== undefined ) { // otherwise, filterInView.value is a single value const label = field !== undefined ? field.getValueFormatted( { item: { [ field.id ]: filterInView.value }, field, } ) : String( filterInView.value ); activeElements = [ { value: filterInView.value, label, }, ]; } const isPrimary = filter.isPrimary; const isLocked = filterInView?.isLocked; const hasValues = ! isLocked && filterInView?.value !== undefined; const canResetOrRemove = ! isLocked && ( ! isPrimary || hasValues ); return ( { toggleRef.current?.focus(); } } renderToggle={ ( { isOpen, onToggle } ) => (
{ if ( ! isLocked ) { onToggle(); } } } onKeyDown={ ( event ) => { if ( ! isLocked && [ ENTER, SPACE ].includes( event.key ) ) { onToggle(); event.preventDefault(); } } } aria-disabled={ isLocked } aria-pressed={ isOpen } aria-expanded={ isOpen } ref={ toggleRef } >
{ canResetOrRemove && ( ) }
) } renderContent={ () => { return ( { commonProps.filter.hasElements ? ( ) : ( ) } ); } } /> ); }