diff --git a/packages/client/src/components/panels/InvitePanel/StyledInvitePanel.js b/packages/client/src/components/panels/InvitePanel/StyledInvitePanel.js index 20d2e36aad..05c47d80bd 100644 --- a/packages/client/src/components/panels/InvitePanel/StyledInvitePanel.js +++ b/packages/client/src/components/panels/InvitePanel/StyledInvitePanel.js @@ -359,7 +359,12 @@ const StyledDropDown = styled(DropDown)` gap: 4px; p { - color: #4781d1; + color: ${(props) => props.theme.currentColorScheme.main.accent}; + ${(props) => + props.isRequestRunning && + css` + opacity: 0.65; + `} } svg { @@ -367,7 +372,12 @@ const StyledDropDown = styled(DropDown)` theme.interfaceDirection === "rtl" && "transform: scaleX(-1);"}; path { - fill: #4781d1; + fill: ${(props) => props.theme.currentColorScheme.main.accent}; + ${(props) => + props.isRequestRunning && + css` + opacity: 0.65; + `} } } } diff --git a/packages/client/src/components/panels/InvitePanel/sub-components/InviteInput.js b/packages/client/src/components/panels/InvitePanel/sub-components/InviteInput.js index 12ed29495f..e13655d579 100644 --- a/packages/client/src/components/panels/InvitePanel/sub-components/InviteInput.js +++ b/packages/client/src/components/panels/InvitePanel/sub-components/InviteInput.js @@ -39,7 +39,7 @@ import { ComboBox } from "@docspace/shared/components/combobox"; import Filter from "@docspace/shared/api/people/filter"; import BetaBadge from "../../../BetaBadgeWrapper"; -import { getMembersList } from "@docspace/shared/api/people"; +import { getMembersList, getUserList } from "@docspace/shared/api/people"; import { AccountsSearchArea, EmployeeType, @@ -72,6 +72,9 @@ import ArrowIcon from "PUBLIC_DIR/images/arrow.right.react.svg"; import PaidQuotaLimitError from "SRC_DIR/components/PaidQuotaLimitError"; const minSearchValue = 2; +const filterSeparator = ";"; +const regex = + /^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; const InviteInput = ({ defaultAccess, @@ -101,21 +104,19 @@ const InviteInput = ({ const isPublicRoomType = roomType === RoomsType.PublicRoom; const [inputValue, setInputValue] = useState(""); + const [usersList, setUsersList] = useState([]); + const [invitedUsers, setInvitedUsers] = useState(new Map()); + const [isChangeLangMail, setIsChangeLangMail] = useState(false); const [isAddEmailPanelBlocked, setIsAddEmailPanelBlocked] = useState(true); const [selectedAccess, setSelectedAccess] = useState(defaultAccess); const [dropDownWidth, setDropDownWidth] = useState(0); + const [searchRequestRunning, setSearchRequestRunning] = useState(false); const searchRef = useRef(); - - const selectedLanguage = cultureNames.find((item) => item.key === language) || - cultureNames.find((item) => item.key === culture.key) || { - key: language, - label: "", - isBeta: isBetaLanguage(language), - }; + const prevDropDownContent = useRef(null); useEffect(() => { setTimeout(() => { @@ -124,15 +125,53 @@ const InviteInput = ({ }, 0); }); + const selectedLanguage = useMemo( + () => + cultureNames.find((item) => item.key === language) || + cultureNames.find((item) => item.key === culture.key) || { + key: language, + label: "", + isBeta: isBetaLanguage(language), + }, + [cultureNames, language, culture], + ); + + const cultureNamesNew = useMemo( + () => + cultureNames.map((item) => ({ + label: item.label, + key: item.key, + isBeta: isBetaLanguage(item.key), + })), + [cultureNames], + ); + useEffect(() => { - !culture.key && + if (!culture.key) { setInviteLanguage({ key: language, label: selectedLanguage.label, isBeta: isBetaLanguage(language), }); + } }, []); + const onLanguageSelect = (language) => { + setInviteLanguage(language); + setCultureKey(language.key); + if (language.key !== i18n.language) setIsChangeLangMail(true); + else setIsChangeLangMail(false); + }; + + const onResetLangMail = () => { + setInviteLanguage({ + key: selectedLanguage.key, + label: selectedLanguage.label, + isBeta: selectedLanguage.isBeta, + }); + setIsChangeLangMail(false); + }; + const toUserItems = (query) => { const addresses = parseAddresses(query); const uid = () => Math.random().toString(36).slice(-6); @@ -192,38 +231,67 @@ const InviteInput = ({ } } - return { - email: addresses[0].email, - id: uid(), - access: userAccess, - displayName: addresses[0].email, - errors: addresses[0].parseErrors, - isEmailInvite: true, - }; + return [ + { + email: addresses[0].email, + id: uid(), + access: userAccess, + displayName: addresses[0].email, + errors: addresses[0].parseErrors, + isEmailInvite: true, + }, + ]; }; const searchByQuery = async (value) => { - const query = value.trim(); - - if (query.length >= minSearchValue) { - const searchArea = isPublicRoomType - ? AccountsSearchArea.People - : AccountsSearchArea.Any; - const filter = Filter.getFilterWithOutDisabledUser(); - filter.search = query; - - const users = await getMembersList(searchArea, roomId, filter); - - setUsersList(users.items); - - if (users.total) setIsAddEmailPanelBlocked(false); - } + const query = getParts(value.trim()).join(filterSeparator); if (!query) { setInputValue(""); setUsersList([]); setIsAddEmailPanelBlocked(true); + setSearchRequestRunning(false); + + return; } + + let isBlocked = true; + + if (query.length >= minSearchValue) { + const filter = Filter.getDefault(); + + const searchArea = isPublicRoomType + ? AccountsSearchArea.People + : AccountsSearchArea.Any; + + filter.search = query; + filter.filterSeparator = filterSeparator; + + const users = + roomId === -1 + ? await getUserList(filter) + : await getMembersList(searchArea, roomId, filter); + + setUsersList( + roomId === -1 + ? users.items.map((value) => ({ ...value, shared: true })) + : users.items, + ); + + if (users.total) isBlocked = false; + } + + const parts = getParts(value); + + parts.forEach((part) => { + isBlocked = regex.test(part) ? false : isBlocked; + }); + + console.log("set here", isBlocked); + + setIsAddEmailPanelBlocked(isBlocked); + + setSearchRequestRunning(false); }; const debouncedSearch = useCallback( @@ -247,22 +315,8 @@ const InviteInput = ({ return; } - if (roomId !== -1) { - debouncedSearch(clearValue); - } - - const regex = - /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{0,}))$/g; - - const parts = getParts(value); - for (let i = 0; i < parts.length; i += 1) { - if (regex.test(parts[i])) { - setIsAddEmailPanelBlocked(false); - return; - } - } - - setIsAddEmailPanelBlocked(true); + setSearchRequestRunning(true); + debouncedSearch(clearValue); }; const removeExist = (items) => { @@ -360,12 +414,62 @@ const InviteInput = ({ }; const addEmail = () => { - if (!inputValue.trim()) return; + if (!inputValue.trim() || searchRequestRunning) return; const items = toUserItems(inputValue); - const newItems = - items.length > 1 ? [...items, ...inviteItems] : [items, ...inviteItems]; + const filteredItems = items + .filter( + (item) => + !usersList.find((value) => value.email === item.email)?.shared, + ) + .map((item) => { + const userItem = usersList.find((value) => value.email === item.email); + + if (userItem) { + if (userItem.isOwner || userItem.isAdmin) + userItem.access = ShareAccessRights.RoomManager; + + if (userItem.isGroup && checkIfAccessPaid(userItem.access)) { + const topFreeRole = getTopFreeRole(t, roomType); + userItem.access = topFreeRole.access; + userItem.warning = t("GroupMaxAvailableRoleWarning", { + roleName: topFreeRole.label, + }); + } + + if ( + isUserTariffLimit && + userItem.isVisitor && + isPaidUserRole(item.access) + ) { + const freeRole = getTopFreeRole(t, roomType)?.access; + + if (freeRole) { + userItem.access = freeRole; + toastr.error(); + } + } + + return userItem; + } + + return item; + }); + + if (filteredItems.length !== items.length) { + toastr.warning(t("UsersAlreadyAdded")); + } + + if (!filteredItems.length) { + setInputValue(""); + setIsAddEmailPanelBlocked(true); + setUsersList([]); + + return; + } + + const newItems = [...filteredItems, ...inviteItems]; const filtered = removeExist(newItems); @@ -420,30 +524,43 @@ const InviteInput = ({ setAddUsersPanelVisible(false); }; - const foundUsers = usersList.map((user) => getItemContent(user)); + const dropDownContent = useMemo(() => { + const partsLength = getParts(inputValue).length; - const addEmailPanel = ( - -
- - - {inputValue} - -
-
- - {t("Common:AddButton")} - - -
-
- ); + if (searchRequestRunning && prevDropDownContent.current) { + return prevDropDownContent.current; + } + + if (partsLength === 1 && !!usersList.length) { + prevDropDownContent.current = usersList.map((user) => + getItemContent(user), + ); + } else { + prevDropDownContent.current = ( + +
+ + + {inputValue} + +
{" "} +
+ + {t("Common:AddButton")} + + +
+
+ ); + } + return prevDropDownContent.current; + }, [usersList, inputValue]); const accessOptions = getAccessOptions( t, @@ -479,30 +596,18 @@ const InviteInput = ({ document.addEventListener("keyup", onKeyPress); return () => document.removeEventListener("keyup", onKeyPress); }); - const onLanguageSelect = (language) => { - setInviteLanguage(language); - setCultureKey(language.key); - if (language.key !== i18n.language) setIsChangeLangMail(true); - else setIsChangeLangMail(false); - }; - const onResetLangMail = () => { - setInviteLanguage({ - key: selectedLanguage.key, - label: selectedLanguage.label, - isBeta: selectedLanguage.isBeta, - }); - setIsChangeLangMail(false); - }; - const cultureNamesNew = cultureNames.map((item) => ({ - label: item.label, - key: item.key, - isBeta: isBetaLanguage(item.key), - })); + useEffect(() => { + const newInviteItems = new Map(); - const invitedUsers = useMemo( - () => inviteItems.map((item) => item.id), - [inviteItems], + inviteItems.forEach((item) => newInviteItems.set(item?.id, item)); + + setInvitedUsers((value) => new Map([...value, ...newInviteItems])); + }, [inviteItems]); + + const invitedUsersArray = useMemo( + () => Array.from(invitedUsers.keys()), + [invitedUsers], ); return ( @@ -614,8 +719,9 @@ const InviteInput = ({ withBackdrop={false} zIndex={399} {...dropDownMaxHeight} + isRequestRunning={searchRequestRunning} > - {!!usersList.length ? foundUsers : addEmailPanel} + {dropDownContent} )} @@ -650,7 +756,7 @@ const InviteInput = ({ roomId={roomId} withGroups={!isPublicRoomType} withAccessRights - invitedUsers={invitedUsers} + invitedUsers={invitedUsersArray} disableDisabledUsers /> )} diff --git a/packages/client/src/components/panels/InvitePanel/sub-components/Item.js b/packages/client/src/components/panels/InvitePanel/sub-components/Item.js index aaeded37f4..3a58fd13ca 100644 --- a/packages/client/src/components/panels/InvitePanel/sub-components/Item.js +++ b/packages/client/src/components/panels/InvitePanel/sub-components/Item.js @@ -23,18 +23,22 @@ // All the Product's GUI elements, including illustrations and icon sets, as well as technical writing // content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0 // International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode +import debounce from "lodash.debounce"; import InfoEditReactSvgUrl from "PUBLIC_DIR/images/info.edit.react.svg?url"; import AtReactSvgUrl from "PUBLIC_DIR/images/@.react.svg?url"; import AlertSvgUrl from "PUBLIC_DIR/images/icons/12/alert.react.svg?url"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import { inject, observer } from "mobx-react"; import { Avatar } from "@docspace/shared/components/avatar"; import { Text } from "@docspace/shared/components/text"; import { parseAddresses } from "@docspace/shared/utils"; import { getUserTypeLabel } from "@docspace/shared/utils/common"; +import { getMembersList, getUserList } from "@docspace/shared/api/people"; +import { AccountsSearchArea, RoomsType } from "@docspace/shared/enums"; +import { toastr } from "@docspace/shared/components/toast"; import { getAccessOptions, @@ -55,6 +59,7 @@ import { filterGroupRoleOptions, filterUserRoleOptions } from "SRC_DIR/helpers"; import AccessSelector from "../../../AccessSelector"; import PaidQuotaLimitError from "SRC_DIR/components/PaidQuotaLimitError"; +import Filter from "@docspace/shared/api/people/filter"; const Item = ({ t, @@ -101,6 +106,44 @@ const Item = ({ const [inputValue, setInputValue] = useState(name); const [parseErrors, setParseErrors] = useState(errors); + const [searchRequestRunning, setSearchRequestRunning] = useState(false); + const [isSharedUser, setIsSharedUser] = useState(false); + + const searchByQuery = async (value) => { + if (!value) { + setSearchRequestRunning(false); + setIsSharedUser(false); + + return; + } + + const isPublicRoomType = roomType === RoomsType.PublicRoom; + + const filter = Filter.getDefault(); + + const searchArea = isPublicRoomType + ? AccountsSearchArea.People + : AccountsSearchArea.Any; + + filter.search = value; + + const users = + roomId === -1 + ? await getUserList(filter) + : await getMembersList(searchArea, roomId, filter); + + setSearchRequestRunning(false); + + const user = users.items.find((item) => item.email === value); + + setIsSharedUser(user && (roomId === -1 || user?.shared)); + }; + + const debouncedSearch = useCallback( + debounce((value) => searchByQuery(value), 300), + [], + ); + const accesses = getAccessOptions( t, roomType, @@ -148,9 +191,17 @@ const Item = ({ const cancelEdit = (e) => { setInputValue(name); setEdit(false); + setSearchRequestRunning(false); + setIsSharedUser(false); }; - const saveEdit = (e) => { + const saveEdit = async (e) => { + if (searchRequestRunning) return; + + if (isSharedUser) { + return toastr.warning(t("UsersAlreadyAdded")); + } + const value = inputValue === "" ? name : inputValue; setEdit(false); @@ -185,6 +236,10 @@ const Item = ({ const value = e.target.value.trim(); setInputValue(value); + + setSearchRequestRunning(true); + + debouncedSearch(value); }; const hasError = parseErrors && !!parseErrors.length; @@ -286,7 +341,11 @@ const Item = ({ const editBody = ( <> - + ); diff --git a/packages/shared/api/people/filter.js b/packages/shared/api/people/filter.js index 086224fd9a..05f18a80cc 100644 --- a/packages/shared/api/people/filter.js +++ b/packages/shared/api/people/filter.js @@ -40,6 +40,7 @@ const DEFAULT_PAYMENTS = null; const DEFAULT_ACCOUNT_LOGIN_TYPE = null; const DEFAULT_WITHOUT_GROUP = false; const DEFAULT_QUOTA_FILTER = null; +const DEFAULT_FILTER_SEPARATOR = null; const ACTIVE_EMPLOYEE_STATUS = 1; @@ -56,6 +57,7 @@ const PAYMENTS = "payments"; const ACCOUNT_LOGIN_TYPE = "accountLoginType"; const WITHOUT_GROUP = "withoutGroup"; const QUOTA_FILTER = "quotaFilter"; +const FILTER_SEPARATOR = "filterSeparator"; class Filter { static getDefault(total = DEFAULT_TOTAL) { @@ -145,6 +147,7 @@ class Filter { accountLoginType = DEFAULT_ACCOUNT_LOGIN_TYPE, withoutGroup = DEFAULT_WITHOUT_GROUP, quotaFilter = DEFAULT_QUOTA_FILTER, + filterSeparator = DEFAULT_FILTER_SEPARATOR, ) { this.page = page; this.pageCount = pageCount; @@ -160,6 +163,7 @@ class Filter { this.accountLoginType = accountLoginType; this.withoutGroup = withoutGroup; this.quotaFilter = quotaFilter; + this.filterSeparator = filterSeparator; } getStartIndex = () => { @@ -188,6 +192,7 @@ class Filter { accountLoginType, withoutGroup, quotaFilter, + filterSeparator, } = this; let employeetype = null; @@ -212,6 +217,7 @@ class Filter { accountLoginType, withoutGroup, quotaFilter, + filterSeparator, }; dtoFilter = { ...dtoFilter, ...employeetype }; @@ -235,6 +241,7 @@ class Filter { accountLoginType, withoutGroup, quotaFilter, + filterSeparator, } = this; const dtoFilter = {}; @@ -269,6 +276,8 @@ class Filter { if (quotaFilter) dtoFilter[QUOTA_FILTER] = quotaFilter; + if (filterSeparator) dtoFilter[FILTER_SEPARATOR] = filterSeparator; + dtoFilter[PAGE] = page + 1; dtoFilter[SORT_BY] = sortBy; dtoFilter[SORT_ORDER] = sortOrder; @@ -304,6 +313,7 @@ class Filter { this.accountLoginType, this.withoutGroup, this.quotaFilter, + this.filterSeparator, ); } @@ -323,6 +333,7 @@ class Filter { null, null, false, + null, ); } @@ -342,7 +353,8 @@ class Filter { this.pageCount === filter.pageCount && this.payments === filter.payments && this.accountLoginType === filter.accountLoginType && - this.withoutGroup === filter.withoutGroup; + this.withoutGroup === filter.withoutGroup && + this.filterSeparator === filter.filterSeparator; return equals; } diff --git a/packages/shared/components/drop-down/DropDown.styled.ts b/packages/shared/components/drop-down/DropDown.styled.ts index bb622db405..58802e6a5e 100644 --- a/packages/shared/components/drop-down/DropDown.styled.ts +++ b/packages/shared/components/drop-down/DropDown.styled.ts @@ -112,8 +112,6 @@ const StyledDropdown = styled.div<{ -moz-box-shadow: ${(props) => props.theme.dropDown.boxShadow}; -webkit-box-shadow: ${(props) => props.theme.dropDown.boxShadow}; - padding: ${(props) => - !props.maxHeight && props.itemCount && props.itemCount > 1 && `4px 0px`}; ${(props) => props.columnCount && `