Merge pull request #799 from ONLYOFFICE/feature/selector

Feature/selector
This commit is contained in:
Ilya Oleshko 2022-09-08 10:45:20 +03:00 committed by GitHub
commit e1f6a94eec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 2125 additions and 531 deletions

View File

@ -3,5 +3,11 @@
"CustomAllGroups": "All {{ groupsCaption, lowercase }}",
"EmptySearchUsersResult": "There are no users with such name",
"EmptyUsers": "There are no users",
"SearchUsersPlaceholder": "Search users"
"SearchUsersPlaceholder": "Search users",
"ListAccounts": "List accounts",
"AllAccounts": "All accounts",
"EmptyHeader": "No other accounts here yet",
"EmptyDescription": "The list of users previously invited to DocSpace or separate rooms will appear here. You will be able to invite these users for collaboration at any time.",
"SearchEmptyHeader": "No users found",
"SearchEmptyDescription": "No people matching your filter can be displayed in this section. Please select other filter options or clear filter to view all the people in this section."
}

View File

@ -0,0 +1,7 @@
{
"RoomList": "Room list",
"EmptyRoomsHeader": "No rooms here yet",
"EmptyRoomsDescription": "Please create the first room in Shared.",
"SearchEmptyRoomsHeader": "Nothing found",
"SearchEmptyRoomsDescription": "No rooms match this filter. Try a different one or clear filter to view all rooms."
}

View File

