import React from 'react'; import { dateRangeFilter, idNameArrayFilter, numberRangeFilter, stringOperatorFilter, } from '@wix/bex-core'; import type { Field, Filter, FilterStateBaseParams, FilterOperator, PatternsFieldType, } from '@wix/bex-core'; import type { ToolbarCollectionState } from './ToolbarCollectionState'; import type { FilterElement } from '../components/CollectionToolbarFilters'; import { toFieldType } from '../utils/fieldTypePrefixIcons'; import { CheckboxCustomFieldFilter } from '../components/CustomFieldFilters/CheckboxCustomFieldFilter'; import { DateCustomFieldFilter } from '../components/CustomFieldFilters/DateCustomFieldFilter'; import { DateTimeCustomFieldFilter } from '../components/CustomFieldFilters/DateTimeCustomFieldFilter'; import { OperatorFilterCustomFieldFilter } from '../components/CustomFieldFilters/OperatorFilterCustomFieldFilter'; import { NumberRangeFilter } from '../components/NumberRangeFilter'; type FilterFactory = ( params: Partial>, ) => Filter; /** * The filter behavior a source supplies that the shared builder can't derive * from the neutral field type. The neutral type collapses distinct source types * (e.g. a DataExtension dropdown into SHORT_TEXT), so the source fills the gaps. */ export interface FieldFilterConfig { /** Operators the source's backend can run on text fields. */ textOperators: FilterOperator[]; /** Whether a NUMBER field's filter accepts decimals. Default: false. */ numberIsDecimal?: (field: Field) => boolean; /** Filter type icon. Default: derived from the neutral field type. */ iconType?: (field: Field) => ReturnType; } // Field type → the filter element rendered in the toolbar. Its keys also define // which field types get an auto filter (see isFilterableFieldType). const FILTER_COMPONENT_BY_TYPE: Partial< Record> > = { SHORT_TEXT: OperatorFilterCustomFieldFilter, LONG_TEXT: OperatorFilterCustomFieldFilter, URL: OperatorFilterCustomFieldFilter, NUMBER: NumberRangeFilter, BOOLEAN: CheckboxCustomFieldFilter, DATE: DateCustomFieldFilter, DATETIME: DateTimeCustomFieldFilter, }; // Field type → the filter state registered on the collection query. Text fields // use the source's allowed operators; the rest are fixed. const filterFactoryByType = ( config: FieldFilterConfig, ): Partial> => { const stringFilter: FilterFactory = (params) => stringOperatorFilter({ ...params, allowedOperators: config.textOperators }); return { SHORT_TEXT: stringFilter, LONG_TEXT: stringFilter, URL: stringFilter, NUMBER: numberRangeFilter, BOOLEAN: idNameArrayFilter, DATE: dateRangeFilter, DATETIME: dateRangeFilter, }; }; /** A field gets an auto filter when its type maps to a filter element. */ export const isFilterableFieldType = (type: PatternsFieldType): boolean => type in FILTER_COMPONENT_BY_TYPE; /** * Registers a filter state per filterable field on the collection query, so the * built elements have a filter to read. Idempotent — addFilterIfNeeded skips a * field that is already registered. */ export const registerFieldFilters = ( toolbar: ToolbarCollectionState, fields: Field[], config: FieldFilterConfig, ): void => { const factoryByType = filterFactoryByType(config); fields.forEach((field) => { const factory = factoryByType[field.type]; if (!factory) { return; } toolbar.collection.query.addFilterIfNeeded( field.id as any, factory({ name: field.id, isCustomField: true }), ); }); }; /** Builds the toolbar filter element for a field, or null when it has none. */ const buildFieldFilterElement = ( toolbar: ToolbarCollectionState, field: Field, config: FieldFilterConfig, ): FilterElement | null => { const FilterComponent = FILTER_COMPONENT_BY_TYPE[field.type]; const filter = toolbar.collection.filters[field.id]; if (!FilterComponent || !filter) { return null; } const label = field.displayName || field.id; const decimal = field.type === 'NUMBER' ? config.numberIsDecimal?.(field) : false; return ( ) as FilterElement; }; /** Builds the toolbar filter elements for a source's filterable fields. */ export const buildFieldFilters = ( toolbar: ToolbarCollectionState, fields: Field[], config: FieldFilterConfig, ): FilterElement[] => fields .map((field) => buildFieldFilterElement(toolbar, field, config)) .filter((el): el is FilterElement => el !== null);