import React, { useCallback, useEffect, useRef, useState } from 'react' import moment from 'moment' import { c } from '../../translations/LibraryTranslationService' import styles from './_filter.module.scss' import DatePicker from '../DatePicker/DatePicker' import { hasValue } from '../../services/HelperServiceTyped' import SearchBar from '../SearchBar/SearchBar' import SelectDisplay from '../Selects/SelectDisplay/SelectDisplay' import { type ExposedFilterNameType, type ExposedFilterProps, } from './ExposedFilter.models' import RangeFilter from '../RangeFilter/RangeFilter' import MultiSelect, { type MultiSelectDataItemBase, } from '../MultiSelect/MultiSelect' import { CheckboxState } from '../Form/NestedCheckboxHelper' import ExposedFilterShell from './ExposedFilterShell' import ExposedFilterPopover from './ExposedFilterPopover' import { useTagLayout } from './useTagLayout' import Modal from '../Modal/Modal' // Type guard for checking if an option matches the FilterType interface function isFilterType< T extends MultiSelectDataItemBase & ExposedFilterNameType, >(option: unknown): option is T { return ( typeof option === 'object' && option !== null && 'name' in option && typeof (option as Record).name === 'string' ) } // Type guard for modal props shape function hasSelectedOption( value: unknown, ): value is { selectedOption?: unknown } { return ( typeof value === 'object' && value !== null && 'selectedOption' in value ) } const ExposedFilter = < FilterType extends MultiSelectDataItemBase & ExposedFilterNameType, >( filterValues: ExposedFilterProps, ): React.JSX.Element => { const { labelName, type, position, disableCloseOnScroll, onChangeCallout, stateName, isClearFilter, setIsClearFilter, disabled, disabledTooltip, placeholder, } = filterValues const containerRef = useRef(null) const [selectSearchValue, setSelectSearchValue] = useState('') const exposedFilterDisabledProps = disabled === true ? { disabled: true as const, disabledTooltip } : { disabled: false as const } // Clear filter when clear all is clicked useEffect(() => { if (isClearFilter) { if (type === 'date') onChangeCallout?.(stateName, undefined) if (type === 'search') onChangeCallout?.(stateName, '') setIsClearFilter?.(false) } }, [isClearFilter, setIsClearFilter, onChangeCallout, stateName, type]) // Precompute flags const isMultiSelect = type === 'multi-select' const isModal = type === 'modal' // Precompute Multi-select derived values for hooks const multiSelectProps = isMultiSelect ? filterValues.multiSelectProps : undefined const multiSelectSelectedOptions = multiSelectProps?.selectedOptions const multiSelectSelectedLabels = isMultiSelect ? (multiSelectSelectedOptions?.map((o) => o.name) ?? []) : [] const { selectedTags: multiSelectSelectedTags, hasMore: multiSelectHasMore, truncateTagIndex: multiSelectTruncateTagIndex, truncatedTagWidth: multiSelectTruncatedTagWidth, } = useTagLayout(labelName, multiSelectSelectedLabels) const multiSelectNestedConfig = multiSelectProps?.nestedConfig const multiSelectItemStates = multiSelectNestedConfig?.itemStates const multiSelectSetItemStates = multiSelectNestedConfig?.setItemStates const resetFilters = useCallback(() => { if (!isMultiSelect) return if (multiSelectItemStates && multiSelectSetItemStates) { const resetItemStates = multiSelectItemStates.map((item) => ({ ...item, state: CheckboxState.UNCHECKED, })) multiSelectSetItemStates(resetItemStates) } onChangeCallout?.(stateName, []) }, [ isMultiSelect, multiSelectItemStates, multiSelectSetItemStates, onChangeCallout, stateName, ]) // Precompute Modal derived values for hooks const modalSelectedOption = isModal && hasSelectedOption(filterValues) ? filterValues.selectedOption : undefined const modalSelectedLabels = Array.isArray(modalSelectedOption) ? modalSelectedOption.map((v) => String(v)) : [] const { selectedTags: modalSelectedTags, hasMore: modalHasMore, truncateTagIndex: modalTruncateTagIndex, truncatedTagWidth: modalTruncatedTagWidth, } = useTagLayout(labelName, modalSelectedLabels) if (type === 'multi-select') { const { multiSelectProps } = filterValues const multiSelectBaseProps = { selectedOptions: multiSelectProps.selectedOptions, options: multiSelectProps.options, labelKey: multiSelectProps.labelKey, callout: (selectedOptions: FilterType[]) => onChangeCallout?.(stateName, selectedOptions), formLabelProps: multiSelectProps.formLabelProps, loading: multiSelectProps.loading ?? false, searchBarProps: { ...multiSelectProps.searchBarProps, autoFocus: true, }, emptyStateProps: multiSelectProps.emptyStateProps, isReorderable: multiSelectProps.isReorderable, disabled: multiSelectProps.disabled, hideSelectAll: multiSelectProps.hideSelectAll, sortAlphabetically: multiSelectProps.sortAlphabetically, exposed: true, removeBorder: true, } const multiSelectSharedProps = multiSelectProps.fetchData ? { ...multiSelectBaseProps, fetchData: multiSelectProps.fetchData, hasMore: multiSelectProps.hasMore ?? false, isFetching: multiSelectProps.isFetching, dropdownState: multiSelectProps.dropdownState, } : multiSelectBaseProps return ( ) : ( ) } > {({ visible, setVisible }) => ( 0} tagView={{ tags: multiSelectSelectedTags, totalCount: multiSelectProps.selectedOptions?.length ?? 0, hasMore: multiSelectHasMore, truncateTagIndex: multiSelectTruncateTagIndex, truncatedTagWidth: multiSelectTruncatedTagWidth, }} onMainClick={() => setVisible(!visible)} onClear={ (multiSelectProps.selectedOptions?.length ?? 0) > 0 ? resetFilters : undefined } /> )} ) } else if (type === 'date') { const { dateProps } = filterValues return ( onChangeCallout?.(stateName, value)} showDateValue={true} isExposed={true} /> } > {({ visible, setVisible }) => ( setVisible(!visible)} onClear={ dateProps.selectedDate || dateProps.selectedDateRange ? () => onChangeCallout?.(stateName, undefined) : undefined } /> )} ) } else if (type === 'select') { const { selectProps: { maxHeight, options, selectedOption, noOptionsMessage }, } = filterValues const filteredOptions = options .filter((option) => 'name' in option ? option.name.toLowerCase().includes(selectSearchValue.toLowerCase()) : true, ) .map((option) => ({ ...option, label: 'name' in option ? option.name : '', name: 'name' in option ? option.name : '', })) return ( (
{options?.length > 10 ? (
) : null} {filteredOptions?.length > 0 ? ( { if (isFilterType(option)) { onChangeCallout?.(stateName, option) setVisible(false) } }} /> ) : (
{noOptionsMessage ?? c('noResultsFound')}
)}
)} > {({ visible, setVisible }) => ( setVisible(!visible)} onClear={ selectedOption ? () => onChangeCallout?.(stateName, undefined) : undefined } /> )}
) } else if (type === 'search') { const { searchBarProps } = filterValues return ( onChangeCallout?.(stateName, value)} /> ) } else if (type === 'range') { const { rangeProps } = filterValues return ( onChangeCallout?.(stateName, rangeValues) } /> } > {({ visible, setVisible }) => ( setVisible(!visible)} onClear={ hasValue(rangeProps.selectedValues.min) || hasValue(rangeProps.selectedValues.max) ? () => onChangeCallout?.(stateName, { min: undefined, max: undefined, }) : undefined } /> )} ) } else if (type === 'modal') { const { selectedOption, modalProps } = filterValues return ( <> 0 } tagView={{ tags: modalSelectedTags, totalCount: Array.isArray(selectedOption) ? selectedOption.length : 0, hasMore: modalHasMore, truncateTagIndex: modalTruncateTagIndex, truncatedTagWidth: modalTruncatedTagWidth, }} onMainClick={() => filterValues.openCallout?.()} onClear={ (Array.isArray(selectedOption) ? selectedOption.length : 0) > 0 ? () => onChangeCallout?.(stateName, undefined) : undefined } /> {modalProps?.children} ) } else { return <> } } export default ExposedFilter