@ -1,328 +1,175 @@
import React from "react";
import React, { useState, useEffect, useCallback } from "react";
import { inject, observer } from "mobx-react";
import PropTypes from "prop-types";
import { I18nextProvider, withTranslation } from "react-i18next";
import i18n from "./i18n";
import AdvancedSelector from "@docspace/common/components/AdvancedSelector";
import { getUserList } from "@docspace/common/api/people";
import { getGroupList } from "@docspace/common/api/groups";
import Selector from "@docspace/components/selector";
import Filter from "@docspace/common/api/people/filter";
import UserTooltip from "./sub-components/UserTooltip";
class PeopleSelector extends React.Component {
constructor(props) {
super(props);
import { getUserList } from "@docspace/common/api/people";
import Loaders from "@docspace/common/components/Loaders";
this.state = {
options: [],
groups: [],
total: 0,
page: 0,
hasNextPage: true,
isNextPageLoading: false,
isFirstLoad: true,
};
}
const PeopleSelector = ({
acceptButtonLabel,
accessRights,
cancelButtonLabel,
className,
emptyScreenDescription,
emptyScreenHeader,
emptyScreenImage,
headerLabel,
id,
isMultiSelect,
items,
onAccept,
onAccessRightsChange,
onBackClick,
onCancel,
onSelect,
onSelectAll,
searchEmptyScreenDescription,
searchEmptyScreenHeader,
searchEmptyScreenImage,
searchPlaceholder,
selectAllIcon,
selectAllLabel,
selectedAccessRight,
selectedItems,
style,
t,
withAccessRights,
withCancelButton,
withSelectAll,
filter,
}) => {
const [itemsList, setItemsList] = useState(items);
const [searchValue, setSearchValue] = useState("");
const [total, setTotal] = useState(0);
const [hasNextPage, setHasNextPage] = useState(true);
const [isNextPageLoading, setIsNextPageLoading] = useState(false);
const [isLoading, setIsLoading] = useState(true);
componentDidMount() {
const { groupList, useFake, t } = this.props;
useEffect(() => {
setIsLoading(true);
loadNextPage(0);
}, []);
if (!groupList) {
getGroupList(useFake)
.then((groups) =>
this.setState({
groups: [
{
key: "all",
id: "all",
label: `${t("AllUsers")}`,
total: 0,
selectedCount: 0,
},
].concat(this.convertGroups(groups)),
})
)
.catch((error) => console.log(error));
} else {
this.setState({
groups: [
{
key: "all",
id: "all",
label: `${t("AllUsers")}`,
total: 0,
selectedCount: 0,
},
].concat(groupList),
});
}
}
convertGroups = (groups) => {
return groups
? groups.map((g) => {
return {
key: g.id,
id: g.id,
label: g.name,
total: 0,
selectedCount: 0,
};
})
: [];
};
convertUser = (u) => {
const toListItem = (item) => {
const { id, avatar, icon, displayName } = item;
return {
key: u.id,
groups: u.groups && u.groups.length ? u.groups.map((g) => g.id) : [],
label: u.displayName,
email: u.email,
position: u.title,
avatarUrl: u.avatar,
id,
avatar,
icon,
label: displayName,
};
};
convertUsers = (users) => {
return users ? users.map(this.convertUser) : [];
};
loadNextPage = ({ startIndex, searchValue, currentGroup }) => {
// console.log(
// `loadNextPage(startIndex=${startIndex}, searchValue="${searchValue}", currentGroup="${currentGroup}")`
// );
const loadNextPage = (startIndex, search = searchValue) => {
const pageCount = 100;
this.setState(
{ isNextPageLoading: true, isFirstLoad: startIndex === 0 },
() => {
const { role, employeeStatus, useFake } = this.props;
setIsNextPageLoading(true);
const filter = Filter.getDefault();
filter.page = startIndex / pageCount;
filter.pageCount = pageCount;
const filter = filter || Filter.getDefault();
filter.page = startIndex / pageCount;
filter.pageCount = pageCount;
if (searchValue) {
filter.search = searchValue;
}
if (!!search.length) {
filter.search = search;
}
if (employeeStatus) {
filter.employeeStatus = employeeStatus;
}
getUserList(filter)
.then((response) => {
let newItems = startIndex ? itemsList : [];
if (role) {
filter.role = role;
}
if (employeeStatus) {
filter.employeeStatus = employeeStatus;
}
const items = response.items.map((item) => toListItem(item));
if (currentGroup && currentGroup !== "all") filter.group = currentGroup;
newItems = [...newItems, ...items];
const { defaultOption, defaultOptionLabel } = this.props;
setHasNextPage(newItems.length < response.total);
setItemsList(newItems);
setTotal(response.total);
getUserList(filter, useFake)
.then((response) => {
let newOptions = startIndex ? [...this.state.options] : [];
setIsNextPageLoading(false);
setIsLoading(false);
})
.catch((error) => console.log(error));
};
if (defaultOption) {
const inGroup =
!currentGroup ||
currentGroup === "all" ||
(defaultOption.groups &&
defaultOption.groups.filter((g) => g.id === currentGroup)
.length > 0);
const onSearch = (value) => {
setSearchValue(value);
setIsLoading(true);
loadNextPage(0, value);
};
if (searchValue) {
const exists = response.items.find(
(item) => item.id === defaultOption.id
);
const onClearSearch = () => {
setSearchValue("");
setIsLoading(true);
loadNextPage(0, "");
};
if (exists && inGroup) {
newOptions.push(
this.convertUser({
...defaultOption,
displayName: defaultOptionLabel,
})
);
}
} else if (!startIndex && response.items.length > 0 && inGroup) {
newOptions.push(
this.convertUser({
...defaultOption,
displayName: defaultOptionLabel,
})
);
}
newOptions = newOptions.concat(
this.convertUsers(
response.items.filter((item) => item.id !== defaultOption.id)
)
);
} else {
newOptions = newOptions.concat(this.convertUsers(response.items));
}
this.setState({
hasNextPage: newOptions.length < response.total,
isNextPageLoading: false,
options: newOptions,
total: response.total,
});
})
.catch((error) => console.log(error));
return (
<Selector
id={id}
className={className}
style={style}
headerLabel={headerLabel || t("ListAccounts")}
onBackClick={onBackClick}
searchPlaceholder={searchPlaceholder || t("Common:Search")}
searchValue={searchValue}
onSearch={onSearch}
onClearSearch={onClearSearch}
items={itemsList}
isMultiSelect={isMultiSelect}
selectedItems={selectedItems}
acceptButtonLabel={
acceptButtonLabel || t("PeopleTranslations:AddMembers")
}
);
};
getOptionTooltipContent = (index) => {
if (!index) return null;
const { options } = this.state;
const user = options[+index];
if (!user) return null;
// console.log("onOptionTooltipShow", index, user);
const { defaultOption, theme } = this.props;
const label =
defaultOption && defaultOption.id === user.key
? defaultOption.displayName
: user.label;
return (
<UserTooltip
theme={theme}
avatarUrl={user.avatarUrl}
label={label}
email={user.email}
position={user.position}
/>
);
};
onSearchChanged = () => {
//console.log("onSearchChanged")(value);
this.setState({ options: [], hasNextPage: true, isFirstLoad: true });
};
onGroupChanged = () => {
//console.log("onGroupChanged")(group);
this.setState({ options: [], hasNextPage: true, isFirstLoad: true });
};
render() {
const {
options,
groups,
hasNextPage,
isNextPageLoading,
total,
isFirstLoad,
} = this.state;
const {
id,
className,
style,
isOpen,
isMultiSelect,
isDisabled,
onSelect,
size,
onCancel,
t,
searchPlaceHolderLabel,
withoutAside,
embeddedComponent,
selectedOptions,
showCounter,
smallSectionWidth,
theme,
onArrowClick,
headerLabel,
} = this.props;
// console.log("CustomAllGroups", t("CustomAllGroups", { groupsCaption }));
// console.log("PeopleSelector render");
return (
<AdvancedSelector
theme={theme}
id={id}
className={className}
style={style}
options={options}
groups={groups}
hasNextPage={hasNextPage}
isNextPageLoading={isNextPageLoading}
smallSectionWidth={smallSectionWidth}
loadNextPage={this.loadNextPage}
size={size}
selectedOptions={selectedOptions}
isOpen={isOpen}
isMultiSelect={isMultiSelect}
isDisabled={isDisabled}
searchPlaceHolderLabel={
searchPlaceHolderLabel || t("SearchUsersPlaceholder")
}
isDefaultDisplayDropDown={false}
selectButtonLabel={t("PeopleTranslations:AddMembers")}
emptySearchOptionsLabel={t("EmptySearchUsersResult")}
emptyOptionsLabel={t("EmptyUsers")}
onSelect={onSelect}
onSearchChanged={this.onSearchChanged}
onGroupChanged={this.onGroupChanged}
onCancel={onCancel}
withoutAside={withoutAside}
embeddedComponent={embeddedComponent}
showCounter={showCounter}
onArrowClick={onArrowClick}
headerLabel={headerLabel ? headerLabel : `${t("Common:AddUsers")}`}
total={total}
isFirstLoad={isFirstLoad}
/>
);
}
}
PeopleSelector.propTypes = {
id: PropTypes.string,
className: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
style: PropTypes.object,
isOpen: PropTypes.bool,
onSelect: PropTypes.func,
onCancel: PropTypes.func,
useFake: PropTypes.bool,
isMultiSelect: PropTypes.bool,
isDisabled: PropTypes.bool,
defaultOption: PropTypes.object,
defaultOptionLabel: PropTypes.string,
size: PropTypes.oneOf(["full", "compact"]),
language: PropTypes.string,
t: PropTypes.func,
groupsCaption: PropTypes.string,
searchPlaceHolderLabel: PropTypes.string,
role: PropTypes.oneOf(["admin", "user", "guest"]),
employeeStatus: PropTypes.any,
withoutAside: PropTypes.bool,
embeddedComponent: PropTypes.any,
onAccept={onAccept}
withSelectAll={withSelectAll}
selectAllLabel={selectAllLabel || t("AllAccounts")}
selectAllIcon={selectAllIcon}
withAccessRights={withAccessRights}
accessRights={accessRights}
selectedAccessRight={selectedAccessRight}
withCancelButton={withCancelButton}
cancelButtonLabel={cancelButtonLabel || t("Common:CancelButton")}
onCancel={onCancel}
emptyScreenImage={emptyScreenImage}
emptyScreenHeader={emptyScreenHeader || t("EmptyHeader")}
emptyScreenDescription={emptyScreenDescription || t("EmptyDescription")}
searchEmptyScreenImage={searchEmptyScreenImage}
searchEmptyScreenHeader={
searchEmptyScreenHeader || t("SearchEmptyHeader")
}
searchEmptyScreenDescription={
searchEmptyScreenDescription || t("SearchEmptyDescription")
}
hasNextPage={hasNextPage}
isNextPageLoading={isNextPageLoading}
loadNextPage={loadNextPage}
totalItems={total}
isLoading={isLoading}
searchLoader={<Loaders.SelectorSearchLoader />}
rowLoader={
<Loaders.SelectorRowLoader
isMultiSelect={false}
isContainer={isLoading}
isUser={true}
/>
}
/>
);
};
PeopleSelector.propTypes = {};
PeopleSelector.defaultProps = {
useFake: false,
size: "full",
language: "en",
role: null,
employeeStatus: null,
defaultOption: null,
defaultOptionLabel: "Me",
withoutAside: false,
selectAllIcon: "/static/images/catalog.accounts.react.svg",
emptyScreenImage: "/static/images/empty_screen_persons.png",
searchEmptyScreenImage: "/static/images/empty_screen_persons.png",
};
const ExtendedPeopleSelector = inject(({ auth }) => {

View File

@ -0,0 +1,224 @@
import React from "react";
import { withTranslation } from "react-i18next";
import api from "@docspace/common/api";
import RoomsFilter from "@docspace/common/api/rooms/filter";
import { RoomsType } from "@docspace/common/constants";
import Loaders from "@docspace/common/components/Loaders";
import Selector from "@docspace/components/selector";
import { Backdrop } from "@docspace/components";
const pageCount = 100;
const getRoomLogo = (roomType) => {
const path = `images/icons/32`;
switch (roomType) {
case RoomsType.CustomRoom:
return `${path}/room/custom.svg`;
case RoomsType.FillingFormsRoom:
return `${path}/room/filling.form.svg`;
case RoomsType.EditingRoom:
return `${path}/room/editing.svg`;
case RoomsType.ReadOnlyRoom:
return `${path}/room/view.only.svg`;
case RoomsType.ReviewRoom:
return `${path}/room/review.svg`;
}
};
const convertToItems = (folders) => {
const items = folders.map((folder) => {
const { id, title, roomType, logo } = folder;
const icon = logo.original ? logo.original : getRoomLogo(roomType);
return { id, label: title, icon };
});
return items;
};
const RoomSelector = ({
t,
id,
className,
style,
excludeItems,
headerLabel,
onBackClick,
searchPlaceholder,
onSearch,
onClearSearch,
onSelect,
isMultiSelect,
selectedItems,
acceptButtonLabel,
onAccept,
withSelectAll,
selectAllLabel,
selectAllIcon,
onSelectAll,
withAccessRights,
accessRights,
selectedAccessRight,
onAccessRightsChange,
withCancelButton,
cancelButtonLabel,
onCancel,
emptyScreenImage,
emptyScreenHeader,
emptyScreenDescription,
searchEmptyScreenImage,
searchEmptyScreenHeader,
searchEmptyScreenDescription,
}) => {
const [isFirstLoad, setIsFirstLoad] = React.useState(true);
const [searchValue, setSearchValue] = React.useState("");
const [hasNextPage, setHasNextPage] = React.useState(false);
const [isNextPageLoading, setIsNextPageLoading] = React.useState(false);
const [total, setTotal] = React.useState(0);
const [items, setItems] = React.useState([]);
const timeoutRef = React.useRef(null);
const onSearchAction = React.useCallback(
(value) => {
onSearch && onSearch(value);
setSearchValue(() => {
setIsFirstLoad(true);
return value;
});
},
[onSearch]
);
const onClearSearchAction = React.useCallback(() => {
onClearSearch && onClearSearch();
setSearchValue(() => {
setIsFirstLoad(true);
return "";
});
}, [onClearSearch]);
const onLoadNextPage = React.useCallback(
(startIndex) => {
setIsNextPageLoading(true);
const page = startIndex / pageCount;
const filter = RoomsFilter.getDefault();
filter.page = page;
filter.pageCount = pageCount;
filter.filterValue = searchValue ? searchValue : null;
api.rooms
.getRooms(filter)
.then(({ folders, total, count }) => {
const rooms = convertToItems(folders);
const itemList = rooms.filter((x) => !excludeItems.includes(x.id));
setHasNextPage(count === pageCount);
if (isFirstLoad) {
setTotal(total);
setItems(itemList);
} else {
setItems((value) => [...value, ...itemList]);
}
})
.finally(() => {
if (isFirstLoad) {
setTimeout(() => {
setIsFirstLoad(false);
}, 500);
}
setIsNextPageLoading(false);
});
},
[isFirstLoad, excludeItems, searchValue]
);
React.useEffect(() => {
onLoadNextPage(0);
}, [searchValue]);
return (
<Selector
id={id}
className={className}
style={style}
headerLabel={headerLabel || t("RoomList")}
onBackClick={onBackClick}
searchPlaceholder={searchPlaceholder || t("Common:Search")}
searchValue={searchValue}
onSearch={onSearchAction}
onClearSearch={onClearSearchAction}
onSelect={onSelect}
items={items}
acceptButtonLabel={acceptButtonLabel || t("Common:SelectAction")}
onAccept={onAccept}
withCancelButton={withCancelButton}
cancelButtonLabel={cancelButtonLabel || t("Common:CancelButton")}
onCancel={onCancel}
isMultiSelect={isMultiSelect}
selectedItems={selectedItems}
withSelectAll={withSelectAll}
selectAllLabel={selectAllLabel}
selectAllIcon={selectAllIcon}
onSelectAll={onSelectAll}
withAccessRights={withAccessRights}
accessRights={accessRights}
selectedAccessRight={selectedAccessRight}
onAccessRightsChange={onAccessRightsChange}
emptyScreenImage={emptyScreenImage || "images/empty_screen_corporate.png"}
emptyScreenHeader={emptyScreenHeader || t("EmptyRoomsHeader")}
emptyScreenDescription={
emptyScreenDescription || t("EmptyRoomsDescription")
}
searchEmptyScreenImage={
searchEmptyScreenImage || "images/empty_screen_corporate.png"
}
searchEmptyScreenHeader={
searchEmptyScreenHeader || t("SearchEmptyRoomsHeader")
}
searchEmptyScreenDescription={
searchEmptyScreenDescription || t("SearchEmptyRoomsDescription")
}
totalItems={total}
hasNextPage={hasNextPage}
isNextPageLoading={isNextPageLoading}
loadNextPage={onLoadNextPage}
isLoading={isFirstLoad}
searchLoader={<Loaders.SelectorSearchLoader />}
rowLoader={
<Loaders.SelectorRowLoader
isMultiSelect={isMultiSelect}
isContainer={isFirstLoad}
isUser={false}
/>
}
/>
);
};
RoomSelector.defaultProps = { excludeItems: [] };
export default withTranslation(["RoomSelector", "Common"])(RoomSelector);

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { useState, useEffect } from "react";
import { inject, observer } from "mobx-react";
import PropTypes from "prop-types";
import Backdrop from "@docspace/components/backdrop";
@ -8,64 +8,51 @@ import IconButton from "@docspace/components/icon-button";
import { ShareAccessRights } from "@docspace/common/constants";
import PeopleSelector from "@docspace/client/src/components/PeopleSelector";
import { withTranslation } from "react-i18next";
import {
StyledAddUsersPanelPanel,
StyledContent,
StyledHeaderContent,
StyledBody,
} from "../StyledPanels";
import AccessComboBox from "../SharingPanel/AccessComboBox";
import Loaders from "@docspace/common/components/Loaders";
import withLoader from "../../../HOCs/withLoader";
class AddUsersPanelComponent extends React.Component {
constructor(props) {
super(props);
const AddUsersPanel = ({
isEncrypted,
onClose,
onParentPanelClose,
shareDataItems,
setShareDataItems,
t,
visible,
groupsCaption,
accessOptions,
isMultiSelect,
theme,
}) => {
const accessRight = isEncrypted
? ShareAccessRights.FullAccess
: ShareAccessRights.ReadOnly;
const accessRight = props.isEncrypted
? ShareAccessRights.FullAccess
: ShareAccessRights.ReadOnly;
const onArrowClick = () => onClose();
this.state = {
showActionPanel: false,
accessRight,
};
this.scrollRef = React.createRef();
}
onPlusClick = () =>
this.setState({ showActionPanel: !this.state.showActionPanel });
onArrowClick = () => this.props.onClose();
onKeyPress = (event) => {
if (event.key === "Esc" || event.key === "Escape") {
this.props.onClose();
}
const onKeyPress = (e) => {
if (e.key === "Esc" || e.key === "Escape") onClose();
};
onClosePanels = () => {
this.props.onClose();
this.props.onSharingPanelClose();
useEffect(() => {
window.addEventListener("keyup", onKeyPress);
return () => window.removeEventListener("keyup", onKeyPress);
});
const onClosePanels = () => {
onClose();
onParentPanelClose();
};
onPeopleSelect = (users) => {
const { shareDataItems, setShareDataItems, onClose } = this.props;
const onPeopleSelect = (users) => {
const items = shareDataItems;
for (let item of users) {
const groups = item?.groups.map((group) => ({
id: group,
}));
if (item.key) {
item.id = item.key;
item.groups = groups;
}
const currentItem = shareDataItems.find((x) => x.sharedTo.id === item.id);
if (!currentItem) {
const newItem = {
access: this.state.accessRight,
access: accessRight,
isLocked: false,
isOwner: false,
sharedTo: item,
@ -78,8 +65,7 @@ class AddUsersPanelComponent extends React.Component {
onClose();
};
onOwnerSelect = (owner) => {
const { setShareDataItems, shareDataItems, onClose } = this.props;
const onOwnerSelect = (owner) => {
const ownerItem = shareDataItems.find((x) => x.isOwner);
ownerItem.sharedTo = owner[0];
@ -91,115 +77,54 @@ class AddUsersPanelComponent extends React.Component {
onClose();
};
componentDidMount() {
const scroll = this.scrollRef.current.getElementsByClassName("scroll-body");
setTimeout(() => scroll[1] && scroll[1].focus(), 2000);
window.addEventListener("keyup", this.onKeyPress);
}
const accessRights = accessOptions.map((access) => {
return {
key: access,
label: t(access),
};
});
componentWillUnmount() {
window.removeEventListener("keyup", this.onKeyPress);
}
const selectedAccess = accesses.filter(
(access) => access.key === "Review"
)[0];
onAccessChange = (e) => {
const accessRight = +e.currentTarget.dataset.access;
this.setState({ accessRight });
};
render() {
const {
t,
visible,
groupsCaption,
accessOptions,
isMultiSelect,
theme,
shareDataItems,
} = this.props;
const { accessRight } = this.state;
const selectedOptions = [];
shareDataItems.forEach((item) => {
const { sharedTo } = item;
if (item.isUser) {
const groups = sharedTo?.groups
? sharedTo.groups.map((group) => group.id)
: [];
selectedOptions.push({ key: sharedTo.id, id: sharedTo.id, groups });
}
});
const zIndex = 310;
const embeddedComponent = isMultiSelect
? {
embeddedComponent: (
<AccessComboBox
t={t}
access={accessRight}
directionX="right"
directionY="top"
onAccessChange={this.onAccessChange}
accessOptions={accessOptions}
arrowIconColor={theme.filesPanels.addUsers.arrowColor}
isEmbedded={true}
/>
),
}
: null;
//console.log("AddUsersPanel render");
return (
<StyledAddUsersPanelPanel visible={visible}>
<Backdrop
onClick={this.onClosePanels}
visible={visible}
zIndex={zIndex}
isAside={true}
return (
<div visible={visible}>
<Backdrop
onClick={onClosePanels}
visible={visible}
zIndex={310}
isAside={true}
/>
<Aside
className="header_aside-panel"
visible={visible}
onClose={onClosePanels}
>
<PeopleSelector
isMultiSelect={isMultiSelect}
onAccept={isMultiSelect ? onPeopleSelect : onOwnerSelect}
onBackClick={onArrowClick}
headerLabel={
isMultiSelect
? t("Common:AddUsers")
: t("PeopleTranslations:OwnerChange")
}
accessRights={accessRights}
selectedAccessRight={selectedAccess}
onCancel={onClosePanels}
withCancelButton={!isMultiSelect}
withAccessRights={isMultiSelect}
withSelectAll={isMultiSelect}
/>
<Aside
className="header_aside-panel"
visible={visible}
onClose={this.onClosePanels}
>
<StyledContent>
<StyledBody ref={this.scrollRef}>
<PeopleSelector
className="peopleSelector"
role={isMultiSelect ? null : "user"}
employeeStatus={1}
displayType="aside"
withoutAside
isOpen={visible}
isMultiSelect={isMultiSelect}
onSelect={
isMultiSelect ? this.onPeopleSelect : this.onOwnerSelect
}
{...embeddedComponent}
selectedOptions={selectedOptions}
groupsCaption={groupsCaption}
showCounter
onArrowClick={this.onArrowClick}
headerLabel={
isMultiSelect
? t("Common:AddUsers")
: t("PeopleTranslations:OwnerChange")
}
//onCancel={onClose}
/>
</StyledBody>
</StyledContent>
</Aside>
</StyledAddUsersPanelPanel>
);
}
}
</Aside>
</div>
);
};
AddUsersPanelComponent.propTypes = {
AddUsersPanel.propTypes = {
visible: PropTypes.bool,
onSharingPanelClose: PropTypes.func,
onParentPanelClose: PropTypes.func,
onClose: PropTypes.func,
};
@ -208,7 +133,7 @@ export default inject(({ auth }) => {
})(
observer(
withTranslation(["SharingPanel", "PeopleTranslations", "Common"])(
withLoader(AddUsersPanelComponent)(<Loaders.DialogAsideLoader isPanel />)
withLoader(AddUsersPanel)(<Loaders.DialogAsideLoader isPanel />)
)
)
);

View File

@ -773,7 +773,7 @@ class SharingPanelComponent extends React.Component {
{showAddUsersPanel && (
<AddUsersPanel
onSharingPanelClose={this.onClose}
onParentPanelClose={this.onClose}
onClose={this.onShowUsersPanel}
visible={showAddUsersPanel}
shareDataItems={filteredShareDataItems}
@ -799,7 +799,7 @@ class SharingPanelComponent extends React.Component {
{showChangeOwnerPanel && (
<AddUsersPanel
onSharingPanelClose={this.onClose}
onParentPanelClose={this.onClose}
onClose={this.onShowChangeOwnerPanel}
visible={showChangeOwnerPanel}
shareDataItems={filteredShareDataItems}

View File

@ -23,19 +23,6 @@ const PanelStyles = css`
}
}
.groupSelector,
.peopleSelector {
.combo-buttons_arrow-icon {
flex: 0 0 6px;
width: 6px;
margin-top: auto;
margin-bottom: auto;
display: flex;
justify-content: center;
align-items: center;
}
}
.footer {
padding: 16px;
width: 100%;
@ -137,13 +124,6 @@ const StyledVersionHistoryPanel = styled.div`
StyledVersionHistoryPanel.defaultProps = { theme: Base };
const StyledAddUsersPanelPanel = styled.div`
${PanelStyles}
.combo-button-label {
font-size: 14px;
}
`;
const StyledAddGroupsPanel = styled.div`
${PanelStyles}
.combo-button-label {
@ -695,7 +675,6 @@ StyledModalRowContainer.defaultProps = { theme: Base };
export {
StyledAsidePanel,
StyledAddGroupsPanel,
StyledAddUsersPanelPanel,
StyledEmbeddingPanel,
StyledVersionHistoryPanel,
StyledContent,

View File

@ -339,21 +339,17 @@ const FilterBlock = ({
{showSelector.isAuthor ? (
<PeopleSelector
className="people-selector"
isOpen={showSelector.show}
withoutAside={true}
isMultiSelect={false}
onSelect={selectOption}
onArrowClick={onArrowClick}
onAccept={selectOption}
onBackClick={onArrowClick}
headerLabel={selectorLabel}
/>
) : (
<GroupSelector
className="people-selector"
isOpen={showSelector.show}
withoutAside={true}
isMultiSelect={false}
onSelect={selectOption}
onArrowClick={onArrowClick}
onAccept={selectOption}
onBackClick={onArrowClick}
headerLabel={selectorLabel}
/>
)}

View File

@ -0,0 +1,94 @@
import React from "react";
import styled, { css } from "styled-components";
import RectangleLoader from "../RectangleLoader/RectangleLoader";
const StyledContainer = styled.div`
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
`;
const StyledItem = styled.div`
width: 100%;
height: 48px;
min-height: 48px;
padding: 0 16px;
box-sizing: border-box;
display: flex;
align-items: center;
.avatar {
margin-right: 8px;
${(props) =>
props.isUser &&
css`
border-radius: 50px;
`}
}
.checkbox {
margin-left: auto;
}
`;
const SelectorRowLoader = ({
id,
className,
style,
isMultiSelect,
isContainer,
isUser,
...rest
}) => {
const getRowItem = (key) => {
return (
<StyledItem
id={id}
className={className}
style={style}
isMultiSelect={isMultiSelect}
isUser={isUser}
key={key}
{...rest}
>
<RectangleLoader className={"avatar"} width={"32px"} height={"32px"} />
<RectangleLoader className={"text"} width={"212px"} height={"16px"} />
{isMultiSelect && (
<RectangleLoader
className={"checkbox"}
width={"16px"}
height={"16px"}
/>
)}
</StyledItem>
);
};
const getRowItems = () => {
const rows = [];
for (let i = 0; i < 20; i++) {
rows.push(getRowItem(i));
}
return rows;
};
return isContainer ? (
<StyledContainer id={id} className={className} style={style} {...rest}>
{getRowItems()}
</StyledContainer>
) : (
getRowItem()
);
};
export default SelectorRowLoader;

View File

@ -0,0 +1,23 @@
import React from "react";
import PropTypes from "prop-types";
import RectangleLoader from "../RectangleLoader/RectangleLoader";
const SelectorSearchLoader = ({
id,
className,
style,
...rest
}) => {
return (
<RectangleLoader
width={"calc(100% - 16px)"}
height={"32px"}
style={{ padding: "0 0 0 16px", marginBottom: "8px", ...style }}
{...rest}
/>
);
};
export default SelectorSearchLoader;

View File

@ -34,6 +34,8 @@ import CreateEditRoomDilogHeaderLoader from "./CreateEditRoomLoader/CreateEditRo
import DataBackupLoader from "./DataBackupLoader";
import AutoBackupLoader from "./AutoBackupLoader";
import RestoreBackupLoader from "./RestoreBackupLoader";
import SelectorSearchLoader from "./SelectorSearchLoader";
import SelectorRowLoader from "./SelectorRowLoader";
export default {
Rectangle,
@ -72,4 +74,6 @@ export default {
DataBackupLoader,
AutoBackupLoader,
RestoreBackupLoader,
SelectorSearchLoader,
SelectorRowLoader,
};

View File

@ -64,3 +64,4 @@ export * as Themes from "./themes";
export { default as Portal } from "./portal";
export { default as TableContainer } from "./table-container";
export { default as Slider } from "./slider";
export { default as Selector } from "./Selector";

View File

@ -2,11 +2,13 @@ export const parseChildren = (
children,
headerDisplayName,
bodyDisplayName,
footerDisplayName
footerDisplayName,
containerDisplayName
) => {
let header = null,
body = null,
footer = null;
footer = null,
container = null;
children.forEach((child) => {
const childType =
@ -22,9 +24,12 @@ export const parseChildren = (
case footerDisplayName:
footer = child;
break;
case containerDisplayName:
container = child;
break;
default:
break;
}
});
return [header, body, footer];
return [header, body, footer, container];
};

View File

@ -19,6 +19,9 @@ Body.displayName = "DialogBody";
const Footer = () => null;
Footer.displayName = "DialogFooter";
const Container = () => null;
Container.displayName = "DialogContainer";
const ModalDialog = ({
id,
style,
@ -69,11 +72,12 @@ const ModalDialog = ({
};
}, []);
const [header, body, footer] = parseChildren(
const [header, body, footer, container] = parseChildren(
children,
Header.displayName,
Body.displayName,
Footer.displayName
Footer.displayName,
Container.displayName
);
return (
@ -96,6 +100,7 @@ const ModalDialog = ({
header={header}
body={body}
footer={footer}
container={container}
visible={visible}
modalSwipeOffset={modalSwipeOffset}
/>
@ -173,5 +178,6 @@ ModalDialog.defaultProps = {
ModalDialog.Header = Header;
ModalDialog.Body = Body;
ModalDialog.Footer = Footer;
ModalDialog.Container = Container;
export default ModalDialog;

View File

@ -55,6 +55,11 @@ const Template = ({ onOk, ...args }) => {
onClick={closeModal}
/>
</ModalDialog.Footer>
<ModalDialog.Container>
<div style={{ width: "100%", height: "100%", background: "red" }}>
123
</div>
</ModalDialog.Container>
</ModalDialog>
</>
);
@ -68,7 +73,7 @@ export const Default = Template.bind({});
Default.args = {
displayType: "aside",
displayTypeDetailed: {
desktop: "modal",
desktop: "aside",
tablet: "aside",
smallTablet: "modal",
mobile: "aside",

View File

@ -32,6 +32,7 @@ const Modal = ({
header,
body,
footer,
container,
visible,
withFooterBorder,
modalSwipeOffset,
@ -39,6 +40,7 @@ const Modal = ({
const headerComponent = header ? header.props.children : null;
const bodyComponent = body ? body.props.children : null;
const footerComponent = footer ? footer.props.children : null;
const containerComponent = container ? container.props.children : null;
const validateOnMouseDown = (e) => {
if (e.target.id === "modal-onMouseDown-close") onClose();
@ -90,54 +92,60 @@ const Modal = ({
)
) : (
<>
{header && (
<StyledHeader
id="modal-header-swipe"
className={`modal-header ${header.props.className}`}
currentDisplayType={currentDisplayType}
{...header.props}
>
<Heading
level={1}
className={"heading"}
size="medium"
truncate={true}
>
{headerComponent}
</Heading>
</StyledHeader>
)}
{body && (
<StyledBody
className={`modal-body ${body.props.className}`}
withBodyScroll={withBodyScroll}
isScrollLocked={isScrollLocked}
hasFooter={1 && footer}
currentDisplayType={currentDisplayType}
{...body.props}
>
{currentDisplayType === "aside" && withBodyScroll ? (
<Scrollbar
stype="mediumBlack"
id="modal-scroll"
className="modal-scroll"
{container && currentDisplayType !== "modal" ? (
<>{containerComponent}</>
) : (
<>
{header && (
<StyledHeader
id="modal-header-swipe"
className={`modal-header ${header.props.className}`}
currentDisplayType={currentDisplayType}
{...header.props}
>
{bodyComponent}
</Scrollbar>
) : (
bodyComponent
<Heading
level={1}
className={"heading"}
size="medium"
truncate={true}
>
{headerComponent}
</Heading>
</StyledHeader>
)}
</StyledBody>
)}
{footer && (
<StyledFooter
className={`modal-footer ${footer.props.className}`}
withFooterBorder={withFooterBorder}
currentDisplayType={currentDisplayType}
{...footer.props}
>
{footerComponent}
</StyledFooter>
{body && (
<StyledBody
className={`modal-body ${body.props.className}`}
withBodyScroll={withBodyScroll}
isScrollLocked={isScrollLocked}
hasFooter={1 && footer}
currentDisplayType={currentDisplayType}
{...body.props}
>
{currentDisplayType === "aside" && withBodyScroll ? (
<Scrollbar
stype="mediumBlack"
id="modal-scroll"
className="modal-scroll"
>
{bodyComponent}
</Scrollbar>
) : (
bodyComponent
)}
</StyledBody>
)}
{footer && (
<StyledFooter
className={`modal-footer ${footer.props.className}`}
withFooterBorder={withFooterBorder}
currentDisplayType={currentDisplayType}
{...footer.props}
>
{footerComponent}
</StyledFooter>
)}
</>
)}
</>
)}

View File

@ -0,0 +1,11 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4501_40546)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.0002 8C16.3903 8 16.7449 8.22689 16.9083 8.58115L18.6488 12.3546L22.7753 12.8438C23.1628 12.8898 23.4881 13.1568 23.6087 13.5279C23.7292 13.8989 23.623 14.3062 23.3366 14.5711L20.2857 17.3924L21.0955 21.4682C21.1716 21.8509 21.0181 22.2428 20.7025 22.4721C20.3869 22.7015 19.9667 22.7263 19.6263 22.5357L16.0002 20.506L12.3741 22.5357C12.0337 22.7263 11.6135 22.7015 11.2979 22.4721C10.9823 22.2428 10.8288 21.8509 10.9049 21.4682L11.7147 17.3924L8.66386 14.5711C8.37743 14.3062 8.27119 13.8989 8.39175 13.5279C8.51231 13.1568 8.83764 12.8898 9.22507 12.8438L13.3516 12.3546L15.0921 8.58115C15.2556 8.22689 15.6101 8 16.0002 8ZM16.0002 11.3875L14.9333 13.7005C14.7876 14.0164 14.4883 14.2338 14.143 14.2747L11.6135 14.5747L13.4836 16.3041C13.7389 16.5402 13.8533 16.8921 13.7855 17.2332L13.2891 19.7316L15.5118 18.4874C15.8152 18.3175 16.1852 18.3175 16.4886 18.4874L18.7114 19.7316L18.2149 17.2332C18.1472 16.8921 18.2615 16.5402 18.5168 16.3041L20.3869 14.5747L17.8574 14.2747C17.5121 14.2338 17.2128 14.0164 17.0671 13.7005L16.0002 11.3875Z" fill="#F2557C"/>
</g>
<path d="M6 2H26V-2H6V2ZM30 6V26H34V6H30ZM26 30H6V34H26V30ZM2 26V6H-2V26H2ZM6 30C3.79086 30 2 28.2091 2 26H-2C-2 30.4183 1.58172 34 6 34V30ZM30 26C30 28.2091 28.2091 30 26 30V34C30.4183 34 34 30.4183 34 26H30ZM26 2C28.2091 2 30 3.79086 30 6H34C34 1.58172 30.4183 -2 26 -2V2ZM6 -2C1.58172 -2 -2 1.58172 -2 6H2C2 3.79086 3.79086 2 6 2V-2Z" fill="#F2557C"/>
<defs>
<clipPath id="clip0_4501_40546">
<rect width="16" height="16" fill="white" transform="translate(8 8)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,11 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4529_34510)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.2929 9.29312C19.5119 8.07417 21.4882 8.07417 22.7071 9.29312C23.9261 10.5121 23.9261 12.4884 22.7071 13.7073L13.7071 22.7073C13.579 22.8355 13.4184 22.9264 13.2426 22.9704L9.24256 23.9704C8.90178 24.0556 8.54129 23.9557 8.29291 23.7073C8.04453 23.459 7.94468 23.0985 8.02988 22.7577L9.02988 18.7577C9.07384 18.5819 9.16476 18.4213 9.29291 18.2931L18.2929 9.29312ZM21.2929 10.7073C20.855 10.2694 20.145 10.2694 19.7071 10.7073L18.9142 11.5002L20.5 13.086L21.2929 12.2931C21.7308 11.8552 21.7308 11.1452 21.2929 10.7073ZM19.0858 14.5002L17.5 12.9144L10.903 19.5115L10.3744 21.6259L12.4888 21.0973L19.0858 14.5002Z" fill="#EB7B0C"/>
</g>
<path d="M6 2H26V-2H6V2ZM30 6V26H34V6H30ZM26 30H6V34H26V30ZM2 26V6H-2V26H2ZM6 30C3.79086 30 2 28.2091 2 26H-2C-2 30.4183 1.58172 34 6 34V30ZM30 26C30 28.2091 28.2091 30 26 30V34C30.4183 34 34 30.4183 34 26H30ZM26 2C28.2091 2 30 3.79086 30 6H34C34 1.58172 30.4183 -2 26 -2V2ZM6 -2C1.58172 -2 -2 1.58172 -2 6H2C2 3.79086 3.79086 2 6 2V-2Z" fill="#EB7B0C"/>
<defs>
<clipPath id="clip0_4529_34510">
<rect width="16" height="16" fill="white" transform="translate(8 8)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 9C9.89543 9 9 9.89543 9 11V21C9 22.1046 9.89543 23 11 23H21C22.1046 23 23 22.1046 23 21V11C23 9.89543 22.1046 9 21 9H11ZM11 11L21 11V21H11V11ZM12 14H20V12H12V14ZM12 17H20V15H12V17ZM16 20H20V18H16V20Z" fill="#26ACB8"/>
<path d="M6 2H26V-2H6V2ZM30 6V26H34V6H30ZM26 30H6V34H26V30ZM2 26V6H-2V26H2ZM6 30C3.79086 30 2 28.2091 2 26H-2C-2 30.4183 1.58172 34 6 34V30ZM30 26C30 28.2091 28.2091 30 26 30V34C30.4183 34 34 30.4183 34 26H30ZM26 2C28.2091 2 30 3.79086 30 6H34C34 1.58172 30.4183 -2 26 -2V2ZM6 -2C1.58172 -2 -2 1.58172 -2 6H2C2 3.79086 3.79086 2 6 2V-2Z" fill="#26ACB8"/>
</svg>

After

Width:  |  Height:  |  Size: 729 B

View File

@ -0,0 +1,11 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4529_37943)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 11L16.7574 11L18.7574 9H10C8.89543 9 8 9.89543 8 11V21.5858C8 23.3676 10.1543 24.2599 11.4142 23L13.4142 21H22C23.1046 21 24 20.1046 24 19V12.2426L22 14.2426V19H13.4142C12.8838 19 12.3751 19.2107 12 19.5858L10 21.5858V11ZM22.7071 10.7071L16.2071 17.2071C15.8166 17.5976 15.1834 17.5976 14.7929 17.2071L11.7929 14.2071L13.2071 12.7929L15.5 15.0858L21.2929 9.29289L22.7071 10.7071Z" fill="#CC5BCC"/>
</g>
<path d="M6 2H26V-2H6V2ZM30 6V26H34V6H30ZM26 30H6V34H26V30ZM2 26V6H-2V26H2ZM6 30C3.79086 30 2 28.2091 2 26H-2C-2 30.4183 1.58172 34 6 34V30ZM30 26C30 28.2091 28.2091 30 26 30V34C30.4183 34 34 30.4183 34 26H30ZM26 2C28.2091 2 30 3.79086 30 6H34C34 1.58172 30.4183 -2 26 -2V2ZM6 -2C1.58172 -2 -2 1.58172 -2 6H2C2 3.79086 3.79086 2 6 2V-2Z" fill="#CC5BCC"/>
<defs>
<clipPath id="clip0_4529_37943">
<rect width="16" height="16" fill="white" transform="translate(8 8)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4995 18.4663C11.4719 17.6138 10.664 16.6128 10.2152 16C10.664 15.3872 11.4719 14.3862 12.4995 13.5337C13.5835 12.6344 14.7913 12 16 12C17.2087 12 18.4165 12.6344 19.5005 13.5337C20.5281 14.3862 21.336 15.3872 21.7848 16C21.336 16.6128 20.5281 17.6138 19.5005 18.4663C18.4165 19.3656 17.2087 20 16 20C14.7913 20 13.5835 19.3656 12.4995 18.4663ZM16 10C14.1116 10 12.4571 10.9702 11.2225 11.9944C9.97234 13.0316 9.02832 14.2275 8.54251 14.8996C8.06525 15.5599 8.06526 16.4401 8.54251 17.1004C9.02832 17.7725 9.97234 18.9684 11.2225 20.0056C12.4571 21.0298 14.1116 22 16 22C17.8884 22 19.5429 21.0298 20.7775 20.0056C22.0277 18.9684 22.9717 17.7725 23.4575 17.1004C23.9348 16.4401 23.9348 15.5599 23.4575 14.8996C22.9717 14.2275 22.0277 13.0316 20.7775 11.9944C19.5429 10.9702 17.8884 10 16 10ZM16 14C14.8954 14 14 14.8954 14 16C14 17.1046 14.8954 18 16 18C17.1046 18 18 17.1046 18 16C18 14.8954 17.1046 14 16 14Z" fill="#388BDE"/>
<path d="M6 2H26V-2H6V2ZM30 6V26H34V6H30ZM26 30H6V34H26V30ZM2 26V6H-2V26H2ZM6 30C3.79086 30 2 28.2091 2 26H-2C-2 30.4183 1.58172 34 6 34V30ZM30 26C30 28.2091 28.2091 30 26 30V34C30.4183 34 34 30.4183 34 26H30ZM26 2C28.2091 2 30 3.79086 30 6H34C34 1.58172 30.4183 -2 26 -2V2ZM6 -2C1.58172 -2 -2 1.58172 -2 6H2C2 3.79086 3.79086 2 6 2V-2Z" fill="#388BDE"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,86 @@
# SelectedItem
### Usage
```js
import Selector from "@docspace/components/selector";
```
```jsx
<Selector
acceptButtonLabel="Add"
accessRights={[]}
cancelButtonLabel="Cancel"
emptyScreenDescription="The list of users previously invited to DocSpace or separate rooms will appear here. You will be able to invite these users for collaboration at any time."
emptyScreenHeader="No other accounts here yet"
emptyScreenImage="static/images/empty_screen_filter.png"
hasNextPage={false}
headerLabel="Room list"
items={[]}
isLoading={false}
loadNextPage={() => {}}
onAccept={function noRefCheck() {}}
onAccessRightsChange={function noRefCheck() {}}
onBackClick={function noRefCheck() {}}
onCancel={function noRefCheck() {}}
onClearSearch={function noRefCheck() {}}
onSearch={function noRefCheck() {}}
onSelect={function noRefCheck() {}}
onSelectAll={function noRefCheck() {}}
rowLoader={<></>}
searchEmptyScreenDescription=" SEARCH !!! The list of users previously invited to DocSpace or separate rooms will appear here. You will be able to invite these users for collaboration at any time."
searchEmptyScreenHeader="No other accounts here yet search"
searchEmptyScreenImage="static/images/empty_screen_filter.png"
searchLoader={<></>}
searchPlaceholder="Search"
searchValue=""
selectAllIcon="static/images/room.archive.svg"
selectAllLabel="All accounts"
selectedAccessRight={{}}
selectedItems={[]}
totalItems={0}
/>
```
### Properties
| Props | Type | Required | Values | Default | Description |
| ------------------------------ | :------------: | :------: | :----: | :-----: | ------------------------------------------------------------- |
| `id` | `string` | - | - | - | Accepts id |
| `className` | `string` | - | - | - | Accepts class |
| `style` | `obj`, `array` | - | - | - | Accepts css style |
| `headerLabel` | `string` | - | - | - | Selector header text |
| `onBackClick` | `func` | - | - | - | What the header arrow will trigger when clicked |
| `searchPlaceholder` | `string` | - | - | - | Placeholder for search input |
| `onSearch` | `func` | - | - | - | What the search input will trigger when user stopped typing |
| `onClearSearch` | `func` | - | - | - | What the clear icon of search input will trigger when clicked |
| `items` | `array` | - | - | - | Displaying items |
| `onSelect` | `func` | - | - | - | What the item will trigger when clicked |
| `isMultiSelect` | `bool` | - | - | false | Allows you to select multiple objects |
| `selectedItems` | `array` | - | - | [] | Tells when the items should present a checked state |
| `acceptButtonLabel` | `string` | - | - | - | Accept button text |
| `onAccept` | `func` | - | - | - | What the accept button will trigger when clicked |
| `withSelectAll` | `bool` | - | - | false | Add option for select all items |
| `selectAllLabel` | `string` | - | - | - | Text for select all component |
| `selectAllIcon` | `string` | - | - | - | Icon for select all component |
| `onSelectAll` | `func` | - | - | - | What the select all will trigger when clicked |
| `withAccessRights` | `bool` | - | - | false | Add combobox for displaying access rights at footer |
| `accessRights` | `array` | - | - | - | Access rights items |
| `selectedAccessRight` | `object` | - | - | - | Selected access rights items |
| `onAccessRightsChange` | `func` | - | - | - | What the access right will trigger when changed |
| `withCancelButton` | `bool` | - | - | false | Add cancel button at footer |
| `cancelButtonLabel` | `string` | - | - | - | Displaying text at cancel button |
| `onCancel` | `func` | - | - | - | What the cancel button will trigger when clicked |
| `emptyScreenImage` | `string` | - | - | - | Image for default empty screen |
| `emptyScreenHeader` | `string` | - | - | - | Header for default empty screen |
| `emptyScreenDescription` | `string` | - | - | - | Description for default empty screen |
| `searchEmptyScreenImage` | `string` | - | - | - | Image for search empty screen |
| `searchEmptyScreenHeader` | `string` | - | - | - | Header for search empty screen |
| `searchEmptyScreenDescription` | `string` | - | - | - | Description for search empty screen |
| `totalItems` | `number` | - | - | - | Count items for infinity scroll |
| `hasNextPage` | `bool` | - | - | - | Has next page for infinity scroll |
| `isNextPageLoading` | `bool` | - | - | - | Tells when next page already loading |
| `loadNextPage` | `func` | - | - | - | Function for load next page |
| `isLoading` | `bool` | - | - | - | Set loading state for select |
| `searchLoader` | `node` | - | - | - | Loader element for search block |
| `rowLoader` | `node` | - | - | - | Loader element for item |

View File

@ -0,0 +1,182 @@
import React from "react";
import styled from "styled-components";
import Selector from "./";
const StyledRowLoader = styled.div`
width: 100%;
height: 48px;
background: red;
`;
const StyledSearchLoader = styled.div`
width: 100%;
height: 32px;
background: black;
`;
export default {
title: "Components/Selector",
component: Selector,
parameters: {
docs: {
description: {
component:
"Selector for displaying items list of people or room selector",
},
},
},
};
function makeName() {
var result = "";
var characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var charactersLength = characters.length;
for (var i = 0; i < 15; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
const getItems = (count) => {
const items = [];
for (let i = 0; i < count / 2; i++) {
items.push({
key: `user_${i}`,
id: `user_${i}`,
label: makeName() + " " + i,
avatar: "static/images/room.archive.svg",
});
}
for (let i = 0; i < count / 2; i++) {
items.push({
key: `room_${i}`,
id: `room_${i}`,
label: makeName() + " " + i + " room",
icon: "static/images/icons/32/rooms/custom.svg",
});
}
return items;
};
const getAccessRights = () => {
const accesses = [
{
key: "roomManager",
label: "Room manager",
access: 0,
},
{
key: "editor",
label: "Editor",
access: 1,
},
{
key: "formFiller",
label: "Form filler",
access: 2,
},
{
key: "reviewer",
label: "Reviewer",
access: 3,
},
{
key: "commentator",
label: "Commentator",
access: 4,
},
{
key: "viewer",
label: "Viewer",
access: 5,
},
];
return accesses;
};
const items = getItems(100000);
const selectedItems = [items[0], items[3], items[7]];
const accessRights = getAccessRights();
const selectedAccessRight = accessRights[1];
const renderedItems = items.slice(0, 100);
const totalItems = items.length;
const Template = (args) => {
const [rendItems, setRendItems] = React.useState(renderedItems);
const loadNextPage = (index) => {
setRendItems((val) => [...val, ...items.slice(index, index + 100)]);
};
const rowLoader = <StyledRowLoader />;
const searchLoader = <StyledSearchLoader className="search-loader" />;
return (
<div
style={{
width: "480px",
height: args.height,
border: "1px solid red",
margin: "auto",
}}
>
<Selector
{...args}
items={rendItems}
loadNextPage={loadNextPage}
searchLoader={searchLoader}
rowLoader={rowLoader}
/>
</div>
);
};
export const Default = Template.bind({});
Default.args = {
height: "485px", // container height
headerLabel: "Room list",
onBackClick: () => console.log("back click"),
searchPlaceholder: "Search",
searchValue: "",
items: renderedItems,
onSelect: (item) => console.log("select " + item),
isMultiSelect: false,
selectedItems: selectedItems,
acceptButtonLabel: "Add",
onAccept: (items, access) => console.log("accept " + items + access),
withSelectAll: false,
selectAllLabel: "All accounts",
selectAllIcon: "static/images/room.archive.svg",
onSelectAll: () => console.log("select all"),
withAccessRights: false,
accessRights,
selectedAccessRight,
onAccessRightsChange: (access) =>
console.log("access rights change " + access),
withCancelButton: false,
cancelButtonLabel: "Cancel",
onCancel: () => console.log("cancel"),
emptyScreenImage: "static/images/empty_screen_filter.png",
emptyScreenHeader: "No other accounts here yet",
emptyScreenDescription:
"The list of users previously invited to DocSpace or separate rooms will appear here. You will be able to invite these users for collaboration at any time.",
searchEmptyScreenImage: "static/images/empty_screen_filter.png",
searchEmptyScreenHeader: "No other accounts here yet search",
searchEmptyScreenDescription:
" SEARCH !!! The list of users previously invited to DocSpace or separate rooms will appear here. You will be able to invite these users for collaboration at any time.",
totalItems,
hasNextPage: true,
isNextPageLoading: false,
isLoading: false,
};

View File

@ -0,0 +1,94 @@
import styled, { css } from "styled-components";
import Base from "../themes/base";
const StyledSelector = styled.div`
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
`;
StyledSelector.defaultProps = { theme: Base };
const StyledSelectorHeader = styled.div`
width: calc(100% - 32px);
min-height: 53px;
height: 53px;
max-height: 53px;
padding: 0 16px;
border-bottom: ${(props) => props.theme.selector.border};
display: flex;
align-items: center;
.arrow-button {
cursor: pointer;
margin-right: 12px;
}
.heading-text {
font-weight: 700;
font-size: 21px;
line-height: 28px;
}
`;
StyledSelectorHeader.defaultProps = { theme: Base };
const StyledSelectorBody = styled.div`
width: 100%;
height: ${(props) =>
props.footerVisible
? `calc(100% - 16px - ${props.footerHeight}px - ${props.headerHeight}px)`
: `calc(100% - 16px - ${props.headerHeight}px)`};
padding: 16px 0 0 0;
.search-input,
.search-loader {
padding: 0 16px;
margin-bottom: 12px;
}
`;
StyledSelectorBody.defaultProps = { theme: Base };
const StyledSelectorFooter = styled.div`
width: calc(100% - 32px);
max-height: 73px;
height: 73px;
min-height: 73px;
padding: 0 16px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
border-top: ${(props) => props.theme.selector.border};
.button {
min-height: 40px;
margin-bottom: 2px;
}
`;
StyledSelectorFooter.defaultProps = { theme: Base };
export {
StyledSelector,
StyledSelectorHeader,
StyledSelectorBody,
StyledSelectorFooter,
};

View File

@ -0,0 +1,405 @@
import React from "react";
import PropTypes from "prop-types";
import Header from "./sub-components/Header";
import Body from "./sub-components/Body";
import Footer from "./sub-components/Footer";
import { StyledSelector } from "./StyledSelector";
const Selector = ({
id,
className,
style,
headerLabel,
onBackClick,
searchPlaceholder,
searchValue,
onSearch,
onClearSearch,
items,
onSelect,
isMultiSelect,
selectedItems,
acceptButtonLabel,
onAccept,
withSelectAll,
selectAllLabel,
selectAllIcon,
onSelectAll,
withAccessRights,
accessRights,
selectedAccessRight,
onAccessRightsChange,
withCancelButton,
cancelButtonLabel,
onCancel,
emptyScreenImage,
emptyScreenHeader,
emptyScreenDescription,
searchEmptyScreenImage,
searchEmptyScreenHeader,
searchEmptyScreenDescription,
hasNextPage,
isNextPageLoading,
loadNextPage,
totalItems,
isLoading,
searchLoader,
rowLoader,
}) => {
const [footerVisible, setFooterVisible] = React.useState(false);
const [isSearch, setIsSearch] = React.useState(false);
const [renderedItems, setRenderedItems] = React.useState([]);
const [newSelectedItems, setNewSelectedItems] = React.useState([]);
const [selectedAccess, setSelectedAccess] = React.useState({});
const onBackClickAction = React.useCallback(() => {
onBackClick && onBackClick();
}, [onBackClick]);
const onSearchAction = React.useCallback(
(value) => {
onSearch && onSearch(value);
setIsSearch(true);
},
[onSearch]
);
const onClearSearchAction = React.useCallback(() => {
onClearSearch && onClearSearch();
setIsSearch(true);
}, [onClearSearch]);
const onSelectAction = (item) => {
onSelect &&
onSelect({
id: item.id,
avatar: item.avatar,
icon: item.icon,
label: item.label,
});
if (isMultiSelect) {
setRenderedItems((value) => {
const idx = value.findIndex((x) => item.id === x.id);
const newValue = value.map((item) => ({ ...item }));
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,
avatar: item.avatar,
icon: item.icon,
label: item.label,
});
compareSelectedItems(value);
return value;
});
}
} else {
setRenderedItems((value) => {
const idx = value.findIndex((x) => item.id === x.id);
const newValue = value.map((item) => ({ ...item, isSelected: false }));
if (idx === -1) return newValue;
newValue[idx].isSelected = !item.isSelected;
return newValue;
});
const newItem = {
id: item.id,
avatar: item.avatar,
icon: item.icon,
label: item.label,
};
if (item.isSelected) {
setNewSelectedItems([]);
compareSelectedItems([]);
} else {
setNewSelectedItems([newItem]);
compareSelectedItems([newItem]);
}
}
};
const onSelectAllAction = React.useCallback(() => {
onSelectAll && onSelectAll();
if (newSelectedItems.length === 0) {
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([]);
}
}, [items, newSelectedItems]);
const onAcceptAction = React.useCallback(() => {
onAccept && onAccept(newSelectedItems, selectedAccess);
}, [newSelectedItems, selectedAccess]);
const onCancelAction = React.useCallback(() => {
onCancel && onCancel();
}, [onCancel]);
const onChangeAccessRightsAction = React.useCallback(
(access) => {
setSelectedAccess({ ...access });
onAccessRightsChange && onAccessRightsChange(access);
},
[onAccessRightsChange]
);
const compareSelectedItems = React.useCallback(
(newList) => {
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 loadMoreItems = React.useCallback(
(startIndex) => {
!isNextPageLoading && loadNextPage && loadNextPage(startIndex - 1);
},
[isNextPageLoading, loadNextPage]
);
React.useEffect(() => {
setSelectedAccess({ ...selectedAccessRight });
}, [selectedAccessRight]);
React.useEffect(() => {
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 } = item;
const isSelected = selectedItems.some(
(selectedItem) => selectedItem.id === id
);
return { ...item, isSelected };
});
const cloneSelectedItems = selectedItems.map((item) => ({ ...item }));
setRenderedItems(newItems);
setNewSelectedItems(cloneSelectedItems);
compareSelectedItems(cloneSelectedItems);
}
}, [items, selectedItems, isMultiSelect, compareSelectedItems]);
return (
<StyledSelector id={id} className={className} style={style}>
<Header onBackClickAction={onBackClickAction} headerLabel={headerLabel} />
<Body
footerVisible={footerVisible}
isSearch={isSearch}
isAllIndeterminate={
newSelectedItems.length !== renderedItems.length &&
newSelectedItems.length !== 0
}
isAllChecked={newSelectedItems.length === renderedItems.length}
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}
totalItems={totalItems}
isLoading={isLoading}
searchLoader={searchLoader}
rowLoader={rowLoader}
/>
{footerVisible && (
<Footer
isMultiSelect={isMultiSelect}
acceptButtonLabel={acceptButtonLabel}
selectedItemsCount={newSelectedItems.length}
withCancelButton={withCancelButton}
cancelButtonLabel={cancelButtonLabel}
withAccessRights={withAccessRights}
accessRights={accessRights}
selectedAccessRight={selectedAccess}
onAccept={onAcceptAction}
onCancel={onCancelAction}
onChangeAccessRights={onChangeAccessRightsAction}
/>
)}
</StyledSelector>
);
};
Selector.propTypes = {
/** Accepts id */
id: PropTypes.string,
/** Accepts class */
className: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
/** Accepts css style */
style: PropTypes.object,
/** Selector header text */
headerLabel: PropTypes.string,
/** What the header arrow will trigger when clicked */
onBackClick: PropTypes.func,
/** Placeholder for search input */
searchPlaceholder: PropTypes.string,
/** Start value for search input */
searchValue: PropTypes.string,
/** What the search input will trigger when user stopped typing */
onSearch: PropTypes.func,
/** What the clear icon of search input will trigger when clicked */
onClearSearch: PropTypes.func,
/** Displaying items */
items: PropTypes.array,
/** What the item will trigger when clicked */
onSelect: PropTypes.func,
/** Allows you to select multiple objects */
isMultiSelect: PropTypes.bool,
/** Tells when the items should present a checked state */
selectedItems: PropTypes.array,
/** Accept button text */
acceptButtonLabel: PropTypes.string,
/** What the accept button will trigger when clicked */
onAccept: PropTypes.func,
/** Add option for select all items */
withSelectAll: PropTypes.bool,
/** Text for select all component */
selectAllLabel: PropTypes.string,
/** Icon for select all component */
selectAllIcon: PropTypes.string,
/** What the select all will trigger when clicked */
onSelectAll: PropTypes.func,
/** Add combobox for displaying access rights at footer */
withAccessRights: PropTypes.bool,
/** Access rights items */
accessRights: PropTypes.array,
/** Selected access rights items */
selectedAccessRight: PropTypes.object,
/** What the access right will trigger when changed */
onAccessRightsChange: PropTypes.func,
/** Add cancel button at footer */
withCancelButton: PropTypes.bool,
/** Displaying text at cancel button */
cancelButtonLabel: PropTypes.string,
/** What the cancel button will trigger when clicked */
onCancel: PropTypes.func,
/** Image for default empty screen */
emptyScreenImage: PropTypes.string,
/** Header for default empty screen */
emptyScreenHeader: PropTypes.string,
/** Description for default empty screen */
emptyScreenDescription: PropTypes.string,
/** Image for search empty screen */
searchEmptyScreenImage: PropTypes.string,
/** Header for search empty screen */
searchEmptyScreenHeader: PropTypes.string,
/** Description for search empty screen */
searchEmptyScreenDescription: PropTypes.string,
/** Count items for infinity scroll */
totalItems: PropTypes.number,
/** Has next page for infinity scroll */
hasNextPage: PropTypes.bool,
/** Tells when next page already loading */
isNextPageLoading: PropTypes.bool,
/** Function for load next page */
loadNextPage: PropTypes.func,
/** Set loading state for select */
isLoading: PropTypes.bool,
/** Loader element for search block */
searchLoader: PropTypes.node,
/** Loader element for item */
rowLoader: PropTypes.node,
};
Selector.defaultProps = {
isMultiSelect: false,
withSelectAll: false,
withAccessRights: false,
withCancelButton: false,
selectedItems: [],
};
export default Selector;

View File

@ -0,0 +1,187 @@
import React from "react";
import InfiniteLoader from "react-window-infinite-loader";
import { FixedSizeList as List } from "react-window";
import CustomScrollbarsVirtualList from "../../../scrollbar/custom-scrollbars-virtual-list";
import Search from "../Search";
import SelectAll from "../SelectAll";
import Item from "../Item";
import EmptyScreen from "../EmptyScreen";
import { StyledSelectorBody } from "../../StyledSelector";
const CONTAINER_PADDING = 16;
const HEADER_HEIGHT = 54;
const SEARCH_HEIGHT = 44;
const SELECT_ALL_HEIGHT = 73;
const FOOTER_HEIGHT = 73;
const Body = ({
footerVisible,
isSearch,
isAllIndeterminate,
isAllChecked,
placeholder,
value,
onSearch,
onClearSearch,
items,
onSelect,
isMultiSelect,
withSelectAll,
selectAllLabel,
selectAllIcon,
onSelectAll,
emptyScreenImage,
emptyScreenHeader,
emptyScreenDescription,
searchEmptyScreenImage,
searchEmptyScreenHeader,
searchEmptyScreenDescription,
loadMoreItems,
hasNextPage,
totalItems,
isLoading,
searchLoader,
rowLoader,
}) => {
const [bodyHeight, setBodyHeight] = React.useState(null);
const bodyRef = React.useRef(null);
const listOptionsRef = React.useRef(null);
const itemsCount = hasNextPage ? items.length + 1 : items.length;
const withSearch = isSearch || itemsCount > 0;
const resetCache = React.useCallback(() => {
if (listOptionsRef && listOptionsRef.current) {
listOptionsRef.current.resetloadMoreItemsCache(true);
}
}, [listOptionsRef.current]);
// const onBodyRef = React.useCallback(
// (node) => {
// if (node) {
// node.addEventListener("resize", onBodyResize);
// bodyRef.current = node;
// setBodyHeight(node.offsetHeight);
// }
// },
// [onBodyResize]
// );
const onBodyResize = React.useCallback(() => {
setBodyHeight(bodyRef.current.offsetHeight);
}, [bodyRef?.current?.offsetHeight]);
const isItemLoaded = React.useCallback(
(index) => {
return !hasNextPage || index < itemsCount;
},
[hasNextPage, itemsCount]
);
React.useEffect(() => {
window.addEventListener("resize", onBodyResize);
return () => {
window.removeEventListener("resize", onBodyResize);
};
}, []);
React.useEffect(() => {
onBodyResize();
}, [isLoading, footerVisible]);
React.useEffect(() => {
resetCache();
}, [resetCache, hasNextPage]);
let listHeight = bodyHeight - CONTAINER_PADDING;
if (withSearch) listHeight -= SEARCH_HEIGHT;
if (isMultiSelect && withSelectAll && !isSearch)
listHeight -= SELECT_ALL_HEIGHT;
return (
<StyledSelectorBody
ref={bodyRef}
footerHeight={FOOTER_HEIGHT}
headerHeight={HEADER_HEIGHT}
footerVisible={footerVisible}
>
{isLoading && !isSearch ? (
searchLoader
) : withSearch ? (
<Search
placeholder={placeholder}
value={value}
onSearch={onSearch}
onClearSearch={onClearSearch}
/>
) : null}
{isLoading ? (
rowLoader
) : itemsCount === 0 ? (
<EmptyScreen
withSearch={isSearch && value}
image={emptyScreenImage}
header={emptyScreenHeader}
description={emptyScreenDescription}
searchImage={searchEmptyScreenImage}
searchHeader={searchEmptyScreenHeader}
searchDescription={searchEmptyScreenDescription}
/>
) : (
<>
{isMultiSelect && withSelectAll && !isSearch && (
<SelectAll
label={selectAllLabel}
icon={selectAllIcon}
isChecked={isAllChecked}
isIndeterminate={isAllIndeterminate}
onSelectAll={onSelectAll}
isLoading={isLoading}
rowLoader={rowLoader}
/>
)}
{bodyHeight && (
<InfiniteLoader
ref={listOptionsRef}
isItemLoaded={isItemLoaded}
itemCount={totalItems}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<List
className="items-list"
height={listHeight}
width={"100%"}
itemCount={itemsCount}
itemData={{
items,
onSelect,
isMultiSelect,
rowLoader,
isItemLoaded,
}}
itemSize={48}
onItemsRendered={onItemsRendered}
ref={ref}
outerElementType={CustomScrollbarsVirtualList}
>
{Item}
</List>
)}
</InfiniteLoader>
)}
</>
)}
</StyledSelectorBody>
);
};
export default Body;

View File

@ -0,0 +1,79 @@
import React from "react";
import styled from "styled-components";
import Heading from "../../../heading";
import Text from "../../../text";
import { Base } from "../../../themes";
const StyledContainer = styled.div`
width: 100%;
display: flex;
align-items: center;
flex-direction: column;
margin-top: ${(props) => (props.withSearch ? "80px" : "64px")};
padding: 0 28px;
box-sizing: border-box;
.empty-image {
max-width: 72px;
max-height: 72px;
margin-bottom: 32px;
}
.empty-header {
font-weight: 700;
font-size: 16px;
line-height: 22px;
margin: 0;
}
.empty-description {
font-weight: 400;
font-size: 12px;
line-height: 16px;
text-align: center;
color: ${(props) => props.theme.selector.emptyScreen.descriptionColor};
margin-top: 8px;
}
`;
StyledContainer.defaultProps = { theme: Base };
const EmptyScreen = ({
image,
header,
description,
searchImage,
searchHeader,
searchDescription,
withSearch,
}) => {
const currentImage = withSearch ? searchImage : image;
const currentHeader = withSearch ? searchHeader : header;
const currentDescription = withSearch ? searchDescription : description;
return (
<StyledContainer withSearch={withSearch}>
<img
className="empty-image"
src={currentImage}
alt="empty-screen-image"
/>
<Heading level={3} className="empty-header">
{currentHeader}
</Heading>
<Text className="empty-description" noSelect>
{currentDescription}
</Text>
</StyledContainer>
);
};
export default EmptyScreen;

View File

@ -0,0 +1,87 @@
import React from "react";
import styled from "styled-components";
import Button from "../../../button";
import Combobox from "../../../combobox";
import { StyledSelectorFooter } from "../../StyledSelector";
const StyledComboBox = styled(Combobox)`
margin-bottom: 2px;
max-height: 50px;
.combo-button {
min-height: 40px;
}
.combo-buttons_arrow-icon {
margin-top: 16px;
}
.combo-button-label,
.combo-button-label:hover {
font-size: 14px;
text-decoration: none;
}
`;
const Footer = React.memo(
({
isMultiSelect,
acceptButtonLabel,
selectedItemsCount,
withCancelButton,
cancelButtonLabel,
withAccessRights,
accessRights,
selectedAccessRight,
onAccept,
onCancel,
onChangeAccessRights,
}) => {
const label =
selectedItemsCount && isMultiSelect
? `${acceptButtonLabel} (${selectedItemsCount})`
: acceptButtonLabel;
return (
<StyledSelectorFooter>
<Button
className={"button accept-button"}
label={label}
primary
scale
size={"normal"}
onClick={onAccept}
/>
{withAccessRights && (
<StyledComboBox
onSelect={onChangeAccessRights}
options={accessRights}
size="content"
scaled={false}
manualWidth="fit-content"
selectedOption={selectedAccessRight}
showDisabledItems
directionX={"right"}
directionY={"top"}
/>
)}
{withCancelButton && (
<Button
className={"button cancel-button"}
label={cancelButtonLabel}
scale
size={"normal"}
onClick={onCancel}
/>
)}
</StyledSelectorFooter>
);
}
);
export default Footer;

View File

@ -0,0 +1,22 @@
import React from "react";
import IconButton from "../../../icon-button";
import Heading from "../../../heading";
import { StyledSelectorHeader } from "../../StyledSelector";
const Header = React.memo(({ onBackClickAction, headerLabel }) => {
return (
<StyledSelectorHeader>
<IconButton
className="arrow-button"
iconName="static/images/arrow.path.react.svg"
size={17}
onClick={onBackClickAction}
/>
<Heading className={"heading-text"}>{headerLabel}</Heading>
</StyledSelectorHeader>
);
});
export default Header;

View File

@ -0,0 +1,143 @@
import React from "react";
import styled, { css } from "styled-components";
import { ReactSVG } from "react-svg";
import Avatar from "../../../avatar";
import Text from "../../../text";
import Checkbox from "../../../checkbox";
import { Base } from "../../../themes";
const selectedCss = css`
background: ${(props) =>
props.theme.selector.item.selectedBackground} !important;
`;
const StyledItem = styled.div`
display: flex;
align-items: center;
padding: 0 16px;
box-sizing: border-box;
:hover {
cursor: pointer;
background: ${(props) => props.theme.selector.item.hoverBackground};
}
${(props) => props.isSelected && !props.isMultiSelect && selectedCss}
.room-logo,
.user-avatar {
min-width: 32px;
}
.room-logo {
height: 32px;
border-radius: 6px;
}
.label {
width: 100%;
max-width: 100%;
line-height: 16px;
margin-left: 8px;
}
.checkbox {
svg {
margin-right: 0px;
}
}
`;
StyledItem.defaultProps = { theme: Base };
const compareFunction = (prevProps, nextProps) => {
const prevData = prevProps.data;
const prevItems = prevData.items;
const prevIndex = prevProps.index;
const nextData = nextProps.data;
const nextItems = nextData.items;
const nextIndex = nextProps.index;
const prevItem = prevItems[prevIndex];
const nextItem = nextItems[nextIndex];
return (
prevItem?.id === nextItem?.id &&
prevItem?.isSelected === nextItem?.isSelected
);
};
const Item = React.memo(({ index, style, data }) => {
const { items, onSelect, isMultiSelect, isItemLoaded, rowLoader } = data;
const isLoaded = isItemLoaded(index);
const renderItem = () => {
const item = items[index];
if (!item && !item?.id) return <div style={style}>{rowLoader}</div>;
const { label, avatar, icon, role, isSelected } = item;
const currentRole = role ? role : "user";
const isLogo = !!icon;
const onChangeAction = () => {
onSelect && onSelect(item);
};
const onClick = () => {
!isMultiSelect && onSelect && onSelect(item);
};
return (
<StyledItem
isSelected={isSelected}
isMultiSelect={isMultiSelect}
style={style}
onClick={onClick}
>
{!isLogo ? (
<Avatar
className="user-avatar"
source={avatar}
role={currentRole}
size={"min"}
/>
) : (
<img className="room-logo" src={icon} alt="room logo" />
)}
<Text
className="label"
fontWeight={600}
fontSize={"14px"}
noSelect
truncate
>
{label}
</Text>
{isMultiSelect && (
<Checkbox
className="checkbox"
isChecked={isSelected}
onChange={onChangeAction}
/>
)}
</StyledItem>
);
};
return isLoaded ? renderItem() : <div style={style}>{rowLoader}</div>;
}, compareFunction);
export default Item;

View File

@ -0,0 +1,17 @@
import React from "react";
import SearchInput from "../../../search-input";
const Search = React.memo(({ placeholder, value, onSearch, onClearSearch }) => {
return (
<SearchInput
className="search-input"
placeholder={placeholder}
value={value}
onChange={onSearch}
onClearSearch={onClearSearch}
/>
);
});
export default Search;

View File

@ -0,0 +1,95 @@
import React from "react";
import styled from "styled-components";
import Avatar from "../../../avatar";
import Text from "../../../text";
import Checkbox from "../../../checkbox";
import { Base } from "../../../themes";
const StyledSelectAll = styled.div`
width: 100%;
max-height: 61px;
height: 61px;
min-height: 61px;
display: flex;
align-items: center;
border-bottom: ${(props) => props.theme.selector.border};
box-sizing: border-box;
padding: 8px 16px 20px;
margin-bottom: 12px;
.select-all_avatar {
min-width: 32px;
}
.label {
width: 100%;
max-width: 100%;
line-height: 16px;
margin-left: 8px;
}
.checkbox {
svg {
margin-right: 0px;
}
}
`;
StyledSelectAll.defaultProps = { theme: Base };
const SelectAll = React.memo(
({
label,
icon,
onSelectAll,
isChecked,
isIndeterminate,
isLoading,
rowLoader,
}) => {
return (
<StyledSelectAll>
<div style={{ background: "red", height: "150px" }}></div>
{isLoading ? (
rowLoader
) : (
<>
<Avatar
className="select-all_avatar"
source={icon}
role={"user"}
size={"min"}
/>
<Text
className="label"
fontWeight={600}
fontSize={"14px"}
noSelect
truncate
>
{label}
</Text>
<Checkbox
className="checkbox"
isChecked={isChecked}
isIndeterminate={isIndeterminate}
onChange={onSelectAll}
/>
</>
)}
</StyledSelectAll>
);
}
);
export default SelectAll;

View File

@ -2207,6 +2207,19 @@ const Base = {
},
},
selector: {
border: `1px solid ${grayLightMid}`,
item: {
hoverBackground: grayLight,
selectedBackground: lightHover,
},
emptyScreen: {
descriptionColor: cyanBlueDarkShade,
},
},
floatingButton: {
backgroundColor: "#3B72A7",
color: white,

View File

@ -2212,6 +2212,19 @@ const Dark = {
},
},
selector: {
border: `1px solid #474747`,
item: {
hoverBackground: "#3d3d3d",
selectedBackground: "#3d3d3d",
},
emptyScreen: {
descriptionColor: cyanBlueDarkShade,
},
},
floatingButton: {
backgroundColor: white,
color: black,