import React, { useRef, useState, useEffect, useCallback } from "react";
import PropTypes from "prop-types";
import Header from "./Header";
import Search from "./Search";
import Group from "./Group";
import Option from "./Option";
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 Text from "@appserver/components/text";
import Tooltip from "@appserver/components/tooltip";
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]
);
// 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 ;
}
const option = options[index];
const isChecked = isOptionChecked(option);
ReactTooltip.rebuild();
return (
);
},
[
isItemLoaded,
loadingLabel,
options,
isOptionChecked,
isMultiSelect,
onOptionChange,
onLinkClick,
getOptionTooltipContent,
isMultiSelect,
onOptionChange,
onLinkClick,
]
);
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 (
{renderGroup}
)}