463 lines
12 KiB
TypeScript
463 lines
12 KiB
TypeScript
import React from "react";
|
|
|
|
import { Header } from "./sub-components/Header";
|
|
import { Body } from "./sub-components/Body";
|
|
import { Footer } from "./sub-components/Footer";
|
|
|
|
import { StyledSelector } from "./Selector.styled";
|
|
import { AccessRight, SelectorProps, TSelectorItem } from "./Selector.types";
|
|
|
|
const Selector = ({
|
|
id,
|
|
className,
|
|
style,
|
|
|
|
headerLabel,
|
|
withoutBackButton,
|
|
onBackClick,
|
|
|
|
isBreadCrumbsLoading,
|
|
breadCrumbsLoader,
|
|
withBreadCrumbs,
|
|
breadCrumbs,
|
|
onSelectBreadCrumb,
|
|
|
|
withSearch,
|
|
searchLoader,
|
|
isSearchLoading,
|
|
searchPlaceholder,
|
|
searchValue,
|
|
onSearch,
|
|
onClearSearch,
|
|
|
|
withSelectAll,
|
|
selectAllLabel,
|
|
selectAllIcon,
|
|
onSelectAll,
|
|
|
|
items,
|
|
renderCustomItem,
|
|
isMultiSelect,
|
|
selectedItems,
|
|
acceptButtonLabel,
|
|
onSelect,
|
|
onAccept,
|
|
rowLoader,
|
|
|
|
withAccessRights,
|
|
accessRights,
|
|
selectedAccessRight,
|
|
onAccessRightsChange,
|
|
|
|
withCancelButton,
|
|
cancelButtonLabel,
|
|
onCancel,
|
|
|
|
emptyScreenImage,
|
|
emptyScreenHeader,
|
|
emptyScreenDescription,
|
|
|
|
searchEmptyScreenImage,
|
|
searchEmptyScreenHeader,
|
|
searchEmptyScreenDescription,
|
|
|
|
hasNextPage,
|
|
isNextPageLoading,
|
|
loadNextPage,
|
|
totalItems,
|
|
isLoading,
|
|
|
|
withHeader,
|
|
|
|
withFooterInput,
|
|
withFooterCheckbox,
|
|
footerInputHeader,
|
|
footerCheckboxLabel,
|
|
currentFooterInputValue,
|
|
|
|
alwaysShowFooter,
|
|
disableAcceptButton,
|
|
|
|
descriptionText,
|
|
acceptButtonId,
|
|
cancelButtonId,
|
|
isChecked,
|
|
setIsChecked,
|
|
|
|
withTabs,
|
|
tabsData,
|
|
activeTabId,
|
|
}: SelectorProps) => {
|
|
const [areItemsUpdated, setAreItemsUpdated] = React.useState(false);
|
|
const [footerVisible, setFooterVisible] = React.useState<boolean>(false);
|
|
const [isSearch, setIsSearch] = React.useState<boolean>(false);
|
|
|
|
const [renderedItems, setRenderedItems] = React.useState<TSelectorItem[]>([]);
|
|
const [newSelectedItems, setNewSelectedItems] = React.useState<
|
|
TSelectorItem[]
|
|
>([]);
|
|
|
|
const [newFooterInputValue, setNewFooterInputValue] = React.useState<string>(
|
|
currentFooterInputValue || "",
|
|
);
|
|
const [isFooterCheckboxChecked, setIsFooterCheckboxChecked] =
|
|
React.useState<boolean>(isChecked || false);
|
|
|
|
const [selectedAccess, setSelectedAccess] =
|
|
React.useState<AccessRight | null>(null);
|
|
|
|
const onBackClickAction = React.useCallback(() => {
|
|
onBackClick?.();
|
|
}, [onBackClick]);
|
|
|
|
const onClearSearchAction = React.useCallback(() => {
|
|
onClearSearch?.(() => setIsSearch(false));
|
|
}, [onClearSearch]);
|
|
|
|
const onSearchAction = React.useCallback(
|
|
(value: string) => {
|
|
const v = value.trim();
|
|
|
|
if (v === "") return onClearSearchAction();
|
|
|
|
onSearch?.(v, () => setIsSearch(true));
|
|
},
|
|
[onSearch, onClearSearchAction],
|
|
);
|
|
|
|
const compareSelectedItems = React.useCallback(
|
|
(newList: TSelectorItem[]) => {
|
|
let isEqual = true;
|
|
|
|
if (selectedItems?.length !== newList.length) {
|
|
return setFooterVisible(true);
|
|
}
|
|
|
|
if (newList.length === 0 && selectedItems?.length === 0) {
|
|
return setFooterVisible(false);
|
|
}
|
|
|
|
newList.forEach((item) => {
|
|
isEqual = selectedItems.some((x) => x.id === item.id);
|
|
});
|
|
|
|
return setFooterVisible(!isEqual);
|
|
},
|
|
[selectedItems],
|
|
);
|
|
|
|
const onSelectAction = (item: TSelectorItem) => {
|
|
onSelect?.({
|
|
...item,
|
|
id: item.id,
|
|
email: item.email || "",
|
|
avatar: item.avatar,
|
|
icon: item.icon,
|
|
label: item.label,
|
|
shared: item.shared,
|
|
});
|
|
|
|
if (isMultiSelect) {
|
|
setRenderedItems((value) => {
|
|
const idx = value.findIndex((x) => item.id === x.id);
|
|
|
|
const newValue = value.map((i: TSelectorItem) => ({ ...i }));
|
|
|
|
if (idx === -1) return newValue;
|
|
|
|
newValue[idx].isSelected = !value[idx].isSelected;
|
|
|
|
return newValue;
|
|
});
|
|
|
|
if (item.isSelected) {
|
|
setNewSelectedItems((value) => {
|
|
const newValue = value
|
|
.filter((x) => x.id !== item.id)
|
|
.map((x) => ({ ...x }));
|
|
compareSelectedItems(newValue);
|
|
return newValue;
|
|
});
|
|
} else {
|
|
setNewSelectedItems((value) => {
|
|
value.push({
|
|
id: item.id,
|
|
email: item.email,
|
|
...item,
|
|
});
|
|
|
|
compareSelectedItems(value);
|
|
|
|
return value;
|
|
});
|
|
}
|
|
} else {
|
|
setRenderedItems((value) => {
|
|
const idx = value.findIndex((x) => item.id === x.id);
|
|
|
|
const newValue = value.map((i: TSelectorItem) => ({
|
|
...i,
|
|
isSelected: false,
|
|
}));
|
|
|
|
if (idx === -1) return newValue;
|
|
|
|
newValue[idx].isSelected = !item.isSelected;
|
|
|
|
return newValue;
|
|
});
|
|
|
|
const newItem = {
|
|
id: item.id,
|
|
email: item.email,
|
|
|
|
...item,
|
|
};
|
|
|
|
if (item.isSelected) {
|
|
setNewSelectedItems([]);
|
|
compareSelectedItems([]);
|
|
} else {
|
|
setNewSelectedItems([newItem]);
|
|
compareSelectedItems([newItem]);
|
|
}
|
|
}
|
|
};
|
|
|
|
const onSelectAllAction = React.useCallback(() => {
|
|
onSelectAll?.();
|
|
|
|
if (!items) return;
|
|
|
|
if (
|
|
newSelectedItems.length === 0 ||
|
|
newSelectedItems.length !== items.length
|
|
) {
|
|
const cloneItems = items.map((x) => ({ ...x }));
|
|
|
|
const cloneRenderedItems = items.map((x) => ({ ...x, isSelected: true }));
|
|
|
|
setRenderedItems(cloneRenderedItems);
|
|
setNewSelectedItems(cloneItems);
|
|
compareSelectedItems(cloneItems);
|
|
} else {
|
|
const cloneRenderedItems = items.map((x) => ({
|
|
...x,
|
|
isSelected: false,
|
|
}));
|
|
|
|
setRenderedItems(cloneRenderedItems);
|
|
setNewSelectedItems([]);
|
|
compareSelectedItems([]);
|
|
}
|
|
}, [compareSelectedItems, items, newSelectedItems.length, onSelectAll]);
|
|
|
|
const onAcceptAction = () => {
|
|
onAccept?.(
|
|
newSelectedItems,
|
|
selectedAccess,
|
|
newFooterInputValue,
|
|
isFooterCheckboxChecked,
|
|
);
|
|
};
|
|
|
|
const onCancelAction = React.useCallback(() => {
|
|
onCancel?.();
|
|
}, [onCancel]);
|
|
|
|
const onChangeAccessRightsAction = React.useCallback(
|
|
(access: AccessRight) => {
|
|
setSelectedAccess({ ...access });
|
|
onAccessRightsChange?.(access);
|
|
},
|
|
[onAccessRightsChange],
|
|
);
|
|
|
|
const loadMoreItems = React.useCallback(
|
|
(startIndex: number) => {
|
|
if (startIndex === 1) return; // fix double fetch of the first page
|
|
if (!isNextPageLoading) loadNextPage?.(startIndex - 1);
|
|
},
|
|
[isNextPageLoading, loadNextPage],
|
|
);
|
|
|
|
React.useEffect(() => {
|
|
if (selectedAccessRight) setSelectedAccess({ ...selectedAccessRight });
|
|
}, [selectedAccessRight]);
|
|
|
|
React.useLayoutEffect(() => {
|
|
if (items && selectedItems) {
|
|
if (selectedItems.length === 0 || !isMultiSelect) {
|
|
const cloneItems = items.map((x) => ({ ...x, isSelected: false }));
|
|
return setRenderedItems(cloneItems);
|
|
}
|
|
|
|
const newItems = items.map((item) => {
|
|
const { id: itemId } = item;
|
|
|
|
const isSelected = selectedItems.some(
|
|
(selectedItem) => selectedItem.id === itemId,
|
|
);
|
|
|
|
return { ...item, isSelected };
|
|
});
|
|
|
|
const cloneSelectedItems = selectedItems.map((item) => ({ ...item }));
|
|
|
|
setRenderedItems(newItems);
|
|
setNewSelectedItems(cloneSelectedItems);
|
|
compareSelectedItems(cloneSelectedItems);
|
|
}
|
|
}, [items, selectedItems, isMultiSelect, compareSelectedItems]);
|
|
|
|
React.useEffect(() => {
|
|
if (!areItemsUpdated) return;
|
|
if (!newSelectedItems.length || !isMultiSelect || !items) {
|
|
setAreItemsUpdated(false);
|
|
return;
|
|
}
|
|
|
|
let hasConflict = false;
|
|
|
|
const cloneItems = items.map((x) => {
|
|
if (x.isSelected) return { ...x };
|
|
|
|
const isSelected = newSelectedItems.some(
|
|
(selectedItem) => selectedItem.id === x.id,
|
|
);
|
|
|
|
if (isSelected) hasConflict = true;
|
|
|
|
return { ...x, isSelected };
|
|
});
|
|
|
|
if (hasConflict) {
|
|
setRenderedItems(cloneItems);
|
|
}
|
|
setAreItemsUpdated(false);
|
|
}, [areItemsUpdated, isMultiSelect, items, newSelectedItems]);
|
|
|
|
React.useEffect(() => {
|
|
setAreItemsUpdated(true);
|
|
}, [items]);
|
|
return (
|
|
<StyledSelector
|
|
id={id}
|
|
className={className}
|
|
style={style}
|
|
data-testid="selector"
|
|
>
|
|
{withHeader && (
|
|
<Header
|
|
onBackClickAction={onBackClickAction}
|
|
headerLabel={headerLabel}
|
|
withoutBackButton={withoutBackButton}
|
|
/>
|
|
)}
|
|
|
|
<Body
|
|
withHeader={withHeader}
|
|
footerVisible={footerVisible || !!alwaysShowFooter}
|
|
isSearch={isSearch}
|
|
isAllIndeterminate={
|
|
newSelectedItems.length !== renderedItems.length &&
|
|
newSelectedItems.length !== 0
|
|
}
|
|
isAllChecked={
|
|
newSelectedItems.length === renderedItems.length &&
|
|
renderedItems.length !== 0
|
|
}
|
|
placeholder={searchPlaceholder}
|
|
value={searchValue}
|
|
onSearch={onSearchAction}
|
|
onClearSearch={onClearSearchAction}
|
|
items={renderedItems}
|
|
isMultiSelect={isMultiSelect}
|
|
onSelect={onSelectAction}
|
|
withSelectAll={withSelectAll}
|
|
selectAllLabel={selectAllLabel}
|
|
selectAllIcon={selectAllIcon}
|
|
onSelectAll={onSelectAllAction}
|
|
emptyScreenImage={emptyScreenImage}
|
|
emptyScreenHeader={emptyScreenHeader}
|
|
emptyScreenDescription={emptyScreenDescription}
|
|
searchEmptyScreenImage={searchEmptyScreenImage}
|
|
searchEmptyScreenHeader={searchEmptyScreenHeader}
|
|
searchEmptyScreenDescription={searchEmptyScreenDescription}
|
|
hasNextPage={hasNextPage}
|
|
isNextPageLoading={isNextPageLoading}
|
|
loadMoreItems={loadMoreItems}
|
|
renderCustomItem={renderCustomItem}
|
|
totalItems={totalItems || 0}
|
|
isLoading={isLoading}
|
|
searchLoader={searchLoader}
|
|
rowLoader={rowLoader}
|
|
withBreadCrumbs={withBreadCrumbs}
|
|
breadCrumbs={breadCrumbs}
|
|
onSelectBreadCrumb={onSelectBreadCrumb}
|
|
breadCrumbsLoader={breadCrumbsLoader}
|
|
isBreadCrumbsLoading={isBreadCrumbsLoading}
|
|
isSearchLoading={isSearchLoading}
|
|
withSearch={withSearch}
|
|
withFooterInput={withFooterInput}
|
|
withFooterCheckbox={withFooterCheckbox}
|
|
descriptionText={descriptionText}
|
|
withTabs={withTabs}
|
|
tabsData={tabsData}
|
|
activeTabId={activeTabId}
|
|
/>
|
|
|
|
{(footerVisible || alwaysShowFooter) && (
|
|
<Footer
|
|
isMultiSelect={isMultiSelect}
|
|
acceptButtonLabel={acceptButtonLabel || ""}
|
|
selectedItemsCount={newSelectedItems.length}
|
|
withCancelButton={withCancelButton}
|
|
cancelButtonLabel={cancelButtonLabel}
|
|
withAccessRights={withAccessRights}
|
|
accessRights={accessRights}
|
|
selectedAccessRight={selectedAccess}
|
|
onAccept={onAcceptAction}
|
|
onCancel={onCancelAction}
|
|
onChangeAccessRights={onChangeAccessRightsAction}
|
|
withFooterInput={withFooterInput}
|
|
withFooterCheckbox={withFooterCheckbox}
|
|
footerInputHeader={footerInputHeader}
|
|
footerCheckboxLabel={footerCheckboxLabel}
|
|
currentFooterInputValue={newFooterInputValue}
|
|
setNewFooterInputValue={setNewFooterInputValue}
|
|
isFooterCheckboxChecked={isFooterCheckboxChecked}
|
|
setIsFooterCheckboxChecked={setIsFooterCheckboxChecked}
|
|
setIsChecked={setIsChecked}
|
|
disableAcceptButton={
|
|
withFooterInput
|
|
? disableAcceptButton
|
|
: disableAcceptButton && !newFooterInputValue.trim()
|
|
}
|
|
acceptButtonId={acceptButtonId}
|
|
cancelButtonId={cancelButtonId}
|
|
/>
|
|
)}
|
|
</StyledSelector>
|
|
);
|
|
};
|
|
|
|
Selector.defaultProps = {
|
|
isMultiSelect: false,
|
|
withSelectAll: false,
|
|
withAccessRights: false,
|
|
withCancelButton: false,
|
|
withoutBackButton: false,
|
|
isBreadCrumbsLoading: false,
|
|
withSearch: true,
|
|
withFooterInput: false,
|
|
alwaysShowFooter: false,
|
|
disableAcceptButton: false,
|
|
withHeader: true,
|
|
withTabs: false,
|
|
|
|
selectedItems: [],
|
|
};
|
|
|
|
export { Selector };
|