import React, { useCallback, useEffect, useRef, useState } from 'react' import { Cell, Row, StickyTable } from 'react-sticky-table' import styles from './_treeListBox.module.scss' import tableStyles from '../TableComponents/_table.module.scss' import CheckboxList from './CheckboxList' import EmptyState from '../EmptyState/EmptyState' import ListLoading from '../Loaders/ListLoading' import SearchBar from '../SearchBar/SearchBar' import StickyTableContainer from '../TableComponents/StickyTableContainer' import UnsortedColumn from '../UnsortedColumn/UnsortedColumn' import { type EmptyStateProps } from '../EmptyState/EmptyState' import { c } from '../../translations/LibraryTranslationService' import { CheckboxState, debouncedSearchRefactored, type FlattenNestedDataProps, type GenericNode, type NestedCheckboxProps, toggleExpand, updateItemStates, } from '../Form/NestedCheckboxHelper' type TreeListBoxProps = NestedCheckboxProps function TreeListBox({ data, hasData, isLoading, tableConfig, noDataText, identifierKeys, isSearchable = false, customHeight, customWidth, onChange, setItemStates, itemStates, search, qaTestId = 'tree-list-box', ignoreNodeHierarchy = false, }: TreeListBoxProps) { const { displayKey, idKey, parentKey } = identifierKeys const noDataTextProps = noDataText ?? { primaryText: c('noDataAvailable'), } const [expandedItems, setExpandedItems] = useState<{ [key: string]: boolean }>({}) // TODO: Remove this state once search prop is implemented all instances of TreeListBox const [searchQuery, setSearchQuery] = useState('') const [filteredItems, setFilteredItems] = useState(data) // Track checkbox interactions to prevent search re-runs const isCheckboxInteraction = useRef(false) const searchDataDependencies = useRef<{ data: any[]; query: string }>({ data: [], query: '', }) // Add state to track search loading state const [isSearching, setIsSearching] = useState(false) const getStateForId = useCallback( (id: string | number) => { return ( itemStates?.find((i) => i.id === id)?.state || CheckboxState.UNCHECKED ) }, [itemStates], ) const clickHandler = useCallback( (id: string | number) => { // Set flag to indicate checkbox interaction isCheckboxInteraction.current = true const newState = updateItemStates( ignoreNodeHierarchy, itemStates, data, id, idKey, parentKey, ) setItemStates(newState) onChange?.(newState) // Clear the flag after a short delay setTimeout(() => { isCheckboxInteraction.current = false }, 150) }, [ data, idKey, itemStates, onChange, parentKey, setItemStates, ignoreNodeHierarchy, ], ) const onToggleExpand = useCallback( (idOrIds: string | number | (string | number)[]) => { toggleExpand(idOrIds, setExpandedItems) }, [], ) useEffect(() => { // TODO: Remove searchQuery once search prop is implemented at all instances of TreeListBox const currentQuery = search !== undefined ? search : searchQuery // Skip search if this was triggered by a checkbox interaction if (isCheckboxInteraction.current) { return } // Use a more stable way to detect data changes const searchDependenciesChanged = currentQuery !== searchDataDependencies.current.query || data.length !== searchDataDependencies.current.data.length // Trigger search on query changes, data length changes, or initial data load if (searchDependenciesChanged) { // Set searching state to show loader if (currentQuery && currentQuery.length >= 3) { setIsSearching(true) } // Update dependencies searchDataDependencies.current = { data, query: currentQuery, } // Call debouncedSearch for query changes debouncedSearchRefactored({ query: currentQuery, identifierKeys: identifierKeys, setFilteredItems: (items) => { setFilteredItems(items) // Once we have results, hide the loader setIsSearching(false) }, nestedConfigData: data, toggleExpand: onToggleExpand, }) } else { // Reset searching state if only other dependencies changed setIsSearching(false) } }, [search, data, searchQuery, identifierKeys, onToggleExpand]) const emptyStateContent = (emptyStateProps: EmptyStateProps) => { return (
) } return ( <> {/** TODO: Remove SearchBar and isSearchable prop once search prop is implemented at all instances of TreeListBox */} {search !== undefined ? ( <> ) : ( isSearchable && ( { // Reset checkbox interaction flag when search changes isCheckboxInteraction.current = false setSearchQuery(value) }} value={searchQuery} /> ) )} {tableConfig.map((column, index) => ( ))} {/* empty cell for the expand/collapse */} {isLoading || isSearching ? (
) : (search !== undefined ? search : searchQuery) && filteredItems?.length === 0 ? ( emptyStateContent({ primaryText: c('noResultsFound'), secondaryText: c('updateSearchField'), }) ) : hasData ? ( ) : ( emptyStateContent({ ...noDataTextProps }) )}
) } export default TreeListBox