import React, { useContext, useEffect, useMemo, useReducer, useRef } from 'react' import { type FilterAttributes, type FilterState, type Filters, type FiltersValues } from '../types' import { filterBarStateReducer } from './reducer/filterBarStateReducer' import { setupFilterBarState } from './reducer/setupFilterBarState' import { type ActiveFiltersArray } from './types' import { checkShouldUpdateValues } from './utils/checkShouldUpdateValues' import { createFiltersHash } from './utils/createFiltersHash' import { getInactiveFilters } from './utils/getInactiveFilters' import { getMappedFilters } from './utils/getMappedFilters' import { getValidValue } from './utils/getValidValue' export type FilterBarContextValue< Value, ValuesMap extends FiltersValues = Record, > = { getFilterState: ( id: Id, ) => FilterState isClearable: boolean getActiveFilterValues: () => Partial setFilterOpenState: (id: Id, isOpen: boolean) => void openFilter: (id: Id) => void updateValue: (id: Id, value: ValuesMap[Id]) => void showFilter: (id: Id) => void hideFilter: (id: Id) => void getInactiveFilters: () => FilterAttributes[] clearAllFilters: () => void setFocus: (id: Id | undefined) => void focusId?: keyof ValuesMap } const FilterBarContext = React.createContext | null>(null) export const useFilterBarContext = < Value, Values extends FiltersValues = Record, >(): FilterBarContextValue => { const context = useContext(FilterBarContext) if (!context) { throw new Error('useFilterBarContext must be used within the FilterBarContext.Provider') } return context as FilterBarContextValue } export type FilterBarProviderProps = { children: (activeFilters: ActiveFiltersArray) => JSX.Element filters: Filters values: Partial onValuesChange: (values: Partial) => void } export const FilterBarProvider = ({ children, filters, values, onValuesChange, }: FilterBarProviderProps): JSX.Element => { const filtersHash = useRef(createFiltersHash(filters)) // As `filters` contains components, to prevent unnecessary re-renders, we check only for changes to id and name // eslint-disable-next-line react-hooks/exhaustive-deps const mappedFilters = useMemo(() => getMappedFilters(filters), [filtersHash.current]) const [state, dispatch] = useReducer( filterBarStateReducer, setupFilterBarState(filters, values), ) const activeFilters = Array.from(state.activeFilterIds, (id) => mappedFilters[id]) // Workaround for DateRangePicker populating the values object before the value is valid // (it purposefully persists a state with a 'from' date but no 'to' date, but hides it on the filter button) const isDraftDateRange = (v: ValuesMap): boolean => v && v.from !== undefined && v.to === undefined const hasDraftDateRangeOnly = Object.values(values).every(isDraftDateRange) const isClearable = (Object.keys(values).length > 0 && !hasDraftDateRangeOnly) || (state.hasRemovableFilter && activeFilters.some((f) => f.isRemovable)) const value = { getFilterState: (id: Id) => ({ ...state.filters[id], isActive: state.activeFilterIds.has(id), value: values[id], }), isClearable, getActiveFilterValues: () => values, setFilterOpenState: (id: Id, isOpen: boolean): void => { dispatch({ type: 'update_single_filter', id, data: { isOpen } }) }, openFilter: (id: Id): void => { dispatch({ type: 'update_single_filter', id, data: { isOpen: true } }) }, updateValue: (id: Id, newValue: ValuesMap[Id]): void => { dispatch({ type: 'update_values', values: { ...values, [id]: getValidValue(newValue) }, }) }, showFilter: (id: Id): void => { dispatch({ type: 'activate_filter', id }) dispatch({ type: 'set_focus', id }) }, hideFilter: (id: Id): void => { dispatch({ type: 'deactivate_filter', id }) dispatch({ type: 'set_focus', id: 'add_filter' }) }, getInactiveFilters: () => getInactiveFilters(state), clearAllFilters: () => { state.activeFilterIds.forEach((id) => { if (mappedFilters[id].isRemovable) dispatch({ type: 'deactivate_filter', id }) }) dispatch({ type: 'update_values', values: {} }) }, setFocus: (id: Id | undefined) => { dispatch({ type: 'set_focus', id }) }, focusId: state.focusId, } satisfies FilterBarContextValue useEffect(() => { const shouldUpdate = checkShouldUpdateValues(state, values) if (shouldUpdate) dispatch({ type: 'update_values', values: { ...values } }) // We only want to run this effect when the passed in values change (external event updating the values) // eslint-disable-next-line react-hooks/exhaustive-deps }, [values]) useEffect(() => { if (state.hasUpdatedValues) { onValuesChange({ ...state.values }) dispatch({ type: 'complete_update_values' }) } // We only want to run this effect when the state has updated values // eslint-disable-next-line react-hooks/exhaustive-deps }, [state]) useEffect(() => { const newFiltersHash = createFiltersHash(filters) if (newFiltersHash !== filtersHash.current) { filtersHash.current = newFiltersHash dispatch({ type: 'update_filter_labels', data: filters }) } }, [filters]) return ( >} > {children(activeFilters)} ) }