import React, { useRef, useState, useEffect, useCallback } from "react"; import PropTypes from "prop-types"; import Column from "./Column"; import Footer from "./Footer"; import Header from "./Header"; import Body from "./Body"; 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 Checkbox from "@appserver/components/checkbox"; import Link from "@appserver/components/link"; import ComboBox from "@appserver/components/combobox"; import SearchInput from "@appserver/components/search-input"; import Loader from "@appserver/components/loader"; import Text from "@appserver/components/text"; import Tooltip from "@appserver/components/tooltip"; import CustomScrollbarsVirtualList from "@appserver/components/scrollbar/custom-scrollbars-virtual-list"; import HelpButton from "@appserver/components/help-button"; 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 { displayType, groups, selectButtonLabel, isDisabled, isMultiSelect, hasNextPage, options, isNextPageLoading, loadNextPage, selectedOptions, selectedGroups, groupsHeaderLabel, searchPlaceHolderLabel, emptySearchOptionsLabel, emptyOptionsLabel, loadingLabel, selectAllLabel, onSelect, getOptionTooltipContent, onSearchChanged, onGroupChanged, size, allowGroupSelection, embeddedComponent, showCounter, } = props; //console.log("options", options); //console.log("hasNextPage", hasNextPage); //console.log("isNextPageLoading", isNextPageLoading); const listOptionsRef = useRef(null); const listGroupsRef = 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 [selectedAll, setSelectedAll] = useState(false); const [currentGroup, setCurrentGroup] = useState( getCurrentGroup(convertGroups(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( (e) => { const option = options[+e.target.value]; const newSelected = e.target.checked ? [option, ...selectedOptionList] : selectedOptionList.filter((el) => el.key !== option.key); setSelectedOptionList(newSelected); if (!option.groups) return; const newSelectedGroups = []; const removedSelectedGroups = []; if (e.target.checked) { 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 onGroupChange = useCallback( (e) => { const group = convertGroup(groups[+e.target.value]); group.selected = e.target.checked ? group.total : 0; const newSelectedGroups = e.target.checked ? [group, ...selectedGroupList] : selectedGroupList.filter((el) => el.key !== group.key); //console.log("onGroupChange", item); setSelectedGroupList(newSelectedGroups); onGroupSelect(group); if (e.target.checked) { //const newSelectedOptions = []; //options.forEach(o => o.groups.forEach(gKey => group.)) //setSelectedOptionList() //TODO: Implement setSelectedOptionList changes } }, [groups, selectedGroupList, currentGroup] ); const resetCache = useCallback(() => { if (listOptionsRef && listOptionsRef.current) { listOptionsRef.current.resetloadMoreItemsCache(true); } }, [listOptionsRef]); const onGroupSelect = useCallback( (group) => { if (!currentGroup || !group || currentGroup.key === group.key) { return; } setCurrentGroup(group); onGroupChanged && onGroupChanged(group); if (displayType === "aside" && isMultiSelect) { setSelectedAll(isGroupChecked(group)); } }, [displayType, isMultiSelect, currentGroup] ); const onSelectAllChange = useCallback(() => { const checked = !selectedAll; //console.log("onSelectAllChange", checked); setSelectedAll(checked); if (!currentGroup) return; const group = convertGroup(currentGroup); if (!group) return; group.selected = checked ? group.total : 0; const newSelectedGroups = checked ? [group, ...selectedGroupList] : selectedGroupList.filter((el) => el.key !== group.key); setSelectedGroupList(newSelectedGroups); }, [selectedAll, currentGroup, selectedGroupList]); const onSearchChange = useCallback((value) => { setSearchValue(value); onSearchChanged && onSearchChanged(value); }); const onSearchReset = useCallback(() => { onSearchChanged && onSearchChange(""); }); const onSelectOptions = (items) => { onSelect && onSelect(items); }; 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 onLinkClick = useCallback( (e) => { const index = e.target.dataset.index; if (!index) return; const option = options[index]; if (!option) return; onSelectOptions([option]); }, [options] ); const onAddClick = useCallback(() => { onSelectOptions(selectedOptionList); }, [selectedOptionList]); const renderOptionItem = useCallback( (index, style, option, isChecked, tooltipProps) => { return isMultiSelect ? (
{displayType === "aside" && getOptionTooltipContent && ( )}
) : ( {option.label} {displayType === "aside" && getOptionTooltipContent && ( )} ); }, [ isMultiSelect, onOptionChange, onLinkClick, displayType, getOptionTooltipContent, ] ); 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 = {}; if (displayType === "dropdown") tooltipProps = { "data-for": "user", "data-tip": index }; ReactTooltip.rebuild(); return renderOptionItem(index, style, option, isChecked, tooltipProps); }, [ isItemLoaded, renderOptionLoader, renderOptionItem, loadingLabel, options, isOptionChecked, displayType, isMultiSelect, onOptionChange, onLinkClick, getOptionTooltipContent, ] ); const isGroupChecked = useCallback( (group) => { const selectedGroup = selectedGroupList.find((g) => g.key === group.key); return !!selectedGroup; }, [selectedGroupList] ); const isGroupIndeterminate = useCallback( (group) => { const selectedGroup = selectedGroupList.find((g) => g.key === group.key); return ( selectedGroup && selectedGroup.selected > 0 && group.total !== selectedGroup.selected ); }, [selectedGroupList] ); const getGroupSelected = useCallback( (group) => { const selectedGroup = selectedGroupList.find((g) => g.key === group.key); return isGroupIndeterminate(group) ? selectedGroup.selected : isGroupChecked(group) ? group.total : 0; }, [selectedGroupList] ); const getGroupLabel = useCallback( (group) => { const selected = getGroupSelected(group); return isMultiSelect && allowGroupSelection ? `${group.label} (${group.total}/${selected})` : group.label; }, [isMultiSelect, allowGroupSelection] ); const getSelectorGroups = useCallback( (groups) => { return groups.map((group) => { return { ...group, label: getGroupLabel(group), }; }); }, [groups] ); const onLinkGroupClick = useCallback( (e) => { const index = e.target.dataset.index; if (!index) return; const group = groups[index]; if (!group) return; onGroupSelect(group); }, [groups, currentGroup] ); // eslint-disable-next-line react/prop-types const renderGroup = useCallback( ({ index, style }) => { const group = groups[index]; const isChecked = isGroupChecked(group); const isIndeterminate = isGroupIndeterminate(group); const isSelected = currentGroup.key === group.key; const label = getGroupLabel(group); return ( {isMultiSelect && allowGroupSelection && ( )} {label} ); }, [ groups, currentGroup, isMultiSelect, selectedGroupList, allowGroupSelection, ] ); 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, }; //setLastIndex(startIndex); //console.log("loadMoreItems", options); loadNextPage && loadNextPage(options); }, [isNextPageLoading, searchValue, currentGroup, options] ); return (
{displayType === "aside" && groups && groups.length > 0 && ( <> {isMultiSelect && allowGroupSelection && options && options.length > 0 && ( )} )}
{({ width, height }) => ( {({ onItemsRendered, ref }) => ( {renderOption} )} )} {!hasNextPage && itemCount === 0 && (
{!searchValue ? emptyOptionsLabel : emptySearchOptionsLabel}
)} {getOptionTooltipContent && ( )}
{displayType === "dropdown" && groups && groups.length > 0 && ( <>
{groupsHeaderLabel}
{({ height, width }) => ( {renderGroup} )}
)}