import React, { useRef, useState, useEffect, useCallback } from "react"; import PropTypes from "prop-types"; import Header from "./Header"; import Search from "./Search"; import Footer from "./Footer"; import { FixedSizeList as List } from "react-window"; import InfiniteLoader from "react-window-infinite-loader"; import AutoSizer from "react-virtualized-auto-sizer"; import ReactTooltip from "react-tooltip"; import Avatar from "@appserver/components/avatar"; import Checkbox from "@appserver/components/checkbox"; import Loader from "@appserver/components/loader"; import Text from "@appserver/components/text"; import Tooltip from "@appserver/components/tooltip"; import Heading from "@appserver/components/heading"; import IconButton from "@appserver/components/icon-button"; import CustomScrollbarsVirtualList from "@appserver/components/scrollbar/custom-scrollbars-virtual-list"; import StyledSelector from "./StyledSelector"; const convertGroups = (items) => { if (!items) return []; const wrappedGroups = items.map(convertGroup); return wrappedGroups; }; const convertGroup = (group) => { return { key: group.key, label: `${group.label} (${group.total})`, total: group.total, selected: 0, }; }; const getCurrentGroup = (items) => { const currentGroup = items.length > 0 ? items[0] : {}; return currentGroup; }; const Selector = (props) => { const { groups, isDisabled, isMultiSelect, hasNextPage, options, isNextPageLoading, loadNextPage, selectedOptions, selectedGroups, searchPlaceHolderLabel, emptySearchOptionsLabel, emptyOptionsLabel, loadingLabel, onSelect, getOptionTooltipContent, onSearchChanged, onGroupChanged, size, allowGroupSelection, embeddedComponent, showCounter, onArrowClick, headerLabel, } = props; const listOptionsRef = useRef(null); useEffect(() => { Object.keys(currentGroup).length === 0 && setCurrentGroup(getCurrentGroup(convertGroups(groups))); resetCache(); }, [searchValue, currentGroup, hasNextPage]); const [selectedOptionList, setSelectedOptionList] = useState( selectedOptions || [] ); const [selectedGroupList, setSelectedGroupList] = useState( selectedGroups || [] ); const [searchValue, setSearchValue] = useState(""); const [currentGroup, setCurrentGroup] = useState( getCurrentGroup(convertGroups(groups)) ); const [groupHeader, setGroupHeader] = useState(null); useEffect(() => { if (groups.length === 1) setGroupHeader(groups[0]); }, [groups]); // Every row is loaded except for our loading indicator row. const isItemLoaded = useCallback( (index) => { return !hasNextPage || index < options.length; }, [hasNextPage, options] ); const onOptionChange = useCallback( (index, isChecked) => { const option = options[index]; const newSelected = !isChecked ? [option, ...selectedOptionList] : selectedOptionList.filter((el) => el.key !== option.key); setSelectedOptionList(newSelected); if (!option.groups) return; const newSelectedGroups = []; const removedSelectedGroups = []; if (isChecked) { option.groups.forEach((g) => { let index = selectedGroupList.findIndex((sg) => sg.key === g); if (index > -1) { // exists const selectedGroup = selectedGroupList[index]; const newSelected = selectedGroup.selected + 1; newSelectedGroups.push( Object.assign({}, selectedGroup, { selected: newSelected, }) ); } else { index = groups.findIndex((sg) => sg.key === g); if (index < 0) return; const notSelectedGroup = convertGroup(groups[index]); newSelectedGroups.push( Object.assign({}, notSelectedGroup, { selected: 1, }) ); } }); } else { option.groups.forEach((g) => { let index = selectedGroupList.findIndex((sg) => sg.key === g); if (index > -1) { // exists const selectedGroup = selectedGroupList[index]; const newSelected = selectedGroup.selected - 1; if (newSelected > 0) { newSelectedGroups.push( Object.assign({}, selectedGroup, { selected: newSelected, }) ); } else { removedSelectedGroups.push( Object.assign({}, selectedGroup, { selected: newSelected, }) ); } } }); } selectedGroupList.forEach((g) => { const indexNew = newSelectedGroups.findIndex((sg) => sg.key === g.key); if (indexNew === -1) { const indexRemoved = removedSelectedGroups.findIndex( (sg) => sg.key === g.key ); if (indexRemoved === -1) { newSelectedGroups.push(g); } } }); setSelectedGroupList(newSelectedGroups); }, [options, selectedOptionList, groups, selectedGroupList] ); const resetCache = useCallback(() => { if (listOptionsRef && listOptionsRef.current) { listOptionsRef.current.resetloadMoreItemsCache(true); } }, [listOptionsRef]); const onSearchChange = useCallback( (value) => { setSearchValue(value); onSearchChanged && onSearchChanged(value); }, [onSearchChanged] ); const onSearchReset = useCallback(() => { onSearchChanged && onSearchChange(""); }, [onSearchChanged]); const isOptionChecked = useCallback( (option) => { const checked = selectedOptionList.findIndex((el) => el.key === option.key) > -1 || (option.groups && option.groups.filter((gKey) => { const selectedGroup = selectedGroupList.find( (sg) => sg.key === gKey ); if (!selectedGroup) return false; return selectedGroup.total === selectedGroup.selected; }).length > 0); return checked; }, [selectedOptionList, selectedGroupList] ); const onSelectOptions = (items) => { onSelect && onSelect(items); }; const onAddClick = useCallback(() => { onSelectOptions(selectedOptionList); }, [selectedOptionList]); const onLinkClick = useCallback( (index) => { const option = options[index]; if (!option) return; onSelectOptions([option]); }, [options] ); const renderOptionItem = useCallback( (index, style, option, isChecked, tooltipProps) => { return isMultiSelect ? (
onOptionChange(index, isChecked)} {...tooltipProps} >
{option.label}
) : (
onLinkClick(index)} {...tooltipProps} >
{" "} {option.label}
); }, [isMultiSelect, onOptionChange, onLinkClick] ); const renderOptionLoader = useCallback( (style) => { return (
{loadingLabel}
); }, [loadingLabel] ); // Render an item or a loading indicator. // eslint-disable-next-line react/prop-types const renderOption = useCallback( ({ index, style }) => { const isLoaded = isItemLoaded(index); if (!isLoaded) { return renderOptionLoader(style); } const option = options[index]; const isChecked = isOptionChecked(option); let tooltipProps = {}; ReactTooltip.rebuild(); return renderOptionItem(index, style, option, isChecked, tooltipProps); }, [ isItemLoaded, renderOptionLoader, renderOptionItem, loadingLabel, options, isOptionChecked, isMultiSelect, onOptionChange, onLinkClick, getOptionTooltipContent, ] ); const hasSelected = useCallback(() => { return selectedOptionList.length > 0 || selectedGroupList.length > 0; }, [selectedOptionList, selectedGroupList]); // If there are more items to be loaded then add an extra row to hold a loading indicator. const itemCount = hasNextPage ? options.length + 1 : options.length; // Only load 1 page of items at a time. // Pass an empty callback to InfiniteLoader in case it asks us to load more than once. const loadMoreItems = useCallback( (startIndex) => { if (isNextPageLoading) return; const options = { startIndex: startIndex || 0, searchValue: searchValue, currentGroup: currentGroup ? currentGroup.key : null, }; loadNextPage && loadNextPage(options); }, [isNextPageLoading, searchValue, currentGroup, options] ); const getGroupSelectedOptions = useCallback( (group) => { const selectedGroup = selectedOptionList.filter( (o) => o.groups && o.groups.indexOf(group) > -1 ); if (group === "all") { selectedGroup.push(...selectedOptionList); } return selectedGroup; }, [selectedOptionList] ); const onGroupClick = useCallback( (index) => { const group = groups[index]; setGroupHeader({ ...group }); onGroupChanged && onGroupChanged(group); setCurrentGroup(group); }, [groups, onGroupChanged] ); const renderGroup = useCallback( ({ index, style }) => { const group = groups[index]; const selectedOption = getGroupSelectedOptions(group.id); const isIndeterminate = selectedOption.length > 0; let label = group.label; if (isMultiSelect && selectedOption.length > 0) { label = `${group.label} (${selectedOption.length})`; } return (
onGroupClick(index)} >
{label}
{isMultiSelect && ( )}
); }, [ isMultiSelect, groups, currentGroup, selectedGroupList, selectedOptionList, getGroupSelectedOptions, ] ); const renderGroupsList = useCallback(() => { if (groups.length === 0) return renderOptionLoader(); return ( {({ width, height }) => ( {renderGroup} )} ); }, [isMultiSelect, groups, selectedOptionList, getGroupSelectedOptions]); const renderGroupHeader = useCallback(() => { const selectedOption = getGroupSelectedOptions(groupHeader.id); const isIndeterminate = selectedOption.length > 0; let label = groupHeader.label; if (isMultiSelect && selectedOption.length > 0) { label = `${groupHeader.label} (${selectedOption.length})`; } return ( <>
{label}
{isMultiSelect && ( )}
); }, [isMultiSelect, groupHeader, selectedOptionList, getGroupSelectedOptions]); const onArrowClickAction = useCallback(() => { if (groupHeader && groups.length !== 1) { setGroupHeader(null); onGroupChanged && onGroupChanged([]); setCurrentGroup([]); return; } onArrowClick && onArrowClick(); }, [groups, groupHeader, onArrowClick, onGroupChanged]); return (
{!groupHeader && !searchValue && groups ? ( renderGroupsList() ) : ( <> {!searchValue && renderGroupHeader()} {!hasNextPage && itemCount === 0 ? (
{!searchValue ? emptyOptionsLabel : emptySearchOptionsLabel}
) : ( {({ width, height }) => ( {({ onItemsRendered, ref }) => ( {renderOption} )} )} )} )} {getOptionTooltipContent && ( )}