/** * External dependencies */ import { subDays, subWeeks, subMonths, subYears } from 'date-fns'; /** * WordPress dependencies */ import { __, sprintf } from '@wordpress/i18n'; import { createInterpolateElement } from '@wordpress/element'; import { getDate } from '@wordpress/date'; import type { ReactElement } from 'react'; /** * Internal dependencies */ import type { FilterOperator, NormalizedFilter, Operator, Option, } from '../types'; import { OPERATOR_AFTER, OPERATOR_AFTER_INC, OPERATOR_BEFORE, OPERATOR_BEFORE_INC, OPERATOR_BETWEEN, OPERATOR_CONTAINS, OPERATOR_GREATER_THAN, OPERATOR_GREATER_THAN_OR_EQUAL, OPERATOR_IN_THE_PAST, OPERATOR_IS, OPERATOR_IS_ALL, OPERATOR_IS_ANY, OPERATOR_IS_NONE, OPERATOR_IS_NOT, OPERATOR_IS_NOT_ALL, OPERATOR_LESS_THAN, OPERATOR_LESS_THAN_OR_EQUAL, OPERATOR_NOT_CONTAINS, OPERATOR_NOT_ON, OPERATOR_ON, OPERATOR_OVER, OPERATOR_STARTS_WITH, } from '../constants'; const filterTextWrappers = { Name: , Value: , }; /** * Calculates a date offset from now. * * @param value Number of units to offset. * @param unit Unit of time to offset (days, weeks, months, years). * @return Date offset from now. */ function getRelativeDate( value: number, unit: string ): Date { switch ( unit ) { case 'days': return subDays( new Date(), value ); case 'weeks': return subWeeks( new Date(), value ); case 'months': return subMonths( new Date(), value ); case 'years': return subYears( new Date(), value ); default: return new Date(); } } // Shared operator definition for IS_NONE and IS_NOT_ALL (deprecated). const isNoneOperatorDefinition = { /* translators: DataViews operator name */ label: __( 'Is none of' ), filterText: ( filter: NormalizedFilter, activeElements: Option[] ) => createInterpolateElement( sprintf( /* translators: 1: Filter name (e.g. "Author"). 2: Filter value (e.g. "Admin"): "Author is none of: Admin, Editor". */ __( '%1$s is none of: %2$s' ), filter.name, activeElements.map( ( element ) => element.label ).join( ', ' ) ), filterTextWrappers ), filter: ( ( item, field, filterValue ) => { if ( ! filterValue?.length ) { return true; } const fieldValue = field.getValue( { item } ); if ( Array.isArray( fieldValue ) ) { return ! filterValue.some( ( fv: any ) => fieldValue.includes( fv ) ); } else if ( typeof fieldValue === 'string' ) { return ! filterValue.includes( fieldValue ); } return false; } ) as FilterOperator< any >, selection: 'multi' as const, }; const OPERATORS: { name: Operator; label: string; filterText: ( filter: NormalizedFilter, activeElements: Option[] ) => ReactElement; filter?: FilterOperator< any >; selection: 'single' | 'multi' | 'custom'; }[] = [ { name: OPERATOR_IS_ANY, /* translators: DataViews operator name */ label: __( 'Includes' ), filterText: ( filter: NormalizedFilter, activeElements: Option[] ) => createInterpolateElement( sprintf( /* translators: 1: Filter name (e.g. "Author"). 2: Filter value (e.g. "Admin"): "Author is any: Admin, Editor". */ __( '%1$s includes: %2$s' ), filter.name, activeElements .map( ( element ) => element.label ) .join( ', ' ) ), filterTextWrappers ), filter( item, field, filterValue ) { if ( ! filterValue?.length ) { return true; } const fieldValue = field.getValue( { item } ); if ( Array.isArray( fieldValue ) ) { return filterValue.some( ( fv: any ) => fieldValue.includes( fv ) ); } else if ( typeof fieldValue === 'string' ) { return filterValue.includes( fieldValue ); } return false; }, selection: 'multi', }, { name: OPERATOR_IS_NONE, ...isNoneOperatorDefinition, }, { name: OPERATOR_IS_ALL, /* translators: DataViews operator name */ label: __( 'Includes all' ), filterText: ( filter: NormalizedFilter, activeElements: Option[] ) => createInterpolateElement( sprintf( /* translators: 1: Filter name (e.g. "Author"). 2: Filter value (e.g. "Admin"): "Author includes all: Admin, Editor". */ __( '%1$s includes all: %2$s' ), filter.name, activeElements .map( ( element ) => element.label ) .join( ', ' ) ), filterTextWrappers ), filter( item, field, filterValue ) { if ( ! filterValue?.length ) { return true; } return filterValue.every( ( value: any ) => { return field.getValue( { item } )?.includes( value ); } ); }, selection: 'multi', }, { name: OPERATOR_IS_NOT_ALL, ...isNoneOperatorDefinition, }, { name: OPERATOR_BETWEEN, /* translators: DataViews operator name */ label: __( 'Between (inc)' ), filterText: ( filter: NormalizedFilter, activeElements: Option[] ) => createInterpolateElement( sprintf( /* translators: 1: Filter name (e.g. "Item count"). 2: Filter value min. 3: Filter value max. e.g.: "Item count between (inc): 10 and 180". */ __( '%1$s between (inc): %2$s and %3$s' ), filter.name, activeElements[ 0 ].label[ 0 ], activeElements[ 0 ].label[ 1 ] ), filterTextWrappers ), filter( item, field, filterValue ) { if ( ! Array.isArray( filterValue ) || filterValue.length !== 2 || filterValue[ 0 ] === undefined || filterValue[ 1 ] === undefined ) { return true; } const fieldValue = field.getValue( { item } ); if ( typeof fieldValue === 'number' || fieldValue instanceof Date || typeof fieldValue === 'string' ) { return ( fieldValue >= filterValue[ 0 ] && fieldValue <= filterValue[ 1 ] ); } return false; }, selection: 'custom', }, { name: OPERATOR_IN_THE_PAST, /* translators: DataViews operator name */ label: __( 'In the past' ), filterText: ( filter: NormalizedFilter, activeElements: Option[] ) => createInterpolateElement( sprintf( /* translators: 1: Filter name (e.g. "Date"). 2: Filter value (e.g. "7 days"): "Date is in the past: 7 days". */ __( '%1$s is in the past: %2$s' ), filter.name, `${ activeElements[ 0 ].value.value } ${ activeElements[ 0 ].value.unit }` ), filterTextWrappers ), filter( item, field, filterValue ) { if ( filterValue?.value === undefined || filterValue?.unit === undefined ) { return true; } const targetDate = getRelativeDate( filterValue.value, filterValue.unit ); const fieldValue = getDate( field.getValue( { item } ) ); return fieldValue >= targetDate && fieldValue <= new Date(); }, selection: 'custom', }, { name: OPERATOR_OVER, /* translators: DataViews operator name */ label: __( 'Over' ), filterText: ( filter: NormalizedFilter, activeElements: Option[] ) => createInterpolateElement( sprintf( /* translators: 1: Filter name (e.g. "Date"). 2: Filter value (e.g. "7 days"): "Date is over: 7 days". */ __( '%1$s is over: %2$s' ), filter.name, `${ activeElements[ 0 ].value.value } ${ activeElements[ 0 ].value.unit }` ), filterTextWrappers ), filter( item, field, filterValue ) { if ( filterValue?.value === undefined || filterValue?.unit === undefined ) { return true; } const targetDate = getRelativeDate( filterValue.value, filterValue.unit ); const fieldValue = getDate( field.getValue( { item } ) ); return fieldValue < targetDate; }, selection: 'custom', }, { name: OPERATOR_IS, /* translators: DataViews operator name */ label: __( 'Is' ), filterText: ( filter: NormalizedFilter, activeElements: Option[] ) => createInterpolateElement( sprintf( /* translators: 1: Filter name (e.g. "Author"). 2: Filter value (e.g. "Admin"): "Author is: Admin". */ __( '%1$s is: %2$s' ), filter.name, activeElements[ 0 ].label ), filterTextWrappers ), filter( item, field, filterValue ) { return ( filterValue === field.getValue( { item } ) || filterValue === undefined ); }, selection: 'single', }, { name: OPERATOR_IS_NOT, /* translators: DataViews operator name */ label: __( 'Is not' ), filterText: ( filter: NormalizedFilter, activeElements: Option[] ) => createInterpolateElement( sprintf( /* translators: 1: Filter name (e.g. "Author"). 2: Filter value (e.g. "Admin"): "Author is not: Admin". */ __( '%1$s is not: %2$s' ), filter.name, activeElements[ 0 ].label ), filterTextWrappers ), filter( item, field, filterValue ) { return filterValue !== field.getValue( { item } ); }, selection: 'single', }, { name: OPERATOR_LESS_THAN, /* translators: DataViews operator name */ label: __( 'Less than' ), filterText: ( filter: NormalizedFilter, activeElements: Option[] ) => createInterpolateElement( sprintf( /* translators: 1: Filter name (e.g. "Count"). 2: Filter value (e.g. "10"): "Count is less than: 10". */ __( '%1$s is less than: %2$s' ), filter.name, activeElements[ 0 ].label ), filterTextWrappers ), filter( item, field, filterValue ) { if ( filterValue === undefined ) { return true; } const fieldValue = field.getValue( { item } ); return fieldValue < filterValue; }, selection: 'single', }, { name: OPERATOR_GREATER_THAN, /* translators: DataViews operator name */ label: __( 'Greater than' ), filterText: ( filter: NormalizedFilter, activeElements: Option[] ) => createInterpolateElement( sprintf( /* translators: 1: Filter name (e.g. "Count"). 2: Filter value (e.g. "10"): "Count is greater than: 10". */ __( '%1$s is greater than: %2$s' ), filter.name, activeElements[ 0 ].label ), filterTextWrappers ), filter( item, field, filterValue ) { if ( filterValue === undefined ) { return true; } const fieldValue = field.getValue( { item } ); return fieldValue > filterValue; }, selection: 'single', }, { name: OPERATOR_LESS_THAN_OR_EQUAL, /* translators: DataViews operator name */ label: __( 'Less than or equal' ), filterText: ( filter: NormalizedFilter, activeElements: Option[] ) => createInterpolateElement( sprintf( /* translators: 1: Filter name (e.g. "Count"). 2: Filter value (e.g. "10"): "Count is less than or equal to: 10". */ __( '%1$s is less than or equal to: %2$s' ), filter.name, activeElements[ 0 ].label ), filterTextWrappers ), filter( item, field, filterValue ) { if ( filterValue === undefined ) { return true; } const fieldValue = field.getValue( { item } ); return fieldValue <= filterValue; }, selection: 'single', }, { name: OPERATOR_GREATER_THAN_OR_EQUAL, /* translators: DataViews operator name */ label: __( 'Greater than or equal' ), filterText: ( filter: NormalizedFilter, activeElements: Option[] ) => createInterpolateElement( sprintf( /* translators: 1: Filter name (e.g. "Count"). 2: Filter value (e.g. "10"): "Count is greater than or equal to: 10". */ __( '%1$s is greater than or equal to: %2$s' ), filter.name, activeElements[ 0 ].label ), filterTextWrappers ), filter( item, field, filterValue ) { if ( filterValue === undefined ) { return true; } const fieldValue = field.getValue( { item } ); return fieldValue >= filterValue; }, selection: 'single', }, { name: OPERATOR_BEFORE, /* translators: DataViews operator name */ label: __( 'Before' ), filterText: ( filter: NormalizedFilter, activeElements: Option[] ) => createInterpolateElement( sprintf( /* translators: 1: Filter name (e.g. "Date"). 2: Filter value (e.g. "2024-01-01"): "Date is before: 2024-01-01". */ __( '%1$s is before: %2$s' ), filter.name, activeElements[ 0 ].label ), filterTextWrappers ), filter( item, field, filterValue ) { if ( filterValue === undefined ) { return true; } const filterDate = getDate( filterValue ); const fieldDate = getDate( field.getValue( { item } ) ); return fieldDate < filterDate; }, selection: 'single', }, { name: OPERATOR_AFTER, /* translators: DataViews operator name */ label: __( 'After' ), filterText: ( filter: NormalizedFilter, activeElements: Option[] ) => createInterpolateElement( sprintf( /* translators: 1: Filter name (e.g. "Date"). 2: Filter value (e.g. "2024-01-01"): "Date is after: 2024-01-01". */ __( '%1$s is after: %2$s' ), filter.name, activeElements[ 0 ].label ), filterTextWrappers ), filter( item, field, filterValue ) { if ( filterValue === undefined ) { return true; } const filterDate = getDate( filterValue ); const fieldDate = getDate( field.getValue( { item } ) ); return fieldDate > filterDate; }, selection: 'single', }, { name: OPERATOR_BEFORE_INC, /* translators: DataViews operator name */ label: __( 'Before (inc)' ), filterText: ( filter: NormalizedFilter, activeElements: Option[] ) => createInterpolateElement( sprintf( /* translators: 1: Filter name (e.g. "Date"). 2: Filter value (e.g. "2024-01-01"): "Date is on or before: 2024-01-01". */ __( '%1$s is on or before: %2$s' ), filter.name, activeElements[ 0 ].label ), filterTextWrappers ), filter( item, field, filterValue ) { if ( filterValue === undefined ) { return true; } const filterDate = getDate( filterValue ); const fieldDate = getDate( field.getValue( { item } ) ); return fieldDate <= filterDate; }, selection: 'single', }, { name: OPERATOR_AFTER_INC, /* translators: DataViews operator name */ label: __( 'After (inc)' ), filterText: ( filter: NormalizedFilter, activeElements: Option[] ) => createInterpolateElement( sprintf( /* translators: 1: Filter name (e.g. "Date"). 2: Filter value (e.g. "2024-01-01"): "Date is on or after: 2024-01-01". */ __( '%1$s is on or after: %2$s' ), filter.name, activeElements[ 0 ].label ), filterTextWrappers ), filter( item, field, filterValue ) { if ( filterValue === undefined ) { return true; } const filterDate = getDate( filterValue ); const fieldDate = getDate( field.getValue( { item } ) ); return fieldDate >= filterDate; }, selection: 'single', }, { name: OPERATOR_CONTAINS, /* translators: DataViews operator name */ label: __( 'Contains' ), filterText: ( filter: NormalizedFilter, activeElements: Option[] ) => createInterpolateElement( sprintf( /* translators: 1: Filter name (e.g. "Title"). 2: Filter value (e.g. "Hello"): "Title contains: Hello". */ __( '%1$s contains: %2$s' ), filter.name, activeElements[ 0 ].label ), filterTextWrappers ), filter( item, field, filterValue ) { if ( filterValue === undefined ) { return true; } const fieldValue = field.getValue( { item } ); return ( typeof fieldValue === 'string' && filterValue && fieldValue .toLowerCase() .includes( String( filterValue ).toLowerCase() ) ); }, selection: 'single', }, { name: OPERATOR_NOT_CONTAINS, /* translators: DataViews operator name */ label: __( "Doesn't contain" ), filterText: ( filter: NormalizedFilter, activeElements: Option[] ) => createInterpolateElement( sprintf( /* translators: 1: Filter name (e.g. "Title"). 2: Filter value (e.g. "Hello"): "Title doesn't contain: Hello". */ __( "%1$s doesn't contain: %2$s" ), filter.name, activeElements[ 0 ].label ), filterTextWrappers ), filter( item, field, filterValue ) { if ( filterValue === undefined ) { return true; } const fieldValue = field.getValue( { item } ); return ( typeof fieldValue === 'string' && filterValue && ! fieldValue .toLowerCase() .includes( String( filterValue ).toLowerCase() ) ); }, selection: 'single', }, { name: OPERATOR_STARTS_WITH, /* translators: DataViews operator name */ label: __( 'Starts with' ), filterText: ( filter: NormalizedFilter, activeElements: Option[] ) => createInterpolateElement( sprintf( /* translators: 1: Filter name (e.g. "Title"). 2: Filter value (e.g. "Hello"): "Title starts with: Hello". */ __( '%1$s starts with: %2$s' ), filter.name, activeElements[ 0 ].label ), filterTextWrappers ), filter( item, field, filterValue ) { if ( filterValue === undefined ) { return true; } const fieldValue = field.getValue( { item } ); return ( typeof fieldValue === 'string' && filterValue && fieldValue .toLowerCase() .startsWith( String( filterValue ).toLowerCase() ) ); }, selection: 'single', }, { name: OPERATOR_ON, /* translators: DataViews operator name */ label: __( 'On' ), filterText: ( filter: NormalizedFilter, activeElements: Option[] ) => createInterpolateElement( sprintf( /* translators: 1: Filter name (e.g. "Date"). 2: Filter value (e.g. "2024-01-01"): "Date is: 2024-01-01". */ __( '%1$s is: %2$s' ), filter.name, activeElements[ 0 ].label ), filterTextWrappers ), filter( item, field, filterValue ) { if ( filterValue === undefined ) { return true; } const filterDate = getDate( filterValue ); const fieldDate = getDate( field.getValue( { item } ) ); return filterDate.getTime() === fieldDate.getTime(); }, selection: 'single', }, { name: OPERATOR_NOT_ON, /* translators: DataViews operator name */ label: __( 'Not on' ), filterText: ( filter: NormalizedFilter, activeElements: Option[] ) => createInterpolateElement( sprintf( /* translators: 1: Filter name (e.g. "Date"). 2: Filter value (e.g. "2024-01-01"): "Date is not: 2024-01-01". */ __( '%1$s is not: %2$s' ), filter.name, activeElements[ 0 ].label ), filterTextWrappers ), filter( item, field, filterValue ) { if ( filterValue === undefined ) { return true; } const filterDate = getDate( filterValue ); const fieldDate = getDate( field.getValue( { item } ) ); return filterDate.getTime() !== fieldDate.getTime(); }, selection: 'single', }, ]; const getOperatorByName = ( name: string | undefined ) => OPERATORS.find( ( op ) => op.name === name ); const getAllOperatorNames = () => OPERATORS.map( ( op ) => op.name ); const isSingleSelectionOperator = ( name: string ) => OPERATORS.filter( ( op ) => op.selection === 'single' ).some( ( op ) => op.name === name ); const isRegisteredOperator = ( name: string ) => OPERATORS.some( ( op ) => op.name === name ); export { getOperatorByName, getAllOperatorNames, isSingleSelectionOperator, isRegisteredOperator, };