Fixed Bug 69896 - Accounts. When adding a registered email to the list for invitation to the portal, display that it is already registered

This commit is contained in:
Timofey Boyko 2024-09-01 12:36:14 +03:00
parent 303bd8b2ac
commit bf5a4d5226
4 changed files with 295 additions and 103 deletions

View File

@ -366,7 +366,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 {
@ -374,7 +379,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;
`}
}
}
}

View File

@ -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,20 @@ 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 searchRequestRunning = useRef(false);
const prevDropDownContent = useRef(null);
useEffect(() => {
setTimeout(() => {
@ -124,15 +126,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 +232,65 @@ 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;
});
setIsAddEmailPanelBlocked(isBlocked);
setSearchRequestRunning(false);
};
const debouncedSearch = useCallback(
@ -247,22 +314,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) => {
@ -362,10 +415,64 @@ const InviteInput = ({
const addEmail = () => {
if (!inputValue.trim()) return;
if (searchRequestRunning.current) {
return setTimeout(addEmail, 100);
}
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(<PaidQuotaLimitError />);
}
}
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 +527,43 @@ const InviteInput = ({
setAddUsersPanelVisible(false);
};
const foundUsers = usersList.map((user) => getItemContent(user));
const dropDownContent = useMemo(() => {
const partsLength = getParts(inputValue).length;
const addEmailPanel = (
<DropDownItem
className="list-item"
style={{ width: "inherit" }}
textOverflow
onClick={addEmail}
height={48}
>
<div className="email-list_avatar">
<Avatar size="min" role="user" source={AtReactSvgUrl} />
<Text truncate fontSize="14px" fontWeight={600}>
{inputValue}
</Text>
</div>
<div className="email-list_add-button">
<Text fontSize="13px" fontWeight={600}>
{t("Common:AddButton")}
</Text>
<ArrowIcon />
</div>
</DropDownItem>
);
if (searchRequestRunning.current && prevDropDownContent.current) {
return prevDropDownContent.current;
}
if (partsLength === 1 && !!usersList.length) {
prevDropDownContent.current = usersList.map((user) =>
getItemContent(user),
);
} else {
prevDropDownContent.current = (
<DropDownItem
className="list-item"
style={{ width: "inherit" }}
textOverflow
onClick={addEmail}
height={48}
>
<div className="email-list_avatar">
<Avatar size="min" role="user" source={AtReactSvgUrl} />
<Text truncate fontSize="14px" fontWeight={600}>
{inputValue}
</Text>
</div>{" "}
<div className="email-list_add-button">
<Text fontSize="13px" fontWeight={600}>
{t("Common:AddButton")}
</Text>
<ArrowIcon />
</div>
</DropDownItem>
);
}
return prevDropDownContent.current;
}, [usersList, inputValue]);
const accessOptions = getAccessOptions(
t,
@ -479,30 +599,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 +722,9 @@ const InviteInput = ({
withBackdrop={false}
zIndex={399}
{...dropDownMaxHeight}
isRequestRunning={searchRequestRunning}
>
{!!usersList.length ? foundUsers : addEmailPanel}
{dropDownContent}
</StyledDropDown>
)}
@ -650,7 +759,7 @@ const InviteInput = ({
roomId={roomId}
withGroups={!isPublicRoomType}
withAccessRights
invitedUsers={invitedUsers}
invitedUsers={invitedUsersArray}
disableDisabledUsers
/>
)}

View File

@ -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,19 @@ const Item = ({
const cancelEdit = (e) => {
setInputValue(name);
setEdit(false);
setSearchRequestRunning(false);
setIsSharedUser(false);
};
const saveEdit = (e) => {
const saveEdit = async (e) => {
if (searchRequestRunning) return;
console.log(isSharedUser);
if (isSharedUser) {
return toastr.warning(t("UsersAlreadyAdded"));
}
const value = inputValue === "" ? name : inputValue;
setEdit(false);
@ -185,6 +238,10 @@ const Item = ({
const value = e.target.value.trim();
setInputValue(value);
setSearchRequestRunning(true);
debouncedSearch(value);
};
const hasError = parseErrors && !!parseErrors.length;
@ -286,7 +343,11 @@ const Item = ({
const editBody = (
<>
<StyledEditInput value={inputValue} onChange={changeValue} />
<StyledEditButton icon={okIcon} onClick={saveEdit} />
<StyledEditButton
icon={okIcon}
onClick={saveEdit}
isDisabled={searchRequestRunning}
/>
<StyledEditButton icon={cancelIcon} onClick={cancelEdit} />
</>
);

View File

@ -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;
}