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 = (
-
-
-
-
- {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 = (
+
+ {" "}
+
+
+ {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 &&
`