/* eslint-disable @typescript-eslint/no-explicit-any */ import { useCallback, useEffect, useRef, useState } from "react"; import debounce from "lodash/debounce"; import GlobalSearchView from "./GlobalSearchView"; export type GlobalSearchProps = { uiElementGroupData: Record; visible: boolean; disabled: boolean; config: { uiElementGroupId: string }; onModelUpdate: ( callBack: ((args: any) => void) | null, fieldName: string, value: any ) => void; }; const GlobalSearch = (props: GlobalSearchProps) => { const [searchValue, setSearchValue] = useState(""); const isLocalChange = useRef(false); // If appliedQuery has value but filterQuery doesn't, update filterQuery useEffect(() => { if ( props.uiElementGroupData?.globalSearch?.appliedQuery && !props.uiElementGroupData?.globalSearch?.filterQuery ) { const updatedUiElementGroupData = { ...props.uiElementGroupData, globalSearch: { ...props.uiElementGroupData.globalSearch, filterQuery: props.uiElementGroupData.globalSearch.appliedQuery, }, }; props.onModelUpdate( null, props.config.uiElementGroupId, updatedUiElementGroupData ); } }, [props.uiElementGroupData?.globalSearch?.appliedQuery]); // The query the user has actually typed, updated synchronously by // `onSearchChange`. `onSearch` (Enter / magnifier) must read this rather than // `props...filterQuery`, which only catches up after the 300 ms debounce - // pressing Enter inside that window applied the previous query. Flushing the // debounce is not a fix: the debounced callback rebuilds its payload from its // own stale closure. const latestFilterQueryRef = useRef( props.uiElementGroupData?.globalSearch?.filterQuery ?? "" ); useEffect(() => { if (isLocalChange.current) { isLocalChange.current = false; return; } const filterQuery = props.uiElementGroupData?.globalSearch?.filterQuery ?? ""; latestFilterQueryRef.current = filterQuery; setSearchValue(filterQuery); }, [props.uiElementGroupData?.globalSearch?.filterQuery]); const latestUiElementGroupDataRef = useRef(props.uiElementGroupData); useEffect(() => { latestUiElementGroupDataRef.current = props.uiElementGroupData; }, [props.uiElementGroupData]); const debouncedModelUpdate = useCallback( debounce((searchKey: string) => { const updatedUiElementGroupData = { ...latestUiElementGroupDataRef.current, globalSearch: { ...latestUiElementGroupDataRef.current.globalSearch, filterQuery: searchKey, }, }; props.onModelUpdate( null, props.config.uiElementGroupId, updatedUiElementGroupData, ); }, 300), [props.config.uiElementGroupId], ); // A debounce left pending at unmount fires into a dead component. useEffect( () => () => { debouncedModelUpdate.cancel(); }, [debouncedModelUpdate], ); const onSearch = () => { const filterQuery = latestFilterQueryRef.current; if (filterQuery) { // The pending debounce would only re-apply the same filterQuery. debouncedModelUpdate.cancel(); const updatedUiElementGroupData = { ...props.uiElementGroupData, globalSearch: { ...props.uiElementGroupData.globalSearch, filterQuery, appliedQuery: filterQuery, }, pagination: { ...props.uiElementGroupData.pagination, skip: 0 }, }; props.onModelUpdate( null, props.config.uiElementGroupId, updatedUiElementGroupData ); } }; const onClearSearch = () => { const updatedUiElementGroupData = { ...props.uiElementGroupData, globalSearch: { ...props.uiElementGroupData.globalSearch, filterQuery: "", appliedQuery: "", }, pagination: { ...props.uiElementGroupData.pagination, skip: 0 }, }; props.onModelUpdate( null, props.config.uiElementGroupId, updatedUiElementGroupData ); }; const onSearchChange = (searchKey: string | undefined) => { isLocalChange.current = true; const nextSearchKey = searchKey ?? ""; latestFilterQueryRef.current = nextSearchKey; setSearchValue(nextSearchKey); if (!nextSearchKey) { // Unconditional: guarding this on the *previous* filterQuery left a // pending debounce alive whenever the box was emptied while filterQuery // was still "", and it landed 300 ms later. debouncedModelUpdate.cancel(); if (props.uiElementGroupData?.globalSearch?.filterQuery) { onClearSearch(); } } else { debouncedModelUpdate(nextSearchKey); } }; return props.visible ? ( ) : ( <> ); }; export default GlobalSearch;