diff --git a/build/install/common/product-ssl-setup b/build/install/common/product-ssl-setup index 9e351324e1..c1d838a434 100644 --- a/build/install/common/product-ssl-setup +++ b/build/install/common/product-ssl-setup @@ -46,7 +46,7 @@ if [ "$#" -ge "2" ]; then if [ -f "${OPENRESTY}/onlyoffice-proxy-ssl.conf.template" ]; then cp -f ${OPENRESTY}/onlyoffice-proxy-ssl.conf.template ${OPENRESTY}/onlyoffice-proxy.conf - ENVIRONMENT=$(grep -oP 'ENVIRONMENT=\K.*' /usr/lib/systemd/system/${PRODUCT}-api.service) + ENVIRONMENT=$(grep -oP 'ENVIRONMENT=\K.*' $(dirname $(dpkg-query -L ${PRODUCT}-api | grep systemd/system/))/${PRODUCT}-api.service) sed -i "s/\(\"portal\":\).*/\1 \"https:\/\/${DOMAIN:-$(hostname --fqdn)}\"/" /etc/onlyoffice/docspace/appsettings.$ENVIRONMENT.json sed -i "s~\(ssl_certificate \).*;~\1${CERTIFICATE_FILE};~g" ${OPENRESTY}/onlyoffice-proxy.conf sed -i "s~\(ssl_certificate_key \).*;~\1${PRIVATEKEY_FILE};~g" ${OPENRESTY}/onlyoffice-proxy.conf diff --git a/common/ASC.Api.Core/Model/EmployeeFullDto.cs b/common/ASC.Api.Core/Model/EmployeeFullDto.cs index add3386a3a..bb16574faa 100644 --- a/common/ASC.Api.Core/Model/EmployeeFullDto.cs +++ b/common/ASC.Api.Core/Model/EmployeeFullDto.cs @@ -157,6 +157,7 @@ public class EmployeeFullDto : EmployeeDto /// Portal used space /// System.Double, System public double UsedSpace { get; set; } + public bool? Shared { get; set; } public static new EmployeeFullDto GetSample() { @@ -257,6 +258,7 @@ public class EmployeeFullDtoHelper : EmployeeDtoHelper return lambda; } + public async Task GetSimple(UserInfo userInfo) { var result = new EmployeeFullDto @@ -279,7 +281,7 @@ public class EmployeeFullDtoHelper : EmployeeDtoHelper return result; } - public async Task GetFullAsync(UserInfo userInfo) + public async Task GetFullAsync(UserInfo userInfo, bool? shared = null) { var currentType = await _userManager.GetUserTypeAsync(userInfo.Id); @@ -300,7 +302,8 @@ public class EmployeeFullDtoHelper : EmployeeDtoHelper IsOwner = userInfo.IsOwner(_context.Tenant), IsCollaborator = currentType is EmployeeType.Collaborator, IsLDAP = userInfo.IsLDAP(), - IsSSO = userInfo.IsSSO() + IsSSO = userInfo.IsSSO(), + Shared = shared, }; await InitAsync(result, userInfo); @@ -373,6 +376,7 @@ public class EmployeeFullDtoHelper : EmployeeDtoHelper return result; } + private async Task FillGroupsAsync(EmployeeFullDto result, UserInfo userInfo) { if (!_context.Check("groups") && !_context.Check("department")) diff --git a/packages/client/public/locales/en/InfoPanel.json b/packages/client/public/locales/en/InfoPanel.json index 2c92ac3d04..e4fb6a9461 100644 --- a/packages/client/public/locales/en/InfoPanel.json +++ b/packages/client/public/locales/en/InfoPanel.json @@ -39,5 +39,9 @@ "SubmenuHistory": "History", "SystemProperties": "System properties", "UsersInRoom": "Users in room", - "Versions": "Versions" + "Versions": "Versions", + "Administration": "Administration", + "Users": "Users", + "ExpectUsers": "Expect users", + "InfoBanner": "The list of invited users includes the owner and/or admins of this DocSpace with full access to all rooms. The owner and/or administrator cannot be assigned other access rights. Once added to the room, they will be notified of all changes." } diff --git a/packages/client/src/components/dialogs/DeleteLinkDialog/index.js b/packages/client/src/components/dialogs/DeleteLinkDialog/index.js index bb3d9f973b..a16fd5dc9a 100644 --- a/packages/client/src/components/dialogs/DeleteLinkDialog/index.js +++ b/packages/client/src/components/dialogs/DeleteLinkDialog/index.js @@ -15,7 +15,7 @@ const DeleteLinkDialogComponent = (props) => { setIsVisible, tReady, roomId, - setExternalLinks, + deleteExternalLink, editExternalLink, } = props; @@ -45,8 +45,8 @@ const DeleteLinkDialogComponent = (props) => { newLink.access = 0; editExternalLink(roomId, newLink) - .then((res) => { - setExternalLinks(res); + .then(() => { + deleteExternalLink(newLink.sharedTo.id); toastr.success(t("Files:LinkDeletedSuccessfully")); }) .catch((err) => toastr.error(err?.message)) @@ -104,7 +104,7 @@ export default inject(({ auth, dialogsStore, publicRoomStore }) => { setDeleteLinkDialogVisible: setIsVisible, linkParams, } = dialogsStore; - const { editExternalLink, setExternalLinks } = publicRoomStore; + const { editExternalLink, deleteExternalLink } = publicRoomStore; return { visible, @@ -112,6 +112,6 @@ export default inject(({ auth, dialogsStore, publicRoomStore }) => { roomId: selectionParentRoom.id, link: linkParams.link, editExternalLink, - setExternalLinks, + deleteExternalLink, }; })(observer(DeleteLinkDialog)); diff --git a/packages/client/src/components/panels/AddUsersPanel/index.js b/packages/client/src/components/panels/AddUsersPanel/index.js index ed81d89d52..790ea973a4 100644 --- a/packages/client/src/components/panels/AddUsersPanel/index.js +++ b/packages/client/src/components/panels/AddUsersPanel/index.js @@ -6,19 +6,27 @@ import Heading from "@docspace/components/heading"; import Aside from "@docspace/components/aside"; import IconButton from "@docspace/components/icon-button"; import { ShareAccessRights } from "@docspace/common/constants"; -import PeopleSelector from "@docspace/client/src/components/PeopleSelector"; +import Selector from "@docspace/components/selector"; import { withTranslation } from "react-i18next"; import Loaders from "@docspace/common/components/Loaders"; import withLoader from "../../../HOCs/withLoader"; import toastr from "@docspace/components/toast/toastr"; import Filter from "@docspace/common/api/people/filter"; +import { getMembersList } from "@docspace/common/api/people"; +import { getUserRole } from "@docspace/common/utils"; +import DefaultUserPhoto from "PUBLIC_DIR/images/default_user_photo_size_82-82.png"; +import CatalogAccountsReactSvgUrl from "PUBLIC_DIR/images/catalog.accounts.react.svg?url"; +import EmptyScreenPersonsSvgUrl from "PUBLIC_DIR/images/empty_screen_persons.svg?url"; +import EmptyScreenPersonsSvgDarkUrl from "PUBLIC_DIR/images/empty_screen_persons_dark.svg?url"; + +let timer = null; + const AddUsersPanel = ({ isEncrypted, defaultAccess, onClose, onParentPanelClose, - shareDataItems, tempDataItems, setDataItems, t, @@ -29,6 +37,7 @@ const AddUsersPanel = ({ theme, withoutBackground, withBlur, + roomId, }) => { const accessRight = defaultAccess ? defaultAccess @@ -61,25 +70,21 @@ const AddUsersPanel = ({ const items = []; for (let item of users) { - const currentItem = shareDataItems.find((x) => x.sharedTo.id === item.id); + const currentAccess = + item.isOwner || item.isAdmin + ? ShareAccessRights.RoomManager + : access.access; - if (!currentItem) { - const currentAccess = - item.isOwner || item.isAdmin - ? ShareAccessRights.RoomManager - : access.access; - - const newItem = { - access: currentAccess, - email: item.email, - id: item.id, - displayName: item.label, - avatar: item.avatar, - isOwner: item.isOwner, - isAdmin: item.isAdmin, - }; - items.push(newItem); - } + const newItem = { + access: currentAccess, + email: item.email, + id: item.id, + displayName: item.label, + avatar: item.avatar, + isOwner: item.isOwner, + isAdmin: item.isAdmin, + }; + items.push(newItem); } if (users.length > items.length) @@ -89,22 +94,128 @@ const AddUsersPanel = ({ onClose(); }; - const onUserSelect = (owner) => { - const ownerItem = shareDataItems.find((x) => x.isOwner); - ownerItem.sharedTo = owner[0]; - - if (owner[0].key) { - owner[0].id = owner[0].key; - } - - setDataItems(shareDataItems); - onClose(); - }; - const selectedAccess = accessOptions.filter( (access) => access.access === accessRight )[0]; + const [itemsList, setItemsList] = useState(null); + const [searchValue, setSearchValue] = useState(""); + const [hasNextPage, setHasNextPage] = useState(true); + const [isNextPageLoading, setIsNextPageLoading] = useState(false); + const [total, setTotal] = useState(0); + const [isLoading, setIsLoading] = useState(false); + + const cleanTimer = () => { + timer && clearTimeout(timer); + timer = null; + }; + + useEffect(() => { + loadNextPage(0); + }, []); + + useEffect(() => { + if (isLoading) { + cleanTimer(); + timer = setTimeout(() => { + setIsLoading(true); + }, 100); + } else { + cleanTimer(); + setIsLoading(false); + } + + return () => { + cleanTimer(); + }; + }, [isLoading]); + + const onSearch = (value) => { + setSearchValue(value); + loadNextPage(0, value); + }; + + const onClearSearch = () => { + setSearchValue(""); + loadNextPage(0, ""); + }; + + const toListItem = (item) => { + const { + id, + email, + avatar, + icon, + displayName, + hasAvatar, + isOwner, + isAdmin, + isVisitor, + isCollaborator, + } = item; + + const role = getUserRole(item); + + const userAvatar = hasAvatar ? avatar : DefaultUserPhoto; + + return { + id, + email, + avatar: userAvatar, + icon, + label: displayName || email, + role, + isOwner, + isAdmin, + isVisitor, + isCollaborator, + }; + }; + + const loadNextPage = (startIndex, search = searchValue) => { + const pageCount = 100; + + setIsNextPageLoading(true); + + if (startIndex === 0) { + setIsLoading(true); + } + + const currentFilter = getFilterWithOutDisabledUser(); + + currentFilter.page = startIndex / pageCount; + currentFilter.pageCount = pageCount; + currentFilter.excludeShared = true; + + if (!!search.length) { + currentFilter.search = search; + } + + getMembersList(roomId, currentFilter) + .then((response) => { + let newItems = startIndex ? itemsList : []; + let totalDifferent = startIndex ? response.total - total : 0; + + const items = response.items.map((item) => toListItem(item)); + + newItems = [...newItems, ...items]; + + const newTotal = response.total - totalDifferent; + + setHasNextPage(newItems.length < newTotal); + setItemsList(newItems); + setTotal(newTotal); + + setIsNextPageLoading(false); + setIsLoading(false); + }) + .catch((error) => console.log(error)); + }; + + const emptyScreenImage = theme.isBase + ? EmptyScreenPersonsSvgUrl + : EmptyScreenPersonsSvgDarkUrl; + return ( <> - } + rowLoader={ + + } /> @@ -146,7 +284,9 @@ AddUsersPanel.propTypes = { }; export default inject(({ auth }) => { - return { theme: auth.settingsStore.theme }; + return { + theme: auth.settingsStore.theme, + }; })( observer( withTranslation(["SharingPanel", "PeopleTranslations", "Common"])( diff --git a/packages/client/src/components/panels/EditLinkPanel/index.js b/packages/client/src/components/panels/EditLinkPanel/index.js index 4a21e388a2..f989df63ce 100644 --- a/packages/client/src/components/panels/EditLinkPanel/index.js +++ b/packages/client/src/components/panels/EditLinkPanel/index.js @@ -21,20 +21,18 @@ import LinkBlock from "./LinkBlock"; import ToggleBlock from "./ToggleBlock"; import PasswordAccessBlock from "./PasswordAccessBlock"; import LimitTimeBlock from "./LimitTimeBlock"; -import { LinkType } from "../../../helpers/constants"; import { isMobileOnly } from "react-device-detect"; const EditLinkPanel = (props) => { const { t, roomId, - linkId, isEdit, visible, password, setIsVisible, editExternalLink, - setExternalLinks, + setExternalLink, shareLink, unsavedChangesDialogVisible, setUnsavedChangesDialog, @@ -47,7 +45,7 @@ const EditLinkPanel = (props) => { const [isLoading, setIsLoading] = useState(false); - const linkTitle = link.sharedTo.title ?? ""; + const linkTitle = link?.sharedTo?.title ?? ""; const [linkNameValue, setLinkNameValue] = useState(linkTitle); const [passwordValue, setPasswordValue] = useState(password); const [expirationDate, setExpirationDate] = useState(date); @@ -93,7 +91,9 @@ const EditLinkPanel = (props) => { return; } - const newLink = JSON.parse(JSON.stringify(link)); + const externalLink = link ?? { access: 2, sharedTo: {} }; + + const newLink = JSON.parse(JSON.stringify(externalLink)); newLink.sharedTo.title = linkNameValue; newLink.sharedTo.password = passwordAccessIsChecked ? passwordValue : null; @@ -102,10 +102,8 @@ const EditLinkPanel = (props) => { setIsLoading(true); editExternalLink(roomId, newLink) - .then((res) => { - setExternalLinks(res); - - const link = res.find((l) => l?.sharedTo?.id === linkId); + .then((link) => { + setExternalLink(link); if (isEdit) { copy(linkValue); @@ -271,25 +269,22 @@ export default inject(({ auth, dialogsStore, publicRoomStore }) => { setUnsavedChangesDialog, linkParams, } = dialogsStore; - const { externalLinks, editExternalLink, setExternalLinks } = publicRoomStore; + const { externalLinks, editExternalLink, setExternalLink } = publicRoomStore; const { isEdit } = linkParams; const linkId = linkParams?.link?.sharedTo?.id; const link = externalLinks.find((l) => l?.sharedTo?.id === linkId); - const template = externalLinks.find( - (t) => - t?.sharedTo?.isTemplate && t?.sharedTo?.linkType === LinkType.External - ); - const shareLink = link?.sharedTo?.shareLink ?? template?.sharedTo?.shareLink; + + const shareLink = link?.sharedTo?.shareLink; return { visible: editLinkPanelIsVisible, setIsVisible: setEditLinkPanelIsVisible, isEdit, - linkId: link?.sharedTo?.id ?? template?.sharedTo?.id, + linkId: link?.sharedTo?.id, editExternalLink, roomId: selectionParentRoom.id, - setExternalLinks, + setExternalLink, isLocked: !!link?.sharedTo?.password, password: link?.sharedTo?.password ?? "", date: link?.sharedTo?.expirationDate, @@ -298,7 +293,7 @@ export default inject(({ auth, dialogsStore, publicRoomStore }) => { externalLinks, unsavedChangesDialogVisible, setUnsavedChangesDialog, - link: link ?? template, + link, language: auth.language, }; })( diff --git a/packages/client/src/components/panels/InvitePanel/StyledInvitePanel.js b/packages/client/src/components/panels/InvitePanel/StyledInvitePanel.js index 48c8400c47..65c943b0c6 100644 --- a/packages/client/src/components/panels/InvitePanel/StyledInvitePanel.js +++ b/packages/client/src/components/panels/InvitePanel/StyledInvitePanel.js @@ -181,6 +181,13 @@ const StyledRow = styled.div` margin-right: 0; `} } + + .combo-button-label { + color: ${(props) => props.theme.text.disableColor}; + } + .combo-buttons_expander-icon path { + fill: ${(props) => props.theme.text.disableColor}; + } `; const StyledInviteInput = styled.div` diff --git a/packages/client/src/components/panels/InvitePanel/index.js b/packages/client/src/components/panels/InvitePanel/index.js index d3d08a9f26..30e86ef5d9 100644 --- a/packages/client/src/components/panels/InvitePanel/index.js +++ b/packages/client/src/components/panels/InvitePanel/index.js @@ -24,6 +24,9 @@ import InviteInput from "./sub-components/InviteInput"; import ExternalLinks from "./sub-components/ExternalLinks"; import Scrollbar from "@docspace/components/scrollbar"; import { LinkType } from "../../../helpers/constants"; + +import InfoBar from "./sub-components/InfoBar"; + const InvitePanel = ({ folders, getFolderInfo, @@ -52,14 +55,16 @@ const InvitePanel = ({ const [selectedRoom, setSelectedRoom] = useState(null); const [hasErrors, setHasErrors] = useState(false); const [shareLinks, setShareLinks] = useState([]); - const [roomUsers, setRoomUsers] = useState([]); const [isLoading, setIsLoading] = useState(false); const [externalLinksVisible, setExternalLinksVisible] = useState(false); const [scrollAllPanelContent, setScrollAllPanelContent] = useState(false); const [activeLink, setActiveLink] = useState({}); + const [infoBarIsVisible, setInfoBarIsVisible] = useState(true); const [addUsersPanelVisible, setAddUsersPanelVisible] = useState(false); const [isMobileView, setIsMobileView] = useState(isMobileOnly); + const onCloseBar = () => setInfoBarIsVisible(false); + const inputsRef = useRef(); const invitePanelBodyRef = useRef(); @@ -67,10 +72,6 @@ const InvitePanel = ({ setExternalLinksVisible(visible); }; - const onChangeActiveLink = (activeLink) => { - setActiveLink(activeLink); - }; - const selectRoom = () => { const room = folders.find((folder) => folder.id === roomId); @@ -84,26 +85,24 @@ const InvitePanel = ({ }; const getInfo = () => { - getRoomSecurityInfo(roomId).then((users) => { - let links = []; + getRoomSecurityInfo(roomId).then((links) => { + const link = links[0]; + if (link) { + const { shareLink, id, title, expirationDate } = link.sharedTo; - users.map((user) => { - const { shareLink, id, title, expirationDate, linkType } = - user.sharedTo; + const activeLink = { + id, + title, + shareLink, + expirationDate, + access: link.access || defaultAccess, + }; - if (!!shareLink && linkType === LinkType.Invite) { - links.push({ - id, - title, - shareLink, - expirationDate, - access: user.access || defaultAccess, - }); - } - }); + onChangeExternalLinksVisible(!!links.length); - setShareLinks(links); - setRoomUsers(users); + setShareLinks([activeLink]); + setActiveLink(activeLink); + } }); }; @@ -241,6 +240,7 @@ const InvitePanel = ({ const roomType = selectedRoom ? selectedRoom.roomType : -1; const hasInvitedUsers = !!inviteItems.length; + const hasAdmins = inviteItems.findIndex((u) => u.isAdmin || u.isOwner) > -1; const bodyInvitePanel = useMemo(() => { return ( @@ -248,11 +248,12 @@ const InvitePanel = ({ @@ -260,13 +261,15 @@ const InvitePanel = ({ + {infoBarIsVisible && hasAdmins && ( + + )} {hasInvitedUsers && ( { "Translations", "Common", "InfoPanel", + "PeopleSelector", ])(observer(InvitePanel)) ); diff --git a/packages/client/src/components/panels/InvitePanel/sub-components/AccessSelector.js b/packages/client/src/components/panels/InvitePanel/sub-components/AccessSelector.js index 89615d067f..670348c098 100644 --- a/packages/client/src/components/panels/InvitePanel/sub-components/AccessSelector.js +++ b/packages/client/src/components/panels/InvitePanel/sub-components/AccessSelector.js @@ -21,6 +21,7 @@ const AccessSelector = ({ className, standalone, isMobileView, + noBorder = false, }) => { const [horizontalOrientation, setHorizontalOrientation] = useState(false); const width = containerRef?.current?.offsetWidth - 32; @@ -64,7 +65,7 @@ const AccessSelector = ({ selectedOption={selectedOption} onSelect={onSelectAccess} accessOptions={filteredAccesses ? filteredAccesses : accessOptions} - noBorder={false} + noBorder={noBorder} directionX="right" directionY="bottom" fixedDirection={true} @@ -82,7 +83,7 @@ const AccessSelector = ({ selectedOption={selectedOption} onSelect={onSelectAccess} accessOptions={filteredAccesses ? filteredAccesses : accessOptions} - noBorder={false} + noBorder={noBorder} directionX="right" directionY="top" fixedDirection={true} diff --git a/packages/client/src/components/panels/InvitePanel/sub-components/ExternalLinks.js b/packages/client/src/components/panels/InvitePanel/sub-components/ExternalLinks.js index d4ad7ae80d..0cc47ac884 100644 --- a/packages/client/src/components/panels/InvitePanel/sub-components/ExternalLinks.js +++ b/packages/client/src/components/panels/InvitePanel/sub-components/ExternalLinks.js @@ -1,6 +1,6 @@ import MediaDownloadReactSvgUrl from "PUBLIC_DIR/images/media.download.react.svg?url"; import CopyReactSvgUrl from "PUBLIC_DIR/images/copy.react.svg?url"; -import React, { useState, useEffect, useRef, useCallback } from "react"; +import React, { useState, useRef, useCallback } from "react"; import { inject, observer } from "mobx-react"; import copy from "copy-to-clipboard"; @@ -22,6 +22,7 @@ import { StyledToggleButton, StyledDescription, } from "../StyledInvitePanel"; +import { RoomsType, ShareAccessRights } from "@docspace/common/constants"; const ExternalLinks = ({ t, @@ -29,12 +30,12 @@ const ExternalLinks = ({ roomType, defaultAccess, shareLinks, + setShareLinks, setInvitationLinks, isOwner, - getInfo, onChangeExternalLinksVisible, externalLinksVisible, - onChangeActiveLink, + setActiveLink, activeLink, isMobileView, }) => { @@ -42,44 +43,37 @@ const ExternalLinks = ({ const inputsRef = useRef(); - useEffect(() => { - if (shareLinks[0]?.expirationDate) toggleLinks(false); - }, [shareLinks]); - - const toggleLinks = (withCopy = true) => { - let link = null; - if (!shareLinks.length) return; - - if (roomId === -1) { - link = shareLinks.find((l) => l.access === +defaultAccess); - - onChangeActiveLink(link); - } else { - link = shareLinks[0]; - - !externalLinksVisible ? editLink() : disableLink(); - } - + const toggleLinks = () => { + !externalLinksVisible ? editLink() : disableLink(); onChangeExternalLinksVisible(!externalLinksVisible); - - if (!externalLinksVisible && withCopy) copyLink(link?.shareLink); }; const disableLink = () => { - setInvitationLinks(roomId, shareLinks[0].id, "Invite", 0); - setTimeout(() => getInfo(), 100); + setInvitationLinks(roomId, "Invite", 0, shareLinks[0].id); + setShareLinks([]); }; - const editLink = () => { - if (!shareLinks[0].expirationDate) { - setInvitationLinks( - roomId, - shareLinks[0].id, - "Invite", - shareLinks[0].access - ); - } - onChangeActiveLink(shareLinks[0]); + const editLink = async () => { + const type = + roomType === RoomsType.PublicRoom + ? ShareAccessRights.RoomManager + : ShareAccessRights.ReadOnly; + + const link = await setInvitationLinks(roomId, "Invite", type); + + const { shareLink, id, title, expirationDate } = link.sharedTo; + + const activeLink = { + id, + title, + shareLink, + expirationDate, + access: link.access || defaultAccess, + }; + + copyLink(shareLink); + setShareLinks([activeLink]); + setActiveLink(activeLink); }; const onSelectAccess = (access) => { @@ -87,12 +81,12 @@ const ExternalLinks = ({ if (roomId === -1) { link = shareLinks.find((l) => l.access === access.access); - onChangeActiveLink(link); + setActiveLink(link); } else { - setInvitationLinks(roomId, shareLinks[0].id, "Invite", +access.access); + setInvitationLinks(roomId, "Invite", +access.access, shareLinks[0].id); link = shareLinks[0]; - onChangeActiveLink(shareLinks[0]); + setActiveLink(shareLinks[0]); } copyLink(link.shareLink); diff --git a/packages/client/src/components/panels/InvitePanel/sub-components/InfoBar.js b/packages/client/src/components/panels/InvitePanel/sub-components/InfoBar.js new file mode 100644 index 0000000000..4709ea0f4e --- /dev/null +++ b/packages/client/src/components/panels/InvitePanel/sub-components/InfoBar.js @@ -0,0 +1,80 @@ +import React from "react"; +import { ReactSVG } from "react-svg"; +import styled from "styled-components"; +import InfoIcon from "PUBLIC_DIR/images/info.outline.react.svg?url"; +import CrossReactSvg from "PUBLIC_DIR/images/cross.react.svg?url"; +import IconButton from "@docspace/components/icon-button"; +import Text from "@docspace/components/text"; + +const StyledInfoBar = styled.div` + display: flex; + background-color: #f8f9f9; + color: #333; + font-size: 12px; + padding: 12px 16px; + border-radius: 6px; + margin-bottom: 10px; + margin: -4px 16px 20px; + .text-container { + width: 100%; + display: flex; + flex-direction: column; + } + .header-body { + display: flex; + height: fit-content; + width: 100%; + gap: 8px; + font-weight: 600; + .header-icon { + svg { + path { + fill: #ed7309; + } + } + } + } + .body-container { + color: #555f65; + font-weight: 400; + } + .close-icon { + margin: 3px 1px 0px 0px; + path { + fill: ${({ theme }) => theme.iconButton.color}; + } + svg { + weight: 10px; + height: 10px; + } + } +`; + +const InfoBar = (props) => { + const { t, iconName, onClose, ...rest } = props; + + return ( + +
+
+
+ +
+ + {t("Common:Info")} + +
+
{t("InfoPanel:InfoBanner")}
+
+ + +
+ ); +}; + +export default InfoBar; 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 1bb1e24a8b..7204e982b7 100644 --- a/packages/client/src/components/panels/InvitePanel/sub-components/InviteInput.js +++ b/packages/client/src/components/panels/InvitePanel/sub-components/InviteInput.js @@ -24,16 +24,19 @@ import { StyledDescription, } from "../StyledInvitePanel"; +import Filter from "@docspace/common/api/people/filter"; +import { getMembersList } from "@docspace/common/api/people"; + +const searchUsersThreshold = 2; + const InviteInput = ({ defaultAccess, - getUsersByQuery, hideSelector, inviteItems, onClose, roomId, roomType, setInviteItems, - roomUsers, t, isOwner, inputsRef, @@ -44,15 +47,12 @@ const InviteInput = ({ const [inputValue, setInputValue] = useState(""); const [usersList, setUsersList] = useState([]); const [searchPanelVisible, setSearchPanelVisible] = useState(false); + const [isAddEmailPanelBlocked, setIsAddEmailPanelBlocked] = useState(true); const [selectedAccess, setSelectedAccess] = useState(defaultAccess); const searchRef = useRef(); - const inRoom = (id) => { - return roomUsers.some((user) => user.sharedTo.id === id); - }; - const toUserItems = (query) => { const addresses = parseAddresses(query); const uid = () => Math.random().toString(36).slice(-6); @@ -81,10 +81,17 @@ const InviteInput = ({ const searchByQuery = async (value) => { const query = value.trim(); - if (!!query.length) { - const users = await getUsersByQuery(query); - setUsersList(users); - } else { + if (query.length >= searchUsersThreshold) { + const filter = Filter.getFilterWithOutDisabledUser(); + filter.search = query; + + const users = await getMembersList(roomId, filter); + + setUsersList(users.items); + setIsAddEmailPanelBlocked(false); + } + + if (!query) { closeInviteInputPanel(); setInputValue(""); setUsersList([]); @@ -100,11 +107,20 @@ const InviteInput = ({ const value = e.target.value; const clearValue = value.trim(); - if ((!!usersList.length || clearValue.length > 2) && !searchPanelVisible) { - openInviteInputPanel(); + setInputValue(value); + + if (clearValue.length < searchUsersThreshold) { + setUsersList([]); + setIsAddEmailPanelBlocked(true); + return; } - setInputValue(value); + if ( + (!!usersList.length || clearValue.length >= searchUsersThreshold) && + !searchPanelVisible + ) { + openInviteInputPanel(); + } if (roomId !== -1) { debouncedSearch(clearValue); @@ -124,9 +140,7 @@ const InviteInput = ({ }; const getItemContent = (item) => { - const { avatar, displayName, email, id } = item; - - const invited = inRoom(id); + const { avatar, displayName, email, id, shared } = item; item.access = selectedAccess; @@ -146,18 +160,18 @@ const InviteInput = ({
- + {displayName} {email}
- {invited && {t("Invited")}} + {shared && {t("Invited")}}
); }; @@ -203,13 +217,27 @@ const InviteInput = ({ }; const closeInviteInputPanel = (e) => { - // if (e?.target.tagName.toUpperCase() == "INPUT") return; + if (e?.target.tagName.toUpperCase() === "INPUT") return; setSearchPanelVisible(false); }; const foundUsers = usersList.map((user) => getItemContent(user)); + const addEmailPanel = isAddEmailPanelBlocked ? ( + <> + ) : ( + + {t("Common:AddButton")} «{inputValue}» + + ); + const accessOptions = getAccessOptions(t, roomType); const onSelectAccess = (item) => { @@ -277,7 +305,7 @@ const InviteInput = ({ onKeyDown={onKeyDown} /> - {inputValue.length > 2 && ( + {inputValue.length >= searchUsersThreshold && ( - {!!usersList.length ? ( - foundUsers - ) : ( - - {t("Common:AddButton")} «{inputValue}» - - )} + {!!usersList.length ? foundUsers : addEmailPanel} )} @@ -320,7 +336,6 @@ const InviteInput = ({ onParentPanelClose={onClose} onClose={closeUsersPanel} visible={addUsersPanelVisible} - shareDataItems={roomUsers} tempDataItems={inviteItems} setDataItems={addItems} accessOptions={accessOptions} @@ -329,6 +344,7 @@ const InviteInput = ({ defaultAccess={selectedAccess} withoutBackground={isMobileView} withBlur={!isMobileView} + roomId={roomId} /> )} @@ -336,16 +352,14 @@ const InviteInput = ({ ); }; -export default inject(({ auth, peopleStore, filesStore, dialogsStore }) => { +export default inject(({ auth, dialogsStore }) => { const { theme } = auth.settingsStore; const { isOwner } = auth.userStore.user; - const { getUsersByQuery } = peopleStore.usersStore; const { invitePanelOptions, setInviteItems, inviteItems } = dialogsStore; return { setInviteItems, inviteItems, - getUsersByQuery, roomId: invitePanelOptions.roomId, hideSelector: invitePanelOptions.hideSelector, defaultAccess: invitePanelOptions.defaultAccess, 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 127d0c2f15..7c0924e1af 100644 --- a/packages/client/src/components/panels/InvitePanel/sub-components/Item.js +++ b/packages/client/src/components/panels/InvitePanel/sub-components/Item.js @@ -149,6 +149,7 @@ const Item = ({ filteredAccesses={filteredAccesses} setIsOpenItemAccess={setIsOpenItemAccess} isMobileView={isMobileView} + noBorder /> )} diff --git a/packages/client/src/pages/Home/InfoPanel/Body/styles/common.js b/packages/client/src/pages/Home/InfoPanel/Body/styles/common.js index 717c0332c3..0c4d7e8689 100644 --- a/packages/client/src/pages/Home/InfoPanel/Body/styles/common.js +++ b/packages/client/src/pages/Home/InfoPanel/Body/styles/common.js @@ -63,6 +63,25 @@ const StyledTitle = styled.div` align-items: center; height: 32px; + .info_title-icons { + display: flex; + margin-left: auto; + gap: 14px; + .icon { + cursor: pointer; + path, + rect { + fill: ${(props) => props.theme.infoPanel.members.iconColor}; + } + &:hover { + path, + rect { + fill: ${(props) => props.theme.infoPanel.members.iconHoverColor}; + } + } + } + } + img { &.icon { display: flex; diff --git a/packages/client/src/pages/Home/InfoPanel/Body/styles/members.js b/packages/client/src/pages/Home/InfoPanel/Body/styles/members.js index a84b5fd710..476cd7fce2 100644 --- a/packages/client/src/pages/Home/InfoPanel/Body/styles/members.js +++ b/packages/client/src/pages/Home/InfoPanel/Body/styles/members.js @@ -6,30 +6,18 @@ const StyledUserTypeHeader = styled.div` display: flex; align-items: center; justify-content: space-between; - padding-top: ${props => (props.isExpect ? "20px" : "8px")}; + padding-top: ${(props) => (props.isExpect ? "20px" : "16px")}; padding-bottom: 12px; .title { font-weight: 600; font-size: 14px; line-height: 20px; - color: ${props => props.theme.infoPanel.members.subtitleColor}; + color: ${(props) => props.theme.infoPanel.members.subtitleColor}; } .icon { - cursor: pointer; - - path, - rect { - fill: ${props => props.theme.infoPanel.members.iconColor}; - } - - &:hover { - path, - rect { - fill: ${props => props.theme.infoPanel.members.iconHoverColor}; - } - } + margin-right: 8px; } `; @@ -56,7 +44,7 @@ const StyledUser = styled.div` white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - ${props => + ${(props) => props.isExpect && `color: ${props.theme.infoPanel.members.isExpectName}`}; } @@ -64,8 +52,8 @@ const StyledUser = styled.div` font-weight: 600; font-size: 14px; line-height: 16px; - color: ${props => props.theme.infoPanel.members.meLabelColor}; - ${props => + color: ${(props) => props.theme.infoPanel.members.meLabelColor}; + ${(props) => props.theme.interfaceDirection === "rtl" ? css` margin-right: -8px; @@ -76,7 +64,7 @@ const StyledUser = styled.div` } .role-wrapper { - ${props => + ${(props) => props.theme.interfaceDirection === "rtl" ? css` padding-right: 8px; @@ -93,8 +81,10 @@ const StyledUser = styled.div` white-space: nowrap; .disabled-role-combobox { - color: ${props => + color: ${(props) => props.theme.infoPanel.members.disabledRoleSelectorColor}; + + margin-right: 16px; } } @@ -102,14 +92,14 @@ const StyledUser = styled.div` cursor: pointer; svg { path { - fill: ${props => props.theme.iconButton.color}; + fill: ${(props) => props.theme.iconButton.color}; } } :hover { svg { path { - fill: ${props => props.theme.iconButton.hoverColor}; + fill: ${(props) => props.theme.iconButton.hoverColor}; } } } diff --git a/packages/client/src/pages/Home/InfoPanel/Body/sub-components/ItemTitle/FilesItemTitle.js b/packages/client/src/pages/Home/InfoPanel/Body/sub-components/ItemTitle/FilesItemTitle.js index 2b82158909..dd912f1038 100644 --- a/packages/client/src/pages/Home/InfoPanel/Body/sub-components/ItemTitle/FilesItemTitle.js +++ b/packages/client/src/pages/Home/InfoPanel/Body/sub-components/ItemTitle/FilesItemTitle.js @@ -1,16 +1,26 @@ import React, { useRef } from "react"; import { inject, observer } from "mobx-react"; import { withTranslation } from "react-i18next"; -import { ReactSVG } from "react-svg"; - -import { Text } from "@docspace/components"; - +import PersonPlusReactSvgUrl from "PUBLIC_DIR/images/person+.react.svg?url"; +import IconButton from "@docspace/components/icon-button"; +import Text from "@docspace/components/text"; import ItemContextOptions from "./ItemContextOptions"; - import { StyledTitle } from "../../styles/common"; import RoomIcon from "@docspace/client/src/components/RoomIcon"; -const FilesItemTitle = ({ t, selection, isSeveralItems }) => { +import { RoomsType, ShareAccessRights } from "@docspace/common/constants"; + +const FilesItemTitle = ({ + t, + selection, + isSeveralItems, + selectionParentRoom, + setIsMobileHidden, + isGracePeriod, + setInvitePanelOptions, + setInviteUsersWarningDialogVisible, + isPublicRoomType, +}) => { const itemTitleRef = useRef(); if (isSeveralItems) return <>; @@ -18,6 +28,27 @@ const FilesItemTitle = ({ t, selection, isSeveralItems }) => { const icon = selection.icon; const isLoadedRoomIcon = !!selection.logo?.medium; const showDefaultRoomIcon = !isLoadedRoomIcon && selection.isRoom; + const security = selectionParentRoom ? selectionParentRoom.security : {}; + const canInviteUserInRoomAbility = security?.EditAccess; + + const onClickInviteUsers = () => { + setIsMobileHidden(true); + const parentRoomId = selectionParentRoom.id; + + if (isGracePeriod) { + setInviteUsersWarningDialogVisible(true); + return; + } + + setInvitePanelOptions({ + visible: true, + roomId: parentRoomId, + hideSelector: false, + defaultAccess: isPublicRoomType + ? ShareAccessRights.RoomManager + : ShareAccessRights.ReadOnly, + }); + }; return ( @@ -37,21 +68,57 @@ const FilesItemTitle = ({ t, selection, isSeveralItems }) => { )} {selection.title} - {selection && ( - - )} +
+ {canInviteUserInRoomAbility && ( + + )} + {selection && ( + + )} +
); }; -export default withTranslation([ - "Files", - "Common", - "Translations", - "InfoPanel", - "SharingPanel", -])(observer(FilesItemTitle)); +export default inject(({ auth, dialogsStore, selectedFolderStore }) => { + const { selectionParentRoom, setIsMobileHidden } = auth.infoPanelStore; + const { isGracePeriod } = auth.currentTariffStatusStore; + + const { setInvitePanelOptions, setInviteUsersWarningDialogVisible } = + dialogsStore; + + const roomType = + selectedFolderStore.roomType ?? selectionParentRoom?.roomType; + + const isPublicRoomType = + roomType === RoomsType.PublicRoom || roomType === RoomsType.CustomRoom; + + return { + selectionParentRoom, + setIsMobileHidden, + isGracePeriod, + setInvitePanelOptions, + setInviteUsersWarningDialogVisible, + isPublicRoomType, + }; +})( + withTranslation([ + "Files", + "Common", + "Translations", + "InfoPanel", + "SharingPanel", + ])(observer(FilesItemTitle)) +); diff --git a/packages/client/src/pages/Home/InfoPanel/Body/sub-components/ItemTitle/ItemContextOptions.js b/packages/client/src/pages/Home/InfoPanel/Body/sub-components/ItemTitle/ItemContextOptions.js index 8bbbda9a92..4f36210ea7 100644 --- a/packages/client/src/pages/Home/InfoPanel/Body/sub-components/ItemTitle/ItemContextOptions.js +++ b/packages/client/src/pages/Home/InfoPanel/Body/sub-components/ItemTitle/ItemContextOptions.js @@ -6,17 +6,6 @@ import { ContextMenu, ContextMenuButton } from "@docspace/components"; import ContextHelper from "../../helpers/ContextHelper"; -const StyledItemContextOptions = styled.div` - ${props => - props.theme.interfaceDirection === "rtl" - ? css` - margin-right: auto; - ` - : css` - margin-left: auto; - `} -`; - const ItemContextOptions = ({ t, selection, @@ -70,7 +59,7 @@ const ItemContextOptions = ({ }; return ( - + <> )} - + ); }; diff --git a/packages/client/src/pages/Home/InfoPanel/Body/sub-components/ItemTitle/index.js b/packages/client/src/pages/Home/InfoPanel/Body/sub-components/ItemTitle/index.js index f7902f1b77..832b623396 100644 --- a/packages/client/src/pages/Home/InfoPanel/Body/sub-components/ItemTitle/index.js +++ b/packages/client/src/pages/Home/InfoPanel/Body/sub-components/ItemTitle/index.js @@ -49,7 +49,6 @@ const ItemTitle = ({ selectionLength={selectionLength} selection={filesItemSelection} isSeveralItems={isSeveralItems} - getIcon={getIcon} /> ); }; diff --git a/packages/client/src/pages/Home/InfoPanel/Body/views/Members/MembersList.js b/packages/client/src/pages/Home/InfoPanel/Body/views/Members/MembersList.js new file mode 100644 index 0000000000..6bc96823f7 --- /dev/null +++ b/packages/client/src/pages/Home/InfoPanel/Body/views/Members/MembersList.js @@ -0,0 +1,191 @@ +import React, { useState, useCallback, useEffect, memo } from "react"; +import styled from "styled-components"; +import { FixedSizeList as List, areEqual } from "react-window"; +import AutoSizer from "react-virtualized-auto-sizer"; +import CustomScrollbarsVirtualList from "@docspace/components/scrollbar/custom-scrollbars-virtual-list"; +import InfiniteLoader from "react-window-infinite-loader"; +import User from "./User"; +import { tablet, mobile, isMobile } from "@docspace/components/utils/device"; + +const StyledMembersList = styled.div` + height: ${({ withBanner, isPublicRoomType }) => + isPublicRoomType + ? withBanner + ? "calc(100vh - 442px)" + : "calc(100vh - 286px)" + : "calc(100vh - 266px)"}; + + @media ${tablet} { + height: ${({ withBanner, isPublicRoomType }) => + isPublicRoomType + ? withBanner + ? "calc(100vh - 362px)" + : "calc(100vh - 206px)" + : "calc(100vh - 186px)"}; + } + + @media ${mobile} { + height: ${({ withBanner, isPublicRoomType }) => + isPublicRoomType + ? withBanner + ? "calc(100vh - 426px)" + : "calc(100vh - 270px)" + : "calc(100vh - 250px)"}; + } +`; + +const Item = memo(({ data, index, style }) => { + const { + t, + members, + setMembers, + security, + membersHelper, + currentMember, + updateRoomMemberRole, + selectionParentRoom, + setSelectionParentRoom, + changeUserType, + setIsScrollLocked, + canInviteUserInRoomAbility, + onRepeatInvitation, + } = data; + + const user = members[index]; + + return ( +
+ +
+ ); +}, areEqual); + +const MembersList = (props) => { + const { + t, + security, + membersHelper, + currentMember, + updateRoomMemberRole, + selectionParentRoom, + setSelectionParentRoom, + changeUserType, + setIsScrollLocked, + members, + setMembers, + hasNextPage, + itemCount, + onRepeatInvitation, + loadNextPage, + isPublicRoomType, + withBanner, + } = props; + + const itemsCount = members.length; + + const canInviteUserInRoomAbility = security?.EditAccess; + const [isNextPageLoading, setIsNextPageLoading] = useState(false); + const [isMobileView, setIsMobileView] = useState(isMobile()); + + const onResize = () => { + const isMobileView = isMobile(); + setIsMobileView(isMobileView); + }; + + useEffect(() => { + window.addEventListener("resize", onResize); + + return () => { + window.removeEventListener("resize", onResize); + }; + }); + + const isItemLoaded = useCallback( + (index) => { + return !hasNextPage || index < itemsCount; + }, + [hasNextPage, itemsCount] + ); + + const loadMoreItems = useCallback( + async (startIndex) => { + setIsNextPageLoading(true); + if (!isNextPageLoading) { + await loadNextPage(startIndex - 1); + } + setIsNextPageLoading(false); + }, + [isNextPageLoading, loadNextPage] + ); + + return ( + + + {({ height, width }) => ( + + {({ onItemsRendered, ref }) => { + const listWidth = isMobileView ? width + 16 : width + 20; // for scroll + + return ( + + {Item} + + ); + }} + + )} + + + ); +}; + +export default MembersList; diff --git a/packages/client/src/pages/Home/InfoPanel/Body/views/Members/User.js b/packages/client/src/pages/Home/InfoPanel/Body/views/Members/User.js index 241ca42213..6b987785ab 100644 --- a/packages/client/src/pages/Home/InfoPanel/Body/views/Members/User.js +++ b/packages/client/src/pages/Home/InfoPanel/Body/views/Members/User.js @@ -9,10 +9,15 @@ import { isMobileOnly } from "react-device-detect"; import { decode } from "he"; import { filterUserRoleOptions } from "SRC_DIR/helpers/utils"; import { getUserRole } from "@docspace/common/utils"; +import Text from "@docspace/components/text"; +import EmailPlusReactSvgUrl from "PUBLIC_DIR/images/e-mail+.react.svg?url"; +import { StyledUserTypeHeader } from "../../styles/members"; +import IconButton from "@docspace/components/icon-button"; const User = ({ t, user, + setMembers, isExpect, membersHelper, currentMember, @@ -21,6 +26,9 @@ const User = ({ setSelectionParentRoom, changeUserType, setIsScrollLocked, + isTitle, + onRepeatInvitation, + showInviteIcon, }) => { if (!selectionParentRoom) return null; if (!user.displayName && !user.email) return null; @@ -48,22 +56,45 @@ const User = ({ }) .then(() => { setIsLoading(false); - const inRoomMembers = selectionParentRoom.members.inRoom; + const users = selectionParentRoom.members.users; + const administrators = selectionParentRoom.members.administrators; const expectedMembers = selectionParentRoom.members.expected; if (option.key === "remove") { + setMembers({ + users: users?.filter((m) => m.id !== user.id), + administrators: administrators?.filter((m) => m.id !== user.id), + expected: expectedMembers?.filter((m) => m.id !== user.id), + }); + setSelectionParentRoom({ ...selectionParentRoom, members: { - inRoom: inRoomMembers?.filter((m) => m.id !== user.id), + users: users?.filter((m) => m.id !== user.id), + administrators: administrators?.filter((m) => m.id !== user.id), expected: expectedMembers?.filter((m) => m.id !== user.id), }, }); //setUserIsRemoved(true); } else { + setMembers({ + users: users?.map((m) => + m.id === user.id ? { ...m, access: option.access } : m + ), + administrators: administrators?.map((m) => + m.id === user.id ? { ...m, access: option.access } : m + ), + expected: expectedMembers?.map((m) => + m.id === user.id ? { ...m, access: option.access } : m + ), + }); + setSelectionParentRoom({ ...selectionParentRoom, members: { - inRoom: inRoomMembers?.map((m) => + users: users?.map((m) => + m.id === user.id ? { ...m, access: option.access } : m + ), + administrators: administrators?.map((m) => m.id === user.id ? { ...m, access: option.access } : m ), expected: expectedMembers?.map((m) => @@ -126,7 +157,22 @@ const User = ({ user.isOwner ? t("Common:DocSpaceOwner") : t("Common:DocSpaceAdmin") }. ${t("Common:HasFullAccess")}`; - return ( + return isTitle ? ( + + {user.displayName} + + {showInviteIcon && ( + + )} + + ) : ( { const membersHelper = new MembersHelper({ t }); const [members, setMembers] = useState(null); - const [showLoader, setShowLoader] = useState(false); const security = selectionParentRoom ? selectionParentRoom.security : {}; - const canInviteUserInRoomAbility = security?.EditAccess; + const fetchMembers = async (roomId, clearFilter = true) => { + const isPublic = selection?.roomType ?? selectionParentRoom?.roomType; + const requests = [getRoomMembers(roomId, clearFilter)]; - const fetchMembers = async (roomId) => { - let timerId; - if (members) timerId = setTimeout(() => setShowLoader(true), 1000); - let data = await getRoomMembers(roomId); + if (isPublic && clearFilter) { + requests.push(getRoomLinks(roomId)); + } - setExternalLinks(data); + const [data, links] = await Promise.all(requests); - data = data.filter((m) => m.sharedTo.email || m.sharedTo.displayName); - clearTimeout(timerId); + links && setExternalLinks(links); - let inRoomMembers = []; - let expectedMembers = []; + const users = []; + const administrators = []; + const expectedMembers = []; data.map((fetchedMember) => { const member = { access: fetchedMember.access, canEditAccess: fetchedMember.canEditAccess, ...fetchedMember.sharedTo, }; - if (member.activationStatus !== 2) inRoomMembers.push(member); - else expectedMembers.push(member); + + if (member.activationStatus === EmployeeActivationStatus.Pending) { + member.isExpect = true; + expectedMembers.push(member); + } else if ( + member.access === ShareAccessRights.FullAccess || + member.access === ShareAccessRights.RoomManager + ) { + administrators.push(member); + } else { + users.push(member); + } }); - setShowLoader(false); + let hasPrevAdminsTitle = + members?.roomId === roomId + ? getHasPrevTitle(members?.administrators, "administration") + : false; + + if (administrators.length && !hasPrevAdminsTitle) { + administrators.unshift({ + id: "administration", + displayName: t("Administration"), + isTitle: true, + }); + } + + let hasPrevUsersTitle = + members?.roomId === roomId + ? getHasPrevTitle(members?.users, "user") + : false; + + if (users.length && !hasPrevUsersTitle) { + users.unshift({ id: "user", displayName: t("Users"), isTitle: true }); + } + + let hasPrevExpectedTitle = + members?.roomId === roomId + ? getHasPrevTitle(members?.expected, "expected") + : false; + + if (expectedMembers.length && !hasPrevExpectedTitle) { + expectedMembers.unshift({ + id: "expected", + displayName: t("ExpectUsers"), + isTitle: true, + isExpect: true, + }); + } + setUpdateRoomMembers(false); + return { - inRoom: inRoomMembers, + users, + administrators, expected: expectedMembers, + roomId, }; }; - const updateSelectionParentRoomAction = useCallback(async () => { - if (!selectionParentRoom) return; - - if (selectionParentRoom.members) { - setMembers(selectionParentRoom.members); - return; - } - - const fetchedMembers = await fetchMembers(selectionParentRoom.id); - setSelectionParentRoom({ - ...selectionParentRoom, - members: fetchedMembers, - }); - }, [selectionParentRoom]); - - useEffect(() => { - updateSelectionParentRoomAction(); - }, [selectionParentRoom, updateSelectionParentRoomAction]); + const getHasPrevTitle = (array, type) => { + return array.findIndex((x) => x.id === type) > -1; + }; const updateSelectionParentRoomActionSelection = useCallback(async () => { if (!selection.isRoom) return; const fetchedMembers = await fetchMembers(selection.id); + + setMembers(fetchedMembers); + setSelectionParentRoom({ ...selection, members: fetchedMembers, @@ -129,6 +158,7 @@ const Members = ({ ...selectionParentRoom, members: fetchedMembers, }); + setMembers(fetchedMembers); }, [selectionParentRoom, selection?.id, updateRoomMembers]); @@ -141,135 +171,74 @@ const Members = ({ updateMembersAction, ]); - const onClickInviteUsers = () => { - setIsMobileHidden(true); - const parentRoomId = selectionParentRoom.id; - - if (isGracePeriod) { - setInviteUsersWarningDialogVisible(true); - return; - } - - setInvitePanelOptions({ - visible: true, - roomId: parentRoomId, - hideSelector: false, - defaultAccess: isPublicRoomType - ? ShareAccessRights.RoomManager - : ShareAccessRights.ReadOnly, - }); - }; - const onRepeatInvitation = async () => { - const userIds = members.expected.map((user) => user.id); - resendEmailInvitations(selectionParentRoom.id, userIds) + resendEmailInvitations(selectionParentRoom.id, true) .then(() => toastr.success(t("PeopleTranslations:SuccessSentMultipleInvitatios")) ) .catch((err) => toastr.error(err)); }; - if (showLoader) return ; if (!selectionParentRoom || !members) return null; - const [currentMember] = members.inRoom.filter( + const [currentMember] = members.administrators.filter( (member) => member.id === selfId ); + const loadNextPage = async () => { + const roomId = selectionParentRoom.id; + const fetchedMembers = await fetchMembers(roomId, false); + const { users, administrators, expected } = fetchedMembers; + + const newMembers = { + administrators: [...members.administrators, ...administrators], + users: [...members.users, ...users], + expected: [...members.expected, ...expected], + }; + + setMembers(newMembers); + }; + + const { administrators, users, expected } = members; + const membersList = [...administrators, ...users, ...expected]; + + const adminsTitleCount = administrators.length ? 1 : 0; + const usersTitleCount = users.length ? 1 : 0; + const expectedTitleCount = expected.length ? 1 : 0; + + const headersCount = adminsTitleCount + usersTitleCount + expectedTitleCount; + return ( <> {isPublicRoomType && } - - - - {t("UsersInRoom")} : {members.inRoom.length} - - {canInviteUserInRoomAbility && ( - - )} - - - - {Object.values(members.inRoom).map((user) => ( - - ))} - - - {!!members.expected.length && ( - - {t("PendingInvitations")} - {canInviteUserInRoomAbility && ( - - )} - - )} - - - {Object.values(members.expected).map((user, i) => ( - - ))} - + 0} + setMembers={setMembers} + /> ); }; export default inject( - ({ - auth, - filesStore, - peopleStore, - dialogsStore, - selectedFolderStore, - publicRoomStore, - }) => { + ({ auth, filesStore, peopleStore, selectedFolderStore, publicRoomStore }) => { const { - setIsMobileHidden, selectionParentRoom, - + selection, setSelectionParentRoom, setView, roomsView, @@ -279,15 +248,17 @@ export default inject( setIsScrollLocked, } = auth.infoPanelStore; - const { getRoomMembers, updateRoomMemberRole, resendEmailInvitations } = - filesStore; + const { + getRoomMembers, + getRoomLinks, + updateRoomMemberRole, + resendEmailInvitations, + membersFilter, + } = filesStore; const { id: selfId } = auth.userStore.user; - const { isGracePeriod } = auth.currentTariffStatusStore; - const { setInvitePanelOptions, setInviteUsersWarningDialogVisible } = - dialogsStore; const { changeType: changeUserType } = peopleStore; - const { setExternalLinks } = publicRoomStore; + const { roomLinks, setExternalLinks } = publicRoomStore; const roomType = selectedFolderStore.roomType ?? selectionParentRoom?.roomType; @@ -298,13 +269,13 @@ export default inject( return { setView, roomsView, - setIsMobileHidden, selectionParentRoom, setSelectionParentRoom, setIsScrollLocked, getRoomMembers, + getRoomLinks, updateRoomMemberRole, updateRoomMembers, @@ -312,13 +283,12 @@ export default inject( selfId, - setInvitePanelOptions, - setInviteUsersWarningDialogVisible, resendEmailInvitations, changeUserType, - isGracePeriod, isPublicRoomType, setExternalLinks, + membersFilter, + externalLinks: roomLinks, }; } )( diff --git a/packages/client/src/pages/Home/InfoPanel/Body/views/Members/sub-components/LinkRow.js b/packages/client/src/pages/Home/InfoPanel/Body/views/Members/sub-components/LinkRow.js index b1e870a676..3aed9275d4 100644 --- a/packages/client/src/pages/Home/InfoPanel/Body/views/Members/sub-components/LinkRow.js +++ b/packages/client/src/pages/Home/InfoPanel/Body/views/Members/sub-components/LinkRow.js @@ -40,15 +40,8 @@ const LinkRow = (props) => { const [isLoading, setIsLoading] = useState(false); - const { - title, - shareLink, - id, - password, - disabled, - expirationDate, - isExpired, - } = link.sharedTo; + const { title, shareLink, password, disabled, expirationDate, isExpired } = + link.sharedTo; const isLocked = !!password; const expiryDate = !!expirationDate; @@ -76,8 +69,8 @@ const LinkRow = (props) => { newLink.sharedTo.disabled = !newLink.sharedTo.disabled; editExternalLink(roomId, newLink) - .then((res) => { - setExternalLink(id, res); + .then((link) => { + setExternalLink(link); disabled ? toastr.success(t("Files:LinkEnabledSuccessfully")) diff --git a/packages/client/src/pages/Home/InfoPanel/Body/views/Members/sub-components/PublicRoomBar.js b/packages/client/src/pages/Home/InfoPanel/Body/views/Members/sub-components/PublicRoomBar.js index 33f124ee95..ebd2059fb0 100644 --- a/packages/client/src/pages/Home/InfoPanel/Body/views/Members/sub-components/PublicRoomBar.js +++ b/packages/client/src/pages/Home/InfoPanel/Body/views/Members/sub-components/PublicRoomBar.js @@ -30,7 +30,6 @@ const PublicRoomBar = (props) => { size={8} iconName={CrossReactSvg} onClick={onClose} - color="#657077" /> */} ); diff --git a/packages/client/src/pages/Home/InfoPanel/Body/views/Members/sub-components/StyledPublicRoom.js b/packages/client/src/pages/Home/InfoPanel/Body/views/Members/sub-components/StyledPublicRoom.js index 8dfb5384a8..1315c50670 100644 --- a/packages/client/src/pages/Home/InfoPanel/Body/views/Members/sub-components/StyledPublicRoom.js +++ b/packages/client/src/pages/Home/InfoPanel/Body/views/Members/sub-components/StyledPublicRoom.js @@ -36,6 +36,10 @@ const StyledPublicRoomBar = styled.div` .close-icon { margin: -5px -17px 0 0; + path { + fill: ${({ theme }) => theme.iconButton.color}; + } + svg { weight: 8px; height: 8px; diff --git a/packages/client/src/pages/Home/InfoPanel/Header/index.js b/packages/client/src/pages/Home/InfoPanel/Header/index.js index 272f97b2db..5443e420c4 100644 --- a/packages/client/src/pages/Home/InfoPanel/Header/index.js +++ b/packages/client/src/pages/Home/InfoPanel/Header/index.js @@ -126,12 +126,14 @@ const InfoPanelHeaderContent = (props) => { style={{ width: "100%" }} data={roomsSubmenu} forsedActiveItemId={roomsView} + size="scale" /> ) : ( )} diff --git a/packages/client/src/pages/Home/Section/Body/TilesView/sub-components/TileContainer.js b/packages/client/src/pages/Home/Section/Body/TilesView/sub-components/TileContainer.js index 735405b468..927a3726ff 100644 --- a/packages/client/src/pages/Home/Section/Body/TilesView/sub-components/TileContainer.js +++ b/packages/client/src/pages/Home/Section/Body/TilesView/sub-components/TileContainer.js @@ -10,7 +10,7 @@ import InfiniteGrid from "./InfiniteGrid"; const paddingCss = css` @media ${desktop} { - ${props => + ${(props) => props.theme.interfaceDirection === "rtl" ? css` margin-right: 1px; @@ -23,7 +23,7 @@ const paddingCss = css` } @media ${tablet} { - ${props => + ${(props) => props.theme.interfaceDirection === "rtl" ? css` margin-right: -1px; @@ -36,12 +36,12 @@ const paddingCss = css` const StyledGridWrapper = styled.div` display: grid; - grid-template-columns: ${props => + grid-template-columns: ${(props) => props.isRooms ? "repeat(auto-fill, minmax(274px, 1fr))" : "repeat(auto-fill, minmax(216px, 1fr))"}; width: 100%; - margin-bottom: ${props => (props.isFolders || props.isRooms ? "23px" : 0)}; + margin-bottom: ${(props) => (props.isFolders || props.isRooms ? "23px" : 0)}; box-sizing: border-box; ${paddingCss}; @@ -90,7 +90,7 @@ const StyledTileContainer = styled.div` cursor: pointer !important; .sort-combo-box { - ${props => + ${(props) => props.theme.interfaceDirection === "rtl" ? css` margin-left: 3px; @@ -119,14 +119,14 @@ const StyledTileContainer = styled.div` .option-item__icon { display: none; cursor: pointer; - ${props => + ${(props) => props.isDesc && css` transform: rotate(180deg); `} path { - fill: ${props => props.theme.filterInput.sort.sortFill}; + fill: ${(props) => props.theme.filterInput.sort.sortFill}; } } @@ -138,7 +138,7 @@ const StyledTileContainer = styled.div` } .selected-option-item { - background: ${props => + background: ${(props) => props.theme.filterInput.sort.hoverBackground}; cursor: auto; @@ -156,10 +156,10 @@ const StyledTileContainer = styled.div` font-size: 12px; font-weight: 600; - color: ${props => props.theme.filterInput.sort.tileSortColor}; + color: ${(props) => props.theme.filterInput.sort.tileSortColor}; .sort-icon { - ${props => + ${(props) => props.theme.interfaceDirection === "rtl" ? css` margin-left: 8px; @@ -169,7 +169,7 @@ const StyledTileContainer = styled.div` `} svg { path { - fill: ${props => props.theme.filterInput.sort.tileSortFill}; + fill: ${(props) => props.theme.filterInput.sort.tileSortFill}; } } } @@ -183,7 +183,7 @@ const StyledTileContainer = styled.div` } @media ${tablet} { - ${props => + ${(props) => props.theme.interfaceDirection === "rtl" ? css` margin-left: -3px; @@ -217,7 +217,7 @@ class TileContainer extends React.PureComponent { const Folders = []; const Files = []; - React.Children.map(children, item => { + React.Children.map(children, (item) => { const { isFolder, isRoom, fileExst, id } = item.props.item; if ((isFolder || id === -1) && !fileExst && !isRoom) { Folders.push( @@ -254,7 +254,8 @@ class TileContainer extends React.PureComponent { + className="tile-items-heading" + > {headingFolders} )} @@ -287,7 +288,8 @@ class TileContainer extends React.PureComponent { className={`${className} files-tile-container`} style={style} useReactWindow={useReactWindow} - isDesc={isDesc}> + isDesc={isDesc} + > {useReactWindow ? ( {renderTile} ) : ( diff --git a/packages/client/src/pages/PortalSettings/categories/security/audit-trail/RowView/AuditContent.js b/packages/client/src/pages/PortalSettings/categories/security/audit-trail/RowView/AuditContent.js index 35630de3e7..f9587411e4 100644 --- a/packages/client/src/pages/PortalSettings/categories/security/audit-trail/RowView/AuditContent.js +++ b/packages/client/src/pages/PortalSettings/categories/security/audit-trail/RowView/AuditContent.js @@ -10,7 +10,6 @@ const StyledRowContent = styled(RowContent)` .row-main-container-wrapper { display: flex; justify-content: flex-start; - width: min-content; } ${(props) => props.isSettingNotPaid && UnavailableStyles} @@ -27,8 +26,7 @@ export const AuditContent = ({ sectionWidth, item, isSettingNotPaid }) => { sideColor="#A3A9AE" nameColor="#D0D5DA" sectionWidth={sectionWidth} - isSettingNotPaid={isSettingNotPaid} - > + isSettingNotPaid={isSettingNotPaid}>
{item.user} @@ -40,16 +38,14 @@ export const AuditContent = ({ sectionWidth, item, isSettingNotPaid }) => { fontSize="12px" fontWeight={600} truncate={true} - className="settings_unavailable" - > + className="settings_unavailable"> {dateStr} + className="settings_unavailable"> {`${item.context ? item.context + " |" : ""} ${item.action}`} diff --git a/packages/client/src/store/FilesStore.js b/packages/client/src/store/FilesStore.js index 3590b61f33..58317cc366 100644 --- a/packages/client/src/store/FilesStore.js +++ b/packages/client/src/store/FilesStore.js @@ -30,6 +30,7 @@ import { getContextMenuKeysByType } from "SRC_DIR/helpers/plugins"; import { PluginContextMenuItemType } from "SRC_DIR/helpers/plugins/constants"; import { CategoryType } from "SRC_DIR/helpers/constants"; import debounce from "lodash.debounce"; +import clone from "lodash/clone"; import Queue from "queue-promise"; const { FilesFilter, RoomsFilter } = api; @@ -80,8 +81,9 @@ class FilesStore { bufferSelection = null; selected = "close"; - filter = FilesFilter.getDefault(); //TODO: FILTER + filter = FilesFilter.getDefault(); roomsFilter = RoomsFilter.getDefault(); + membersFilter = { page: 0, pageCount: 100, total: 0 }; categoryType = getCategoryType(window.location); @@ -2426,9 +2428,48 @@ class FilesStore { return api.rooms.removeLogoFromRoom(id); } - getRoomMembers(id) { - return api.rooms.getRoomMembers(id); - } + getDefaultMembersFilter = () => { + return { page: 0, pageCount: 100, total: 0 }; + }; + + setRoomMembersFilter = (roomMembersFilter) => { + this.roomMembersFilter = roomMembersFilter; + }; + + getRoomMembers = (id, clearFilter = true) => { + let newFilter = this.membersFilter; + + if (clearFilter) { + newFilter = this.getDefaultMembersFilter(); + } else { + newFilter.page += 1; + } + this.setRoomMembersFilter(newFilter); + + const membersFilters = { + startIndex: newFilter.page * newFilter.pageCount, + count: newFilter.pageCount, + filterType: 0, // 0 (Members) + }; + + return api.rooms.getRoomMembers(id, membersFilters).then((res) => { + const newFilter = clone(this.membersFilter); + newFilter.total = res.total; + this.setMembersFilter(newFilter); + + return res.items; + }); + }; + + setMembersFilter = (filter) => { + this.membersFilter = filter; + }; + + getRoomLinks = (id) => { + return api.rooms + .getRoomMembers(id, { filterType: 2 }) // 2 (External link) + .then((res) => res.items); + }; updateRoomMemberRole(id, data) { return api.rooms.updateRoomMemberRole(id, data); @@ -3623,16 +3664,16 @@ class FilesStore { return Math.floor(sectionWidth / minTileWidth); }; - setInvitationLinks = async (roomId, linkId, title, access) => { + setInvitationLinks = async (roomId, title, access, linkId) => { return await api.rooms.setInvitationLinks(roomId, linkId, title, access); }; - resendEmailInvitations = async (id, usersIds) => { - return await api.rooms.resendEmailInvitations(id, usersIds); + resendEmailInvitations = async (id, resendAll) => { + return await api.rooms.resendEmailInvitations(id, resendAll); }; getRoomSecurityInfo = async (id) => { - return await api.rooms.getRoomSecurityInfo(id); + return await api.rooms.getRoomSecurityInfo(id).then((res) => res.items); }; setRoomSecurity = async (id, data) => { diff --git a/packages/client/src/store/PublicRoomStore.js b/packages/client/src/store/PublicRoomStore.js index 089acd0e84..7bb05fef43 100644 --- a/packages/client/src/store/PublicRoomStore.js +++ b/packages/client/src/store/PublicRoomStore.js @@ -45,12 +45,25 @@ class PublicRoomStore { this.externalLinks = externalLinks; }; - setExternalLink = (linkId, data) => { - const linkIndex = this.externalLinks.findIndex( - (l) => l.sharedTo.id === linkId + deleteExternalLink = (linkId) => { + const externalLinks = this.externalLinks.filter( + (l) => l.sharedTo.id !== linkId ); - const dataLink = data.find((l) => l.sharedTo.id === linkId); - this.externalLinks[linkIndex] = dataLink; + this.externalLinks = externalLinks; + }; + + setExternalLink = (link) => { + const linkIndex = this.externalLinks.findIndex( + (l) => l.sharedTo.id === link.sharedTo.id + ); + const externalLinks = this.externalLinks; + + if (linkIndex === -1) { + externalLinks.push(link); + this.externalLinks = externalLinks; + } else { + externalLinks[linkIndex] = link; + } }; setExternalLinks = (links) => { diff --git a/packages/common/api/people/index.js b/packages/common/api/people/index.js index fe1c6dd8f6..5c3ae55094 100644 --- a/packages/common/api/people/index.js +++ b/packages/common/api/people/index.js @@ -358,3 +358,36 @@ export function getUsersByQuery(query) { url: `/people/search?query=${query}`, }); } + +export function getMembersList(roomId, filter = Filter.getDefault()) { + let params = ""; + + if (filter) { + checkFilterInstance(filter, Filter); + + params = `?${filter.toApiUrlParams( + "id,email,avatar,icon,displayName,hasAvatar,isOwner,isAdmin,isVisitor,isCollaborator," + )}`; + } + + const excludeShared = filter.excludeShared ? filter.excludeShared : false; + + if (params) { + params += `&excludeShared=${excludeShared}`; + } else { + params = `excludeShared=${excludeShared}`; + } + + return request({ + method: "get", + url: `people/room/${roomId}${params}`, + }).then((res) => { + res.items = res.items.map((user) => { + if (user && user.displayName) { + user.displayName = Encoder.htmlDecode(user.displayName); + } + return user; + }); + return res; + }); +} diff --git a/packages/common/api/rooms/index.js b/packages/common/api/rooms/index.js index 6e4e3a49b9..12ec2c4e11 100644 --- a/packages/common/api/rooms/index.js +++ b/packages/common/api/rooms/index.js @@ -1,5 +1,9 @@ import { request } from "../client"; -import { checkFilterInstance, decodeDisplayName } from "../../utils"; +import { + checkFilterInstance, + decodeDisplayName, + toUrlParams, +} from "../../utils"; import { FolderType } from "../../constants"; import RoomsFilter from "./filter"; @@ -45,10 +49,17 @@ export function getRoomInfo(id) { }); } -export function getRoomMembers(id) { +export function getRoomMembers(id, filter) { + let params = ""; + const str = toUrlParams(filter); + + if (str) { + params = `?${str}`; + } + const options = { method: "get", - url: `/files/rooms/${id}/share`, + url: `/files/rooms/${id}/share${params}`, }; return request(options).then((res) => { @@ -288,12 +299,12 @@ export const setInvitationLinks = async (roomId, linkId, title, access) => { return res; }; -export const resendEmailInvitations = async (id, usersIds) => { +export const resendEmailInvitations = async (id, resendAll = true) => { const options = { method: "post", url: `/files/rooms/${id}/resend`, data: { - usersIds, + resendAll, }, }; @@ -302,10 +313,11 @@ export const resendEmailInvitations = async (id, usersIds) => { return res; }; +//// 1 (Invitation link) export const getRoomSecurityInfo = async (id) => { const options = { method: "get", - url: `/files/rooms/${id}/share`, + url: `/files/rooms/${id}/share?filterType=1`, }; const res = await request(options); diff --git a/packages/components/submenu/index.js b/packages/components/submenu/index.js index cc1c4ec3dd..f2b9242157 100644 --- a/packages/components/submenu/index.js +++ b/packages/components/submenu/index.js @@ -21,6 +21,7 @@ const Submenu = (props) => { startSelect = 0, forsedActiveItemId, onSelect, + size, ...rest } = props; if (!data) return null; @@ -105,7 +106,7 @@ const Submenu = (props) => {
- + {data.map((d) => { const isActive = @@ -144,9 +145,14 @@ const Submenu = (props) => { ); })} + {size !== "scale" && ( + + )} - + {size === "scale" && ( + + )}
@@ -164,6 +170,8 @@ Submenu.propTypes = { startSelect: PropTypes.oneOfType([PropTypes.object, PropTypes.number]), /** Property that allows explicitly selecting content passed through an external operation */ forsedActiveItemId: PropTypes.any, + /** Scales the width of the bottom line to 100%. */ + size: PropTypes.string, /** Sets a callback function that is triggered when the submenu item is selected */ onSelect: PropTypes.func, }; diff --git a/packages/components/submenu/styled-submenu.js b/packages/components/submenu/styled-submenu.js index 0c71ca21ae..28833502f1 100644 --- a/packages/components/submenu/styled-submenu.js +++ b/packages/components/submenu/styled-submenu.js @@ -128,6 +128,13 @@ export const SubmenuScroller = styled.div` } overflow-x: auto; overflow-y: hidden; + + ${(props) => + props.size !== "scale" && + css` + display: grid; + flex: 0 1 auto; + `} `; export const SubmenuRoot = styled.div` diff --git a/products/ASC.Files/Core/ApiModels/RequestDto/UserInvintationRequestDto.cs b/products/ASC.Files/Core/ApiModels/RequestDto/UserInvitationRequestDto.cs similarity index 95% rename from products/ASC.Files/Core/ApiModels/RequestDto/UserInvintationRequestDto.cs rename to products/ASC.Files/Core/ApiModels/RequestDto/UserInvitationRequestDto.cs index c51d87103c..70e6b8548c 100644 --- a/products/ASC.Files/Core/ApiModels/RequestDto/UserInvintationRequestDto.cs +++ b/products/ASC.Files/Core/ApiModels/RequestDto/UserInvitationRequestDto.cs @@ -28,9 +28,10 @@ namespace ASC.Files.Core.ApiModels.RequestDto; /// /// -public class UserInvintationRequestDto +public class UserInvitationRequestDto { /// List of user IDs /// System.Collections.Generic.IEnumerable{System.Guid}, System.Collections.Generic public IEnumerable UsersIds { get; set; } + public bool ResendAll { get; set; } } \ No newline at end of file diff --git a/products/ASC.Files/Core/ApiModels/ResponseDto/FileShareDto.cs b/products/ASC.Files/Core/ApiModels/ResponseDto/FileShareDto.cs index f121b919fb..3c38b93c07 100644 --- a/products/ASC.Files/Core/ApiModels/ResponseDto/FileShareDto.cs +++ b/products/ASC.Files/Core/ApiModels/ResponseDto/FileShareDto.cs @@ -74,7 +74,6 @@ public class FileShareLink public string Password { get; set; } public bool? Disabled { get; set; } public bool? DenyDownload { get; set; } - public bool IsTemplate { get; set; } public bool? IsExpired { get; set; } } @@ -126,7 +125,6 @@ public class FileShareDtoHelper Password = aceWrapper.FileShareOptions?.Password, Disabled = aceWrapper.FileShareOptions?.Disabled is true ? true : expired, DenyDownload = aceWrapper.FileShareOptions?.DenyDownload, - IsTemplate = aceWrapper.IsTemplate, LinkType = aceWrapper.SubjectType switch { SubjectType.InvitationLink => LinkType.Invitation, diff --git a/products/ASC.Files/Core/Core/Dao/TeamlabDao/SecurityDao.cs b/products/ASC.Files/Core/Core/Dao/TeamlabDao/SecurityDao.cs index 5d88b564c8..6d6e1bf5b0 100644 --- a/products/ASC.Files/Core/Core/Dao/TeamlabDao/SecurityDao.cs +++ b/products/ASC.Files/Core/Core/Dao/TeamlabDao/SecurityDao.cs @@ -24,6 +24,8 @@ // 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 +using User = ASC.Core.Common.EF.User; + namespace ASC.Files.Core.Data; [Scope] @@ -202,6 +204,164 @@ internal abstract class SecurityBaseDao : AbstractDao return InternalGetPureShareRecordsAsync(entry); } + public async Task GetPureSharesCountAsync(FileEntry entry, ShareFilterType filterType, EmployeeActivationStatus? status) + { + if (entry == null) + { + return 0; + } + + await using var filesDbContext = _dbContextFactory.CreateDbContext(); + + var q = await GetPureSharesQuery(entry, filterType, filesDbContext); + + if (status.HasValue) + { + q = q.Join(filesDbContext.Users, s => s.Subject, u => u.Id, + (s, u) => new SecurityUserRecord { Security = s, User = u }) + .Where(s => s.User.ActivationStatus == status.Value) + .Select(r => r.Security); + } + + return await q.CountAsync(); + } + + public async IAsyncEnumerable GetPureSharesAsync(FileEntry entry, ShareFilterType filterType, EmployeeActivationStatus? status, int offset = 0, int count = -1) + { + if (entry == null || count == 0) + { + yield break; + } + + await using var filesDbContext = _dbContextFactory.CreateDbContext(); + + var q = await GetPureSharesQuery(entry, filterType, filesDbContext); + + if (filterType == ShareFilterType.User) + { + var predicate = ShareCompareHelper.GetCompareExpression(s => s.Security.Share); + + var q1 = q.Join(filesDbContext.Users, s => s.Subject, u => u.Id, + (s, u) => new SecurityUserRecord { Security = s, User = u }); + + if (status.HasValue) + { + q = q1.Where(s => s.User.ActivationStatus == status.Value) + .OrderBy(predicate) + .Select(s => s.Security); + } + else + { + q = q1.OrderBy(s => s.User.ActivationStatus) + .ThenBy(predicate) + .Select(s => s.Security); + } + } + else + { + var predicate = ShareCompareHelper.GetCompareExpression(s => s.Share); + q = q.OrderBy(predicate); + } + + if (offset > 0) + { + q = q.Skip(offset); + } + + if (count > 0) + { + q = q.Take(count); + } + + await foreach (var r in q.ToAsyncEnumerable()) + { + yield return await ToFileShareRecordAsync(r); + } + } + + public async IAsyncEnumerable GetUsersWithSharedAsync(FileEntry entry, string text, EmployeeStatus? employeeStatus, EmployeeActivationStatus? activationStatus, + bool excludeShared, int offset, int count) + { + if (entry == null || count == 0) + { + yield break; + } + + await using var filesDbContext = _dbContextFactory.CreateDbContext(); + var tenantId = TenantID; + var mappedId = (await MappingIDAsync(entry.Id)).ToString(); + + var q1 = GetUsersWithSharedQuery(tenantId, mappedId, entry, text, employeeStatus, activationStatus, excludeShared, filesDbContext); + + if (offset > 0) + { + q1 = q1.Skip(offset); + } + + if (count > 0) + { + q1 = q1.Take(count); + } + + await foreach (var r in q1.ToAsyncEnumerable()) + { + yield return new UserInfoWithShared { UserInfo = _mapper.Map(r.User), Shared = r.Shared }; + } + } + + public async Task GetUsersWithSharedCountAsync(FileEntry entry, string text, EmployeeStatus? employeeStatus, EmployeeActivationStatus? activationStatus, + bool excludeShared) + { + if (entry == null) + { + return 0; + } + + await using var filesDbContext = _dbContextFactory.CreateDbContext(); + var tenantId = TenantID; + var mappedId = (await MappingIDAsync(entry.Id)).ToString(); + + var q1 = GetUsersWithSharedQuery(tenantId, mappedId, entry, text, employeeStatus, activationStatus, excludeShared, filesDbContext); + + return await q1.CountAsync(); + } + + private static IQueryable GetUsersWithSharedQuery(int tenantId, string entryId, FileEntry entry, string text, EmployeeStatus? employeeStatus, + EmployeeActivationStatus? activationStatus, bool excludeShared, FilesDbContext filesDbContext) + { + var q = filesDbContext.Users.AsNoTracking().Where(u => u.TenantId == tenantId); + + if (employeeStatus.HasValue) + { + q = q.Where(u => u.Status == employeeStatus.Value); + } + + if (activationStatus.HasValue) + { + q = q.Where(u => u.ActivationStatus == activationStatus.Value); + } + + if (!string.IsNullOrEmpty(text)) + { + q = q.Where(u => u.FirstName.Contains(text) || u.LastName.Contains(text) || u.Email.Contains(text)); + } + + var q1 = excludeShared + ? q.Where(u => !filesDbContext.Security.Any(s => s.TenantId == tenantId && s.EntryType == entry.FileEntryType && s.EntryId == entryId && s.Subject == u.Id) && + u.Id != entry.CreateBy) + .OrderBy(u => u.ActivationStatus) + .ThenBy(u => u.FirstName) + .Select(u => new UserWithShared { User = u, Shared = false }) + : from user in q + join security in filesDbContext.Security.Where(s => s.TenantId == tenantId && s.EntryId == entryId && s.EntryType == entry.FileEntryType) on user.Id equals + security.Subject into grouping + from s in grouping.DefaultIfEmpty() + orderby user.ActivationStatus, user.FirstName + select new UserWithShared { User = user, Shared = s != null || user.Id == entry.CreateBy }; + + return q1; + } + internal async IAsyncEnumerable InternalGetPureShareRecordsAsync(FileEntry entry) { var files = new List(); @@ -234,6 +394,23 @@ internal abstract class SecurityBaseDao : AbstractDao await filesDbContext.SaveChangesAsync(); } + + public async IAsyncEnumerable GetPureSharesAsync(FileEntry entry, IEnumerable subjects) + { + if (subjects == null || !subjects.Any()) + { + yield break; + } + + var entryId = await MappingIDAsync(entry.Id); + + await using var filesDbContext = _dbContextFactory.CreateDbContext(); + + await foreach (var security in Queries.EntrySharesBySubjectsAsync(filesDbContext, TenantID, entryId.ToString(), entry.FileEntryType, subjects)) + { + yield return await ToFileShareRecordAsync(security); + } + } internal async Task SelectFilesAndFoldersForShareAsync(FileEntry entry, ICollection files, ICollection folders, ICollection foldersInt) { @@ -294,6 +471,32 @@ internal abstract class SecurityBaseDao : AbstractDao return result; } + + private async Task> GetPureSharesQuery(FileEntry entry, ShareFilterType filterType, FilesDbContext filesDbContext) + { + var entryId = await MappingIDAsync(entry.Id); + + var q = filesDbContext.Security.AsNoTracking() + .Where(s => s.TenantId == TenantID && s.EntryId == entryId.ToString() && s.EntryType == entry.FileEntryType); + + switch (filterType) + { + case ShareFilterType.User: + q = q.Where(s => s.SubjectType == SubjectType.User); + break; + case ShareFilterType.InvitationLink: + q = q.Where(s => s.SubjectType == SubjectType.InvitationLink); + break; + case ShareFilterType.ExternalLink: + q = q.Where(s => s.SubjectType == SubjectType.ExternalLink); + break; + case ShareFilterType.Link: + q = q.Where(s => s.SubjectType == SubjectType.InvitationLink || s.SubjectType == SubjectType.ExternalLink); + break; + } + + return q; + } } [Scope] @@ -547,6 +750,24 @@ internal class SecurityTreeRecord public int Level { get; set; } } +public class SecurityUserRecord +{ + public DbFilesSecurity Security { get; init; } + public User User { get; init; } +} + +public class UserInfoWithShared +{ + public UserInfo UserInfo { get; set; } + public bool Shared { get; set; } +} + +public class UserWithShared +{ + public User User { get; set; } + public bool Shared { get; set; } +} + static file class Queries { public static readonly Func> @@ -616,4 +837,10 @@ static file class Queries .Where(r => r.TenantId == tenantId && (r.Subject == subject || r.Owner == subject)) .ExecuteDelete()); + + public static readonly Func, IAsyncEnumerable> EntrySharesBySubjectsAsync = + Microsoft.EntityFrameworkCore.EF.CompileAsyncQuery( + (FilesDbContext ctx, int tenantId, string entryId, FileEntryType entryType, IEnumerable subjects) => + ctx.Security + .Where(r => r.TenantId == tenantId && r.EntryId == entryId && r.EntryType == entryType && subjects.Contains(r.Subject))); } \ No newline at end of file diff --git a/products/ASC.Files/Core/Core/EF/FilesDbContext.cs b/products/ASC.Files/Core/Core/EF/FilesDbContext.cs index 513dc710f9..3de523f589 100644 --- a/products/ASC.Files/Core/Core/EF/FilesDbContext.cs +++ b/products/ASC.Files/Core/Core/EF/FilesDbContext.cs @@ -24,6 +24,8 @@ // 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 +using User = ASC.Core.Common.EF.User; + namespace ASC.Files.Core.EF; public class FilesDbContext : DbContext @@ -42,6 +44,7 @@ public class FilesDbContext : DbContext public DbSet FilesProperties { get; set; } public DbSet Tenants { get; set; } public DbSet FilesConverts { get; set; } + public DbSet Users { get; set; } public FilesDbContext(DbContextOptions dbContextOptions) : base(dbContextOptions) { } @@ -63,6 +66,7 @@ public class FilesDbContext : DbContext .AddDbFilesProperties() .AddDbTenant() .AddFilesConverts() + .AddUser() .AddDbFunctions(); } } \ No newline at end of file diff --git a/products/ASC.Files/Core/Core/FileStorageService.cs b/products/ASC.Files/Core/Core/FileStorageService.cs index ce832aef4a..c5a95bbc13 100644 --- a/products/ASC.Files/Core/Core/FileStorageService.cs +++ b/products/ASC.Files/Core/Core/FileStorageService.cs @@ -533,7 +533,7 @@ public class FileStorageService //: IFileStorageService var room = await InternalCreateNewFolderAsync(parentId, title, FolderType.PublicRoom, @private); _ = await SetAceLinkAsync(room, SubjectType.ExternalLink, Guid.NewGuid(), FilesCommonResource.DefaultExternalLinkTitle, FileShare.Read, _actions[SubjectType.ExternalLink]); - + return room; } @@ -2513,17 +2513,47 @@ public class FileStorageService //: IFileStorageService public async Task> GetSharedInfoAsync( IEnumerable fileIds, IEnumerable folderIds, - IEnumerable subjectTypes = null, - bool withoutTemplates = false) + IEnumerable subjectTypes = null) { - return await _fileSharing.GetSharedInfoAsync(fileIds, folderIds, subjectTypes, withoutTemplates); + return await _fileSharing.GetSharedInfoAsync(fileIds, folderIds, subjectTypes); + } + + public async Task> GetSharedInfoShortFileAsync(T fileId) + { + return await _fileSharing.GetSharedInfoShortFileAsync(fileId); + } + + public async IAsyncEnumerable GetRoomSharedInfoAsync(T roomId, ShareFilterType filterType, int offset, int count) + { + var room = await GetFolderDao().GetFolderAsync(roomId).NotFoundIfNull(); + + await foreach (var ace in _fileSharing.GetRoomSharesAsync(room, filterType, null, offset, count)) + { + yield return ace; + } + } + + public async Task GetRoomSharesCountAsync(T roomId, ShareFilterType filterType) + { + var room = await GetFolderDao().GetFolderAsync(roomId).NotFoundIfNull(); + + return await _fileSharing.GetRoomSharesCountAsync(room, filterType); + } + + public async IAsyncEnumerable GetSharedInfoAsync(T roomId, IEnumerable subjects) + { + var room = await GetFolderDao().GetFolderAsync(roomId).NotFoundIfNull(); + + await foreach (var ace in _fileSharing.GetPureSharesAsync(room, subjects)) + { + yield return ace; + } } public async Task SetAceObjectAsync(AceCollection aceCollection, bool notify) { var fileDao = GetFileDao(); var folderDao = GetFolderDao(); - var securityDao = GetSecurityDao(); var entries = new List>(); string warning = null; @@ -2542,77 +2572,49 @@ public class FileStorageService //: IFileStorageService { try { + var result = await _fileSharingAceHelper.SetAceObjectAsync(aceCollection.Aces, entry, notify, aceCollection.Message, aceCollection.AdvancedSettings); + warning ??= result.Warning; - var eventTypes = new List<(UserInfo User, EventType EventType, FileShare Access, string Email)>(); - foreach (var ace in aceCollection.Aces) + if (!result.Changed) { - var user = _userManager.GetUsers(ace.Id); + continue; + } - if (user == Constants.LostUser) + foreach (var (eventType, ace) in result.HandledAces) + { + if (ace.IsLink) { - eventTypes.Add((null, EventType.Create, ace.Access, ace.Email)); continue; } - var userId = user.Id; + var user = !string.IsNullOrEmpty(ace.Email) + ? await _userManager.GetUserByEmailAsync(ace.Email) + : await _userManager.GetUsersAsync(ace.Id); + + var name = user.DisplayUserName(false, _displayUserSettingsHelper); - var userSubjects = await _fileSecurity.GetUserSubjectsAsync(user.Id); - var usersRecords = await securityDao.GetSharesAsync(userSubjects).ToListAsync(); - var recordEntrys = usersRecords.Select(r => r.EntryId.ToString()); - - EventType eventType; - - if (usersRecords.Any() && ace.Access != FileShare.None && recordEntrys.Contains(entry.Id.ToString())) + if (entry is Folder folder && DocSpaceHelper.IsRoom(folder.FolderType)) { - eventType = EventType.Update; - } - else if (!usersRecords.Any() || !recordEntrys.Contains(entry.Id.ToString())) - { - eventType = EventType.Create; + switch (eventType) + { + case EventType.Create: + _ = _filesMessageService.SendAsync(MessageAction.RoomCreateUser, entry, user.Id, name, GetAccessString(ace.Access)); + break; + case EventType.Remove: + _ = _filesMessageService.SendAsync(MessageAction.RoomRemoveUser, entry, user.Id, name, GetAccessString(ace.Access)); + break; + case EventType.Update: + _ = _filesMessageService.SendAsync(MessageAction.RoomUpdateAccessForUser, entry, user.Id, ace.Access, name); + break; + } } else { - eventType = EventType.Remove; - } - eventTypes.Add((user, eventType, ace.Access, null)); - } - - var (changed, warningMessage) = await _fileSharingAceHelper.SetAceObjectAsync(aceCollection.Aces, entry, notify, aceCollection.Message, aceCollection.AdvancedSettings); - warning ??= warningMessage; - - if (changed) - { - foreach (var e in eventTypes) - { - var user = e.User ?? await _userManager.GetUserByEmailAsync(e.Email); - var name = user.DisplayUserName(false, _displayUserSettingsHelper); - - var access = e.Access; - - if (entry.FileEntryType == FileEntryType.Folder && DocSpaceHelper.IsRoom(((Folder)entry).FolderType)) - { - switch (e.EventType) - { - case EventType.Create: - _ = _filesMessageService.SendAsync(MessageAction.RoomCreateUser, entry, user.Id, name, GetAccessString(access)); - break; - case EventType.Remove: - _ = _filesMessageService.SendAsync(MessageAction.RoomRemoveUser, entry, user.Id, name, GetAccessString(access)); - break; - case EventType.Update: - _ = _filesMessageService.SendAsync(MessageAction.RoomUpdateAccessForUser, entry, user.Id, access, name); - break; - } - } - else - { - - _ = _filesMessageService.SendAsync( - entry.FileEntryType == FileEntryType.Folder ? MessageAction.FolderUpdatedAccessFor : MessageAction.FileUpdatedAccessFor, - entry, - entry.Title, name, GetAccessString(access)); - } + _ = _filesMessageService.SendAsync( + entry.FileEntryType == FileEntryType.Folder ? MessageAction.FolderUpdatedAccessFor : MessageAction.FileUpdatedAccessFor, + entry, + entry.Title, name, GetAccessString(ace.Access)); } } } @@ -2666,7 +2668,7 @@ public class FileStorageService //: IFileStorageService } } - public async Task> SetInvitationLinkAsync(T roomId, Guid linkId, string title, FileShare share) + public async Task SetInvitationLinkAsync(T roomId, Guid linkId, string title, FileShare share) { var expirationDate = DateTime.UtcNow.Add(_invitationLinkHelper.IndividualLinkExpirationInterval); @@ -2675,13 +2677,13 @@ public class FileStorageService //: IFileStorageService return await SetAceLinkAsync(room, SubjectType.InvitationLink, linkId, title, share, _actions[SubjectType.InvitationLink], expirationDate); } - public async Task> SetExternalLinkAsync(T entryId, FileEntryType entryType, Guid linkId, string title, FileShare share, DateTime expirationDate = default, + public async Task SetExternalLinkAsync(T entryId, FileEntryType entryType, Guid linkId, string title, FileShare share, DateTime expirationDate = default, string password = null, bool disabled = false, bool denyDownload = false) { FileEntry entry = entryType == FileEntryType.File ? (await GetFileDao().GetFileAsync(entryId)).NotFoundIfNull() : (await GetFolderDao().GetFolderAsync(entryId)).NotFoundIfNull(); - return await SetAceLinkAsync(entry, SubjectType.ExternalLink, linkId, title, share, _actions[SubjectType.ExternalLink], expirationDate, password, disabled, denyDownload, 10); + return await SetAceLinkAsync(entry, SubjectType.ExternalLink, linkId, title, share, _actions[SubjectType.ExternalLink], expirationDate, password, disabled, denyDownload); } public async Task SetAceLinkAsync(T fileId, FileShare share) @@ -2701,8 +2703,8 @@ public class FileStorageService //: IFileStorageService try { - var (changed, _) = await _fileSharingAceHelper.SetAceObjectAsync(aces, file, false, null, null); - if (changed) + var result = await _fileSharingAceHelper.SetAceObjectAsync(aces, file, false, null, null); + if (result.Changed) { _ = _filesMessageService.SendAsync(MessageAction.FileExternalLinkAccessUpdated, file, file.Title, GetAccessString(share)); } @@ -3119,10 +3121,13 @@ public class FileStorageService //: IFileStorageService public async Task KeepNewFileNameAsync(bool set) { - _filesSettingsHelper.KeepNewFileName = set; - await _messageService.SendHeadersMessageAsync(MessageAction.DocumentsKeepNewFileNameSettingsUpdated); - - return _filesSettingsHelper.KeepNewFileName; + var current = _filesSettingsHelper.KeepNewFileName; + if (current != set) + { + _filesSettingsHelper.KeepNewFileName = set; + await _messageService.SendHeadersMessageAsync(MessageAction.DocumentsKeepNewFileNameSettingsUpdated); + } + return set; } public bool HideConfirmConvert(bool isForSave) @@ -3262,36 +3267,62 @@ public class FileStorageService //: IFileStorageService return fileIds; } - public async Task ResendEmailInvitationsAsync(T id, IEnumerable usersIds) + public async Task ResendEmailInvitationsAsync(T id, IEnumerable usersIds, bool resendAll) { - ArgumentNullException.ThrowIfNull(usersIds); + if (!resendAll && (usersIds == null || !usersIds.Any())) + { + return; + } var folderDao = _daoFactory.GetFolderDao(); - var room = await folderDao.GetFolderAsync(id); + var room = await folderDao.GetFolderAsync(id).NotFoundIfNull(); - ErrorIf(room == null, FilesCommonResource.ErrorMassage_FolderNotFound); ErrorIf(!await _fileSecurity.CanEditRoomAsync(room), FilesCommonResource.ErrorMassage_SecurityException); - var shares = (await _fileSharing.GetSharedInfoAsync(room)).ToDictionary(k => k.Id, v => v); - - foreach (var userId in usersIds) + if (!resendAll) { - if (!shares.TryGetValue(userId, out var share)) + await foreach (var ace in _fileSharing.GetPureSharesAsync(room, usersIds)) { - continue; + var user = await _userManager.GetUsersAsync(ace.Id); + + var link = await _invitationLinkService.GetInvitationLinkAsync(user.Email, ace.Access, _authContext.CurrentAccount.ID, room.Id.ToString()); + await _studioNotifyService.SendEmailRoomInviteAsync(user.Email, room.Title, link); + } + + return; + } + + const int margin = 1; + const int packSize = 1000; + var offset = 0; + var finish = false; + + while (!finish) + { + var counter = 0; + + await foreach (var ace in _fileSharing.GetRoomSharesAsync(room, ShareFilterType.User, EmployeeActivationStatus.Pending, offset, packSize + margin)) + { + counter++; + + if (counter > packSize) + { + offset += packSize; + break; + } + + var user = await _userManager.GetUsersAsync(ace.Id); + + var link = await _invitationLinkService.GetInvitationLinkAsync(user.Email, ace.Access, _authContext.CurrentAccount.ID, id.ToString()); + var shortenLink = await _urlShortener.GetShortenLinkAsync(link); + + await _studioNotifyService.SendEmailRoomInviteAsync(user.Email, room.Title, shortenLink); } - var user = await _userManager.GetUserAsync(share.Id, null); - - if (user.ActivationStatus != EmployeeActivationStatus.Pending) + if (counter <= packSize) { - continue; + finish = true; } - - var link = await _invitationLinkService.GetInvitationLinkAsync(user.Email, share.Access, _authContext.CurrentAccount.ID, id.ToString()); - var shortenLink = await _urlShortener.GetShortenLinkAsync(link); - - await _studioNotifyService.SendEmailRoomInviteAsync(user.Email, room.Title, shortenLink); } } @@ -3435,9 +3466,8 @@ public class FileStorageService //: IFileStorageService } } - private async Task> SetAceLinkAsync(FileEntry entry, SubjectType subjectType, Guid linkId, string title, FileShare share, - IReadOnlyDictionary messageActions, DateTime expirationDate = default, string password = null, bool disabled = false, bool denyDownload = false, - int maxLinksCount = int.MaxValue) + private async Task SetAceLinkAsync(FileEntry entry, SubjectType subjectType, Guid linkId, string title, FileShare share, + IReadOnlyDictionary messageActions, DateTime expirationDate = default, string password = null, bool disabled = false, bool denyDownload = false) { ArgumentNullOrEmptyException.ThrowIfNullOrEmpty(title); @@ -3446,25 +3476,6 @@ public class FileStorageService //: IFileStorageService linkId = Guid.NewGuid(); } - var action = EventType.Create; - - var links = (await _fileSecurity.GetSharesAsync(entry)) - .Where(r => r.SubjectType == subjectType).ToList(); - - if (share == FileShare.None) - { - action = EventType.Remove; - } - else if (links.Any(r => r.Subject == linkId)) - { - action = EventType.Update; - } - - if (action == EventType.Create && links.Count == maxLinksCount) - { - throw GenerateException(new InvalidOperationException(FilesCommonResource.ErrorMessage_MaxLinksCount)); - } - var options = new FileShareOptions { Title = title, @@ -3497,11 +3508,16 @@ public class FileStorageService //: IFileStorageService try { - var (changed, _) = await _fileSharingAceHelper.SetAceObjectAsync(aces, entry, false, null, null); + var result = await _fileSharingAceHelper.SetAceObjectAsync(aces, entry, false, null, null); - if (changed) + if (!string.IsNullOrEmpty(result.Warning)) { - _ = _filesMessageService.SendAsync(messageActions[action], entry, entry.Title, GetAccessString(share)); + throw GenerateException(new InvalidOperationException(result.Warning)); + } + + if (result.Changed) + { + _ = _filesMessageService.SendAsync(messageActions[result.HandledAces[0].Event], entry, entry.Title, GetAccessString(share)); } } catch (Exception e) @@ -3509,9 +3525,7 @@ public class FileStorageService //: IFileStorageService throw GenerateException(e); } - return entry.FileEntryType == FileEntryType.File ? - await GetSharedInfoAsync(new[] { entry.Id }, Array.Empty()) : - await GetSharedInfoAsync(Array.Empty(), new[] { entry.Id }); + return (await _fileSharing.GetPureSharesAsync(entry, new[] { linkId }).FirstOrDefaultAsync()); } private async Task> GetFullAceWrappersAsync(IEnumerable share) @@ -3569,13 +3583,6 @@ public class FileStorageService //: IFileStorageService await SetAceObjectAsync(aceCollection, notify); } - private enum EventType - { - Update, - Create, - Remove - } - private static readonly IReadOnlyDictionary> _actions = new Dictionary> { diff --git a/products/ASC.Files/Core/Core/Security/FileSecurity.cs b/products/ASC.Files/Core/Core/Security/FileSecurity.cs index efcfee73ad..7d19ac4f40 100644 --- a/products/ASC.Files/Core/Core/Security/FileSecurity.cs +++ b/products/ASC.Files/Core/Core/Security/FileSecurity.cs @@ -926,8 +926,9 @@ public class FileSecurity : IFileSecurity FileShareRecord ace; - if (!isRoom && e.RootFolderType is FolderType.VirtualRooms or FolderType.Archive && - _cachedRecords.TryGetValue(GetCacheKey(e.ParentId, userId), out var value)) + if (!isRoom && e.RootFolderType is FolderType.VirtualRooms or FolderType.Archive && + (_cachedRecords.TryGetValue(GetCacheKey(e.ParentId, userId), out var value)) || + _cachedRecords.TryGetValue(GetCacheKey(e.ParentId, await _externalShare.GetLinkIdAsync()), out value)) { ace = value.Clone(); ace.EntryId = e.Id; @@ -965,6 +966,14 @@ public class FileSecurity : IFileSecurity .ThenByDescending(r => r.Share, new FileShareRecord.ShareComparer()) .FirstOrDefault(); } + + if (!isRoom && e.RootFolderType is FolderType.VirtualRooms or FolderType.Archive && + ace is { SubjectType: SubjectType.User or SubjectType.ExternalLink }) + { + var id = ace.SubjectType == SubjectType.ExternalLink ? ace.Subject : userId; + + _cachedRecords.TryAdd(GetCacheKey(e.ParentId, id), ace); + } } if (ace is { SubjectType: SubjectType.ExternalLink } && ace.Subject != userId && @@ -984,11 +993,6 @@ public class FileSecurity : IFileSecurity e.Access = ace?.Share ?? defaultShare; e.Access = e.RootFolderType is FolderType.ThirdpartyBackup ? FileShare.Restrict : e.Access; - if (ace is { IsLink: false } && !isRoom && e.RootFolderType is FolderType.VirtualRooms or FolderType.Archive && e.Access != FileShare.None) - { - _cachedRecords.TryAdd(GetCacheKey(e.ParentId, userId), ace); - } - switch (action) { case FilesSecurityActions.Read: @@ -996,7 +1000,8 @@ public class FileSecurity : IFileSecurity case FilesSecurityActions.Mute: return e.Access != FileShare.Restrict; case FilesSecurityActions.Comment: - if (e.Access is FileShare.Comment or FileShare.Review || + if (e.Access is FileShare.Comment || + e.Access == FileShare.Review || e.Access == FileShare.CustomFilter || e.Access == FileShare.ReadWrite || e.Access == FileShare.RoomAdmin || @@ -1149,7 +1154,7 @@ public class FileSecurity : IFileSecurity if (e.Access != FileShare.Restrict && ace?.Options is not { DenyDownload: true }) { return true; - } + } break; } @@ -1193,6 +1198,33 @@ public class FileSecurity : IFileSecurity return await _daoFactory.GetSecurityDao().GetSharesAsync(entry, subjects); } + public IAsyncEnumerable GetPureSharesAsync(FileEntry entry, IEnumerable subjects) + { + return _daoFactory.GetSecurityDao().GetPureSharesAsync(entry, subjects); + } + + public IAsyncEnumerable GetPureSharesAsync(FileEntry entry, ShareFilterType filterType, EmployeeActivationStatus? status, int offset = 0, int count = -1) + { + return _daoFactory.GetSecurityDao().GetPureSharesAsync(entry, filterType, status, offset, count); + } + + public Task GetPureSharesCountAsync(FileEntry entry, ShareFilterType filterType, EmployeeActivationStatus? status) + { + return _daoFactory.GetSecurityDao().GetPureSharesCountAsync(entry, filterType, status); + } + + public IAsyncEnumerable GetUsersWithSharedAsync(FileEntry entry, string text, EmployeeStatus? employeeStatus, EmployeeActivationStatus? activationStatus, + bool excludeShared, int offset, int count) + { + return _daoFactory.GetSecurityDao().GetUsersWithSharedAsync(entry, text, employeeStatus, activationStatus, excludeShared, offset, count); + } + + public async Task GetUsersWithSharedCountAsync(FileEntry entry, string text, EmployeeStatus? employeeStatus, EmployeeActivationStatus? activationStatus, + bool excludeShared) + { + return await _daoFactory.GetSecurityDao().GetUsersWithSharedCountAsync(entry, text, employeeStatus, activationStatus, excludeShared); + } + public async IAsyncEnumerable GetSharesForMeAsync(FilterType filterType, bool subjectGroup, Guid subjectID, string searchText = "", bool searchInContent = false, bool withSubfolders = false) { var securityDao = _daoFactory.GetSecurityDao(); diff --git a/products/ASC.Files/Core/Core/Security/FileShareRecord.cs b/products/ASC.Files/Core/Core/Security/FileShareRecord.cs index 2eda6b582c..acbea42e6d 100644 --- a/products/ASC.Files/Core/Core/Security/FileShareRecord.cs +++ b/products/ASC.Files/Core/Core/Security/FileShareRecord.cs @@ -41,17 +41,23 @@ public class FileShareRecord public class ShareComparer : IComparer { - private static readonly int[] _shareOrder = new[] + private static readonly int[] _shareOrder = { - (int)FileShare.None, - (int)FileShare.ReadWrite, - (int)FileShare.CustomFilter, - (int)FileShare.Review, - (int)FileShare.FillForms, - (int)FileShare.Comment, - (int)FileShare.Read, - (int)FileShare.Restrict, - (int)FileShare.Varies + (int)FileShare.None, + (int)FileShare.RoomAdmin, + (int)FileShare.Collaborator, + (int)FileShare.Editing, + (int)FileShare.FillForms, + (int)FileShare.Review, + (int)FileShare.Comment, + (int)FileShare.Read, + + // Not used + + (int)FileShare.ReadWrite, + (int)FileShare.CustomFilter, + (int)FileShare.Varies, + (int)FileShare.Restrict }; public int Compare(FileShare x, FileShare y) @@ -70,3 +76,39 @@ public class SmallShareRecord public FileShare Share { get; set; } public SubjectType SubjectType { get; set; } } + + +public static class ShareCompareHelper +{ + private static readonly ConcurrentDictionary _predicates = new(); + + public static Expression> GetCompareExpression(Expression> memberExpression) + { + var type = typeof(TType); + var key = type.ToString(); + + if (_predicates.TryGetValue(key, out var value)) + { + return (Expression>)value; + } + + var shares = Enum.GetValues() + .Order(new FileShareRecord.ShareComparer()) + .ToList(); + + ConditionalExpression expression = null; + + for (var i = shares.Count - 1; i >= 0; i--) + { + expression = Expression.Condition( + Expression.Equal(memberExpression.Body, Expression.Constant(shares[i])), Expression.Constant(i), + expression != null ? expression : Expression.Constant(i + 1)); + } + + var predicate = Expression.Lambda>(expression!, memberExpression.Parameters[0]); + + _predicates.TryAdd(key, predicate); + + return predicate; + } +} \ No newline at end of file diff --git a/products/ASC.Files/Core/Core/Security/ISecurityDao.cs b/products/ASC.Files/Core/Core/Security/ISecurityDao.cs index b733854431..1165cae914 100644 --- a/products/ASC.Files/Core/Core/Security/ISecurityDao.cs +++ b/products/ASC.Files/Core/Core/Security/ISecurityDao.cs @@ -38,4 +38,9 @@ public interface ISecurityDao IAsyncEnumerable GetPureShareRecordsAsync(FileEntry entry); Task DeleteShareRecordsAsync(IEnumerable records); Task IsSharedAsync(T entryId, FileEntryType type); + IAsyncEnumerable GetPureSharesAsync(FileEntry entry, ShareFilterType filterType, EmployeeActivationStatus? status, int offset = 0, int count = -1); + Task GetPureSharesCountAsync(FileEntry entry, ShareFilterType filterType, EmployeeActivationStatus? status); + IAsyncEnumerable GetPureSharesAsync(FileEntry entry, IEnumerable subjects); + IAsyncEnumerable GetUsersWithSharedAsync(FileEntry entry, string text, EmployeeStatus? employeeStatus, EmployeeActivationStatus? activationStatus, bool excludeShared, int offset = 0, int count = -1); + Task GetUsersWithSharedCountAsync(FileEntry entry, string text, EmployeeStatus? employeeStatus, EmployeeActivationStatus? activationStatus, bool excludeShared); } diff --git a/products/ASC.Files/Core/Core/Security/SubjectType.cs b/products/ASC.Files/Core/Core/Security/SubjectType.cs index 9d03a46f72..043da2cc7c 100644 --- a/products/ASC.Files/Core/Core/Security/SubjectType.cs +++ b/products/ASC.Files/Core/Core/Security/SubjectType.cs @@ -26,6 +26,15 @@ namespace ASC.Files.Core.Security; +[Flags] +public enum ShareFilterType +{ + User = 0, + InvitationLink = 1, + ExternalLink = 2, + Link = InvitationLink | ExternalLink +} + public enum SubjectType { User = 0, diff --git a/products/ASC.Files/Core/Core/UsersInRoomFeature.cs b/products/ASC.Files/Core/Core/UsersInRoomFeature.cs index 625dc1dddd..4bd1f91da2 100644 --- a/products/ASC.Files/Core/Core/UsersInRoomFeature.cs +++ b/products/ASC.Files/Core/Core/UsersInRoomFeature.cs @@ -65,6 +65,6 @@ public class UsersInRoomStatistic : ITenantQuotaFeatureStat !r.IsLink).CountAsync(); + return await securityDao.GetPureSharesCountAsync(folder, ShareFilterType.User, null); } } \ No newline at end of file diff --git a/products/ASC.Files/Core/Services/WCFService/FileOperations/FileDownloadOperation.cs b/products/ASC.Files/Core/Services/WCFService/FileOperations/FileDownloadOperation.cs index 981bfcbec9..ecb5e081a2 100644 --- a/products/ASC.Files/Core/Services/WCFService/FileOperations/FileDownloadOperation.cs +++ b/products/ASC.Files/Core/Services/WCFService/FileOperations/FileDownloadOperation.cs @@ -217,7 +217,7 @@ class FileDownloadOperation : FileOperation, T> PublishChanges(); - var filesMessageService = _serviceProvider.GetRequiredService(); + var filesMessageService = scope.ServiceProvider.GetRequiredService(); foreach (var file in filesForSend) { var key = file.Id; diff --git a/products/ASC.Files/Core/Services/WCFService/Wrappers/AceWrapper.cs b/products/ASC.Files/Core/Services/WCFService/Wrappers/AceWrapper.cs index ce57a94fa7..774906cab3 100644 --- a/products/ASC.Files/Core/Services/WCFService/Wrappers/AceWrapper.cs +++ b/products/ASC.Files/Core/Services/WCFService/Wrappers/AceWrapper.cs @@ -42,7 +42,6 @@ public class AceWrapper : IMapFrom public SubjectType SubjectType { get; set; } public FileShareOptions FileShareOptions { get; set; } public bool CanEditAccess { get; set; } - public bool IsTemplate { get; set; } [JsonPropertyName("title")] public string SubjectName { get; set; } diff --git a/products/ASC.Files/Core/Utils/FileSharing.cs b/products/ASC.Files/Core/Utils/FileSharing.cs index ac82ebf6ca..c47b1770cb 100644 --- a/products/ASC.Files/Core/Utils/FileSharing.cs +++ b/products/ASC.Files/Core/Utils/FileSharing.cs @@ -47,6 +47,9 @@ public class FileSharingAceHelper private readonly UserManagerWrapper _userManagerWrapper; private readonly CountPaidUserChecker _countPaidUserChecker; private readonly IUrlShortener _urlShortener; + + private const int MaxInvitationLinks = 1; + private const int MaxExternalLinks = 10; public FileSharingAceHelper( FileSecurity fileSecurity, @@ -86,23 +89,20 @@ public class FileSharingAceHelper _urlShortener = urlShortener; } - public async Task<(bool, string)> SetAceObjectAsync(List aceWrappers, FileEntry entry, bool notify, string message, AceAdvancedSettingsWrapper advancedSettings) + public async Task SetAceObjectAsync(List aceWrappers, FileEntry entry, bool notify, string message, AceAdvancedSettingsWrapper advancedSettings) { if (entry == null) { throw new ArgumentNullException(FilesCommonResource.ErrorMassage_BadRequest); } - if (!aceWrappers.All(r => r.Id == _authContext.CurrentAccount.ID && r.Access == FileShare.None) && !await _fileSharingHelper.CanSetAccessAsync(entry) && advancedSettings is not { InvitationLink: true }) - { - throw new SecurityException(FilesCommonResource.ErrorMassage_SecurityException); - } - - if (entry is Folder { Private: true } && advancedSettings is not { AllowSharingPrivateRoom: true }) + if (!aceWrappers.All(r => r.Id == _authContext.CurrentAccount.ID && r.Access == FileShare.None) && + !await _fileSharingHelper.CanSetAccessAsync(entry) && advancedSettings is not { InvitationLink: true }) { throw new SecurityException(FilesCommonResource.ErrorMassage_SecurityException); } + var handledAces = new List<(EventType, AceWrapper)>(aceWrappers.Count); var ownerId = entry.RootFolderType == FolderType.USER ? entry.RootCreateBy : entry.CreateBy; var room = entry is Folder folder && DocSpaceHelper.IsRoom(folder.FolderType) ? folder : null; var entryType = entry.FileEntryType; @@ -110,16 +110,23 @@ public class FileSharingAceHelper var usersWithoutRight = new List(); var changed = false; string warning = null; - var shares = (await _fileSecurity.GetSharesAsync(entry)).ToList(); + var shares = await _fileSecurity.GetPureSharesAsync(entry, aceWrappers.Select(a => a.Id)) + .ToDictionaryAsync(r => r.Subject); foreach (var w in aceWrappers.OrderByDescending(ace => ace.SubjectGroup)) { + if (w.Id == _authContext.CurrentAccount.ID) + { + continue; + } + var emailInvite = !string.IsNullOrEmpty(w.Email); var currentUserType = await _userManager.GetUserTypeAsync(w.Id); var userType = EmployeeType.User; - var existedShare = shares.FirstOrDefault(r => r.Subject == w.Id); + var existedShare = shares.Get(w.Id); var rightIsAvailable = FileSecurity.AvailableUserRights.TryGetValue(currentUserType, out var userAccesses) && userAccesses.Contains(w.Access); + var eventType = existedShare != null ? w.Access == FileShare.None ? EventType.Remove : EventType.Update : EventType.Create; if (room != null) { @@ -127,15 +134,44 @@ public class FileSharingAceHelper { continue; } - - if (room.FolderType is not (FolderType.PublicRoom or FolderType.CustomRoom) && w.SubjectType == SubjectType.ExternalLink) + + if (room.FolderType == FolderType.PublicRoom && w.Access == FileShare.Read && w.SubjectType != SubjectType.ExternalLink) { continue; } - if (room.FolderType == FolderType.PublicRoom && w.Access == FileShare.Read && w.SubjectType != SubjectType.ExternalLink) + switch (w.SubjectType) { - continue; + case SubjectType.ExternalLink when room.FolderType is not (FolderType.PublicRoom or FolderType.CustomRoom): + case SubjectType.ExternalLink when w.Access is not (FileShare.Read or FileShare.None): + continue; + case SubjectType.ExternalLink: + { + if (eventType == EventType.Create) + { + var linksCount = await _fileSecurity.GetPureSharesCountAsync(entry, ShareFilterType.ExternalLink, null); + + if (linksCount >= MaxExternalLinks) + { + warning ??= string.Format(FilesCommonResource.ErrorMessage_MaxLinksCount, MaxExternalLinks); + continue; + } + } + + break; + } + case SubjectType.InvitationLink when eventType == EventType.Create: + { + var linksCount = await _fileSecurity.GetPureSharesCountAsync(entry, ShareFilterType.InvitationLink, null); + + if (linksCount >= MaxInvitationLinks) + { + warning ??= string.Format(FilesCommonResource.ErrorMessage_MaxLinksCount, MaxInvitationLinks); + continue; + } + + break; + } } } @@ -225,6 +261,7 @@ public class FileSharingAceHelper await _fileSecurity.ShareAsync(entry.Id, entryType, w.Id, share, w.SubjectType, w.FileShareOptions); changed = true; + handledAces.Add((eventType, w)); if (emailInvite) { @@ -271,9 +308,7 @@ public class FileSharingAceHelper || (share == FileShare.None && entry.RootFolderType == FolderType.COMMON); var removeNew = share == FileShare.Restrict || (share == FileShare.None - && (entry.RootFolderType == FolderType.USER || - entry.RootFolderType == FolderType.VirtualRooms || - entry.RootFolderType == FolderType.Archive)); + && entry.RootFolderType is FolderType.USER or FolderType.VirtualRooms or FolderType.Archive); listUsersId.ForEach(id => { @@ -303,8 +338,7 @@ public class FileSharingAceHelper await _fileMarker.MarkAsNewAsync(entry, recipients.Keys.ToList()); } - if ((entry.RootFolderType == FolderType.USER - || entry.RootFolderType == FolderType.Privacy) + if (entry.RootFolderType is FolderType.USER or FolderType.Privacy && notify) { await _notifyClient.SendShareNoticeAsync(entry, recipients, message); @@ -322,7 +356,7 @@ public class FileSharingAceHelper await _fileMarker.RemoveMarkAsNewAsync(entry, userId); } - return (changed, warning); + return new AceProcessingResult(changed, warning, handledAces); } public async Task RemoveAceAsync(FileEntry entry) @@ -473,7 +507,72 @@ public class FileSharing return await _fileSharingHelper.CanSetAccessAsync(entry); } - public async Task> GetSharedInfoAsync(FileEntry entry, IEnumerable subjectsTypes = null, bool withoutTemplates = false) + public async IAsyncEnumerable GetPureSharesAsync(FileEntry entry, IEnumerable subjects) + { + if (entry == null) + { + throw new ArgumentNullException(FilesCommonResource.ErrorMassage_BadRequest); + } + + if (!await _fileSecurity.CanReadAsync(entry)) + { + _logger.ErrorUserCanTGetSharedInfo(_authContext.CurrentAccount.ID, entry.FileEntryType, entry.Id.ToString()!); + + yield break; + } + + var canEditAccess = await _fileSecurity.CanEditAccessAsync(entry); + + await foreach (var record in _fileSecurity.GetPureSharesAsync(entry, subjects)) + { + yield return await ToAceAsync(entry, record, canEditAccess); + } + } + + public async IAsyncEnumerable GetRoomSharesAsync(Folder room, ShareFilterType filterType, EmployeeActivationStatus? status, int offset, int count) + { + if (room == null || !DocSpaceHelper.IsRoom(room.FolderType)) + { + throw new ArgumentNullException(FilesCommonResource.ErrorMassage_BadRequest); + } + + if (!await _fileSecurity.CanReadAsync(room)) + { + _logger.ErrorUserCanTGetSharedInfo(_authContext.CurrentAccount.ID, room.FileEntryType, room.Id.ToString()!); + + yield break; + } + + var canEditAccess = await _fileSecurity.CanEditAccessAsync(room); + + var allDefaultAces = await GetDefaultRoomAcesAsync(room, filterType, status).ToListAsync(); + var defaultAces = allDefaultAces.Skip(offset).Take(count).ToList(); + + offset = Math.Max(defaultAces.Count > 0 ? 0 : offset - allDefaultAces.Count, 0); + count -= defaultAces.Count; + + var records = _fileSecurity.GetPureSharesAsync(room, filterType, status, offset, count); + + foreach (var record in defaultAces) + { + yield return record; + } + + await foreach (var record in records) + { + yield return await ToAceAsync(room, record, canEditAccess); + } + } + + public async Task GetRoomSharesCountAsync(Folder room, ShareFilterType filterType) + { + var defaultAces = await GetDefaultRoomAcesAsync(room, filterType, null).CountAsync(); + var sharesCount = await _fileSecurity.GetPureSharesCountAsync(room, filterType, null); + + return defaultAces + sharesCount; + } + + public async Task> GetSharedInfoAsync(FileEntry entry, IEnumerable subjectsTypes = null) { if (entry == null) { @@ -586,38 +685,6 @@ public class FileSharing result.Add(w); } - if (isRoom && canEditAccess && !withoutTemplates) - { - var invitationId = Guid.NewGuid(); - - var invitationAceTemplate = new AceWrapper - { - Id = invitationId, - Link = _invitationLinkService.GetInvitationLink(invitationId, _authContext.CurrentAccount.ID), - SubjectGroup = true, - Access = ((Folder)entry).FolderType == FolderType.PublicRoom ? FileShare.RoomAdmin : FileShare.Read, - Owner = false, - IsTemplate = true, - SubjectType = SubjectType.InvitationLink - }; - - var externalId = Guid.NewGuid(); - - var externalAceTemplate = new AceWrapper - { - Id = externalId, - Link = await _externalShare.GetLinkAsync(externalId), - SubjectGroup = true, - Access = FileShare.Read, - Owner = false, - IsTemplate = true, - SubjectType = SubjectType.ExternalLink - }; - - result.Add(invitationAceTemplate); - result.Add(externalAceTemplate); - } - if (entry.FileEntryType == FileEntryType.File && result.All(w => w.Id != FileConstant.ShareLinkId) && entry.FileEntryType == FileEntryType.File && !((File)entry).Encrypted) @@ -696,8 +763,7 @@ public class FileSharing return result; } - public async Task> GetSharedInfoAsync(IEnumerable fileIds, IEnumerable folderIds, IEnumerable subjectTypes = null, - bool withoutTemplates = false) + public async Task> GetSharedInfoAsync(IEnumerable fileIds, IEnumerable folderIds, IEnumerable subjectTypes = null) { if (!_authContext.IsAuthenticated) { @@ -719,7 +785,7 @@ public class FileSharing IEnumerable acesForObject; try { - acesForObject = await GetSharedInfoAsync(entry, subjectTypes, withoutTemplates); + acesForObject = await GetSharedInfoAsync(entry, subjectTypes); } catch (Exception e) { @@ -821,4 +887,94 @@ public class FileSharing .Where(aceWrapper => !aceWrapper.Id.Equals(FileConstant.ShareLinkId) || aceWrapper.Access != FileShare.Restrict) .Select(aceWrapper => new AceShortWrapper(aceWrapper))); } + + private async IAsyncEnumerable GetDefaultRoomAcesAsync(Folder room, ShareFilterType filterType, EmployeeActivationStatus? status) + { + if (filterType != ShareFilterType.User) + { + yield break; + } + + if (status.HasValue) + { + var user = await _userManager.GetUsersAsync(room.CreateBy); + + if (user.ActivationStatus != status.Value) + { + yield break; + } + } + + var owner = new AceWrapper + { + Id = room.CreateBy, + SubjectName = await _global.GetUserNameAsync(room.CreateBy), + SubjectGroup = false, + Access = FileShare.ReadWrite, + Owner = true, + CanEditAccess = false, + }; + + yield return owner; + } + + private async Task ToAceAsync(FileEntry entry, FileShareRecord record, bool canEditAccess) + { + var w = new AceWrapper + { + Id = record.Subject, + SubjectGroup = false, + Access = record.Share, + FileShareOptions = record.Options, + SubjectType = record.SubjectType + }; + + w.CanEditAccess = _authContext.CurrentAccount.ID != w.Id && (w.SubjectType is SubjectType.User or SubjectType.Group) && canEditAccess; + + if (record.IsLink) + { + var link = record.SubjectType == SubjectType.InvitationLink ? + _invitationLinkService.GetInvitationLink(record.Subject, _authContext.CurrentAccount.ID) : + await _externalShare.GetLinkAsync(record.Subject); + + w.Link = await _urlShortener.GetShortenLinkAsync(link); + w.SubjectGroup = true; + w.CanEditAccess = false; + w.FileShareOptions.Password = await _externalShare.GetPasswordAsync(w.FileShareOptions.Password); + w.SubjectType = record.SubjectType; + } + else + { + var user = await _userManager.GetUsersAsync(record.Subject); + + w.SubjectName = user.DisplayUserName(false, _displayUserSettingsHelper); + w.Owner = entry.RootFolderType == FolderType.USER + ? entry.RootCreateBy == record.Subject + : entry.CreateBy == record.Subject; + w.LockedRights = record.Subject == _authContext.CurrentAccount.ID; + } + + return w; + } +} + +public class AceProcessingResult +{ + public bool Changed { get; } + public string Warning { get; } + public IReadOnlyList<(EventType Event, AceWrapper Ace)> HandledAces { get; } + + public AceProcessingResult(bool changed, string warning, IReadOnlyList<(EventType Event, AceWrapper Ace)> handledAces) + { + Changed = changed; + Warning = warning; + HandledAces = handledAces; + } +} + +public enum EventType +{ + Update, + Create, + Remove } \ No newline at end of file diff --git a/products/ASC.Files/Server/Api/VirtualRoomsController.cs b/products/ASC.Files/Server/Api/VirtualRoomsController.cs index f029ae6200..b92052cb9b 100644 --- a/products/ASC.Files/Server/Api/VirtualRoomsController.cs +++ b/products/ASC.Files/Server/Api/VirtualRoomsController.cs @@ -1,4 +1,4 @@ -// (c) Copyright Ascensio System SIA 2010-2022 +// (c) Copyright Ascensio System SIA 2010-2022 // // This program is a free software product. // You can redistribute it and/or modify it under the terms @@ -40,7 +40,8 @@ public class VirtualRoomsInternalController : VirtualRoomsController FileDtoHelper fileDtoHelper, FileShareDtoHelper fileShareDtoHelper, IMapper mapper, - SocketManager socketManager) : base( + SocketManager socketManager, + ApiContext apiContext) : base( globalFolderHelper, fileOperationDtoHelper, coreBaseSettings, @@ -51,7 +52,8 @@ public class VirtualRoomsInternalController : VirtualRoomsController fileDtoHelper, fileShareDtoHelper, mapper, - socketManager) + socketManager, + apiContext) { } @@ -88,7 +90,8 @@ public class VirtualRoomsThirdPartyController : VirtualRoomsController FileDtoHelper fileDtoHelper, FileShareDtoHelper fileShareDtoHelper, IMapper mapper, - SocketManager socketManager) : base( + SocketManager socketManager, + ApiContext apiContext) : base( globalFolderHelper, fileOperationDtoHelper, coreBaseSettings, @@ -99,7 +102,8 @@ public class VirtualRoomsThirdPartyController : VirtualRoomsController fileDtoHelper, fileShareDtoHelper, mapper, - socketManager) + socketManager, + apiContext) { } @@ -135,6 +139,7 @@ public abstract class VirtualRoomsController : ApiControllerBase private readonly FileShareDtoHelper _fileShareDtoHelper; private readonly IMapper _mapper; private readonly SocketManager _socketManager; + private readonly ApiContext _apiContext; protected VirtualRoomsController( GlobalFolderHelper globalFolderHelper, @@ -147,7 +152,8 @@ public abstract class VirtualRoomsController : ApiControllerBase FileDtoHelper fileDtoHelper, FileShareDtoHelper fileShareDtoHelper, IMapper mapper, - SocketManager socketManager) : base(folderDtoHelper, fileDtoHelper) + SocketManager socketManager, + ApiContext apiContext) : base(folderDtoHelper, fileDtoHelper) { _globalFolderHelper = globalFolderHelper; _fileOperationDtoHelper = fileOperationDtoHelper; @@ -158,6 +164,7 @@ public abstract class VirtualRoomsController : ApiControllerBase _fileShareDtoHelper = fileShareDtoHelper; _mapper = mapper; _socketManager = socketManager; + _apiContext = apiContext; } /// @@ -286,22 +293,25 @@ public abstract class VirtualRoomsController : ApiControllerBase var result = new RoomSecurityDto(); - if (inDto.Invitations != null && inDto.Invitations.Any()) + if (inDto.Invitations == null || !inDto.Invitations.Any()) { - var wrappers = _mapper.Map, List>(inDto.Invitations); - - var aceCollection = new AceCollection - { - Files = Array.Empty(), - Folders = new[] { id }, - Aces = wrappers, - Message = inDto.Message - }; - - result.Warning = await _fileStorageService.SetAceObjectAsync(aceCollection, inDto.Notify); + return result; } - result.Members = await GetRoomSecurityInfoAsync(id).ToListAsync(); + var wrappers = _mapper.Map, List>(inDto.Invitations); + + var aceCollection = new AceCollection + { + Files = Array.Empty(), + Folders = new[] { id }, + Aces = wrappers, + Message = inDto.Message + }; + + result.Warning = await _fileStorageService.SetAceObjectAsync(aceCollection, inDto.Notify); + result.Members = await _fileStorageService.GetSharedInfoAsync(id, inDto.Invitations.Select(s => s.Id)) + .SelectAwait(async a => await _fileShareDtoHelper.Get(a)) + .ToListAsync(); return result; } @@ -312,19 +322,30 @@ public abstract class VirtualRoomsController : ApiControllerBase /// Get room access rights /// Rooms /// Room ID + /// + /// Share type filter + /// /// Security information of room files /// api/2.0/files/rooms/{id}/share /// GET /// list [HttpGet("rooms/{id}/share")] - public async IAsyncEnumerable GetRoomSecurityInfoAsync(T id) + public async IAsyncEnumerable GetRoomSecurityInfoAsync(T id, ShareFilterType filterType = ShareFilterType.User) { - var fileShares = await _fileStorageService.GetSharedInfoAsync(Array.Empty(), new[] { id }); + var offset = Convert.ToInt32(_apiContext.StartIndex); + var count = Convert.ToInt32(_apiContext.Count); + var counter = 0; - foreach (var fileShareDto in fileShares) + var totalCountTask = _fileStorageService.GetRoomSharesCountAsync(id, filterType); + + await foreach (var ace in _fileStorageService.GetRoomSharedInfoAsync(id, filterType, offset, count)) { - yield return await _fileShareDtoHelper.Get(fileShareDto); + counter++; + + yield return await _fileShareDtoHelper.Get(ace); } + + _apiContext.SetCount(counter).SetTotalCount(await totalCountTask); } /// @@ -339,9 +360,9 @@ public abstract class VirtualRoomsController : ApiControllerBase /// PUT /// list [HttpPut("rooms/{id}/links")] - public async IAsyncEnumerable SetLinkAsync(T id, LinkRequestDto inDto) + public async Task SetLinkAsync(T id, LinkRequestDto inDto) { - var fileShares = inDto.LinkType switch + var linkAce = inDto.LinkType switch { LinkType.Invitation => await _fileStorageService.SetInvitationLinkAsync(id, inDto.LinkId, inDto.Title, inDto.Access), LinkType.External => await _fileStorageService.SetExternalLinkAsync(id, FileEntryType.Folder, inDto.LinkId, inDto.Title, @@ -349,10 +370,7 @@ public abstract class VirtualRoomsController : ApiControllerBase _ => throw new InvalidOperationException() }; - foreach (var fileShareDto in fileShares) - { - yield return await _fileShareDtoHelper.Get(fileShareDto); - } + return linkAce != null ? await _fileShareDtoHelper.Get(linkAce) : null; } /// @@ -369,21 +387,24 @@ public abstract class VirtualRoomsController : ApiControllerBase [HttpGet("rooms/{id}/links")] public async IAsyncEnumerable GetLinksAsync(T id, LinkType? type) { - var subjectTypes = type.HasValue - ? type.Value switch + var filterType = type.HasValue ? type.Value switch { - LinkType.Invitation => new[] { SubjectType.InvitationLink }, - LinkType.External => new[] { SubjectType.ExternalLink }, - _ => new[] { SubjectType.InvitationLink, SubjectType.ExternalLink } - } - : new[] { SubjectType.InvitationLink, SubjectType.ExternalLink }; + LinkType.Invitation => ShareFilterType.InvitationLink, + LinkType.External => ShareFilterType.ExternalLink, + _ => ShareFilterType.Link + } + : ShareFilterType.Link; - var fileShares = await _fileStorageService.GetSharedInfoAsync(Array.Empty(), new[] { id }, subjectTypes, true); - - foreach (var fileShareDto in fileShares) + var counter = 0; + + await foreach (var ace in _fileStorageService.GetRoomSharedInfoAsync(id, filterType, 0, 100)) { - yield return await _fileShareDtoHelper.Get(fileShareDto); + counter++; + + yield return await _fileShareDtoHelper.Get(ace); } + + _apiContext.SetCount(counter); } /// @@ -518,9 +539,9 @@ public abstract class VirtualRoomsController : ApiControllerBase /// api/2.0/files/rooms/{id}/resend /// POST [HttpPost("rooms/{id}/resend")] - public async Task ResendEmailInvitationsAsync(T id, UserInvintationRequestDto inDto) + public async Task ResendEmailInvitationsAsync(T id, UserInvitationRequestDto inDto) { - await _fileStorageService.ResendEmailInvitationsAsync(id, inDto.UsersIds); + await _fileStorageService.ResendEmailInvitationsAsync(id, inDto.UsersIds, inDto.ResendAll); } protected void ErrorIfNotDocSpace() diff --git a/products/ASC.People/Server/Api/UserController.cs b/products/ASC.People/Server/Api/UserController.cs index 10e9235c06..131dd0b580 100644 --- a/products/ASC.People/Server/Api/UserController.cs +++ b/products/ASC.People/Server/Api/UserController.cs @@ -1889,3 +1889,72 @@ public class UserController : PeopleControllerBase // } //} } + +[ConstraintRoute("int")] +public class UserControllerAdditionalInternal : UserControllerAdditional +{ + public UserControllerAdditionalInternal( + EmployeeFullDtoHelper employeeFullDtoHelper, + FileSecurity fileSecurity, + ApiContext apiContext, + IDaoFactory daoFactory) + : base(employeeFullDtoHelper, fileSecurity, apiContext, daoFactory) + { + + } +} + +public class UserControllerAdditionalThirdParty : UserControllerAdditional +{ + public UserControllerAdditionalThirdParty( + EmployeeFullDtoHelper employeeFullDtoHelper, + FileSecurity fileSecurity, + ApiContext apiContext, + IDaoFactory daoFactory) + : base(employeeFullDtoHelper, fileSecurity, apiContext, daoFactory) + { + + } +} + +public class UserControllerAdditional : ApiControllerBase +{ + private readonly EmployeeFullDtoHelper _employeeFullDtoHelper; + private readonly FileSecurity _fileSecurity; + private readonly ApiContext _apiContext; + private readonly IDaoFactory _daoFactory; + + public UserControllerAdditional( + EmployeeFullDtoHelper employeeFullDtoHelper, + FileSecurity fileSecurity, + ApiContext apiContext, + IDaoFactory daoFactory) + { + _employeeFullDtoHelper = employeeFullDtoHelper; + _fileSecurity = fileSecurity; + _apiContext = apiContext; + _daoFactory = daoFactory; + } + + [HttpGet("room/{id}")] + public async IAsyncEnumerable GetUsersWithRoomSharedAsync(T id, EmployeeStatus? employeeStatus, EmployeeActivationStatus? activationStatus, bool? excludeShared) + { + var offset = Convert.ToInt32(_apiContext.StartIndex); + var count = Convert.ToInt32(_apiContext.Count); + + var room = (await _daoFactory.GetFolderDao().GetFolderAsync(id)).NotFoundIfNull(); + var totalCountTask = _fileSecurity.GetUsersWithSharedCountAsync(room, _apiContext.FilterValue, employeeStatus, activationStatus, excludeShared ?? false); + + var counter = 0; + + await foreach (var u in _fileSecurity.GetUsersWithSharedAsync(room, _apiContext.FilterValue, employeeStatus, activationStatus, excludeShared ?? false, offset, + count)) + { + counter++; + + yield return await _employeeFullDtoHelper.GetFullAsync(u.UserInfo, u.Shared); + } + + _apiContext.SetCount(counter).SetTotalCount(await totalCountTask); + } +} \ No newline at end of file diff --git a/products/ASC.People/Server/GlobalUsings.cs b/products/ASC.People/Server/GlobalUsings.cs index 2898587e2e..6575d22724 100644 --- a/products/ASC.People/Server/GlobalUsings.cs +++ b/products/ASC.People/Server/GlobalUsings.cs @@ -24,23 +24,24 @@ // 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 -global using System.Globalization; +global using System.Globalization; global using System.Net.Mail; global using System.Security; -global using System.Security.Claims; +global using System.Security.Claims; global using System.ServiceModel.Security; global using System.Web; - + global using ASC.Api.Core; global using ASC.Api.Core.Convention; global using ASC.Api.Core.Extensions; global using ASC.Api.Core.Model; -global using ASC.Api.Core.Security; +global using ASC.Api.Core.Security; +global using ASC.Api.Core.Routing; global using ASC.Api.Utils; global using ASC.Common; -global using ASC.Common.Caching; +global using ASC.Common.Caching; global using ASC.Common.Log; -global using ASC.Common.Threading; +global using ASC.Common.Threading; global using ASC.Common.Utils; global using ASC.Common.Web; global using ASC.Core; @@ -54,19 +55,19 @@ global using ASC.Data.Reassigns; global using ASC.FederatedLogin; global using ASC.FederatedLogin.LoginProviders; global using ASC.FederatedLogin.Profile; -global using ASC.Files.Core; -global using ASC.Files.Core.Core; -global using ASC.Files.Core.EF; +global using ASC.Files.Core; +global using ASC.Files.Core.Core; +global using ASC.Files.Core.EF; global using ASC.Files.Core.Resources; global using ASC.Files.Core.Security; -global using ASC.Files.Core.VirtualRooms; +global using ASC.Files.Core.VirtualRooms; global using ASC.MessagingSystem.Core; global using ASC.MessagingSystem.EF.Model; global using ASC.People; global using ASC.People.Api; global using ASC.People.ApiModels.RequestDto; global using ASC.People.ApiModels.ResponseDto; -global using ASC.People.Log; +global using ASC.People.Log; global using ASC.People.Resources; global using ASC.Security.Cryptography; global using ASC.Web.Api.Models; @@ -74,30 +75,30 @@ global using ASC.Web.Api.Routing; global using ASC.Web.Core; global using ASC.Web.Core.Mobile; global using ASC.Web.Core.PublicResources; -global using ASC.Web.Core.Quota; +global using ASC.Web.Core.Quota; global using ASC.Web.Core.Users; global using ASC.Web.Core.Utility; global using ASC.Web.Files; global using ASC.Web.Studio.Core; global using ASC.Web.Studio.Core.Notify; global using ASC.Web.Studio.Utility; - + global using Autofac; - + global using Microsoft.AspNetCore.Http.Extensions; global using Microsoft.AspNetCore.Mvc; global using Microsoft.AspNetCore.RateLimiting; -global using Microsoft.EntityFrameworkCore; +global using Microsoft.EntityFrameworkCore; global using Microsoft.Extensions.Hosting.WindowsServices; - + global using SixLabors.ImageSharp; -global using SixLabors.ImageSharp.Formats; - +global using SixLabors.ImageSharp.Formats; + global using Module = ASC.Api.Core.Module; -global using SecurityContext = ASC.Core.SecurityContext; -global using AllowAnonymousAttribute = Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute; -global using AuthorizeAttribute = Microsoft.AspNetCore.Authorization.AuthorizeAttribute; -global using HttpDeleteAttribute = Microsoft.AspNetCore.Mvc.HttpDeleteAttribute; -global using HttpGetAttribute = Microsoft.AspNetCore.Mvc.HttpGetAttribute; -global using HttpPostAttribute = Microsoft.AspNetCore.Mvc.HttpPostAttribute; +global using SecurityContext = ASC.Core.SecurityContext; +global using AllowAnonymousAttribute = Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute; +global using AuthorizeAttribute = Microsoft.AspNetCore.Authorization.AuthorizeAttribute; +global using HttpDeleteAttribute = Microsoft.AspNetCore.Mvc.HttpDeleteAttribute; +global using HttpGetAttribute = Microsoft.AspNetCore.Mvc.HttpGetAttribute; +global using HttpPostAttribute = Microsoft.AspNetCore.Mvc.HttpPostAttribute; global using HttpPutAttribute = Microsoft.AspNetCore.Mvc.HttpPutAttribute; \ No newline at end of file diff --git a/public/images/info.outline.react.svg b/public/images/info.outline.react.svg index ac41fda0d9..38fb3cf6db 100644 --- a/public/images/info.outline.react.svg +++ b/public/images/info.outline.react.svg @@ -1,12 +1,10 @@ - - - - - + + + - - + + - + \ No newline at end of file diff --git a/web/ASC.Web.Core/Users/UserPhotoManager.cs b/web/ASC.Web.Core/Users/UserPhotoManager.cs index 4126a46126..5f14ee6ebb 100644 --- a/web/ASC.Web.Core/Users/UserPhotoManager.cs +++ b/web/ASC.Web.Core/Users/UserPhotoManager.cs @@ -164,19 +164,17 @@ public class UserPhotoManagerCache public string SearchInCache(Guid userId, Size size) { - string fileName = null; - if (!_photofiles.TryGetValue(userId, out var val)) { return null; } - if (!val.TryGetValue(UserPhotoManager.ToCache(size), out fileName)) + if (!val.TryGetValue(UserPhotoManager.ToCache(size), out var fileName)) { return null; } - - if (String.IsNullOrEmpty(fileName)) + + if (string.IsNullOrEmpty(fileName)) { fileName = val.Values.FirstOrDefault(x => !string.IsNullOrEmpty(x) && x.Contains("_orig_")); } diff --git a/yarn.lock b/yarn.lock index 16876c82a7..e218ee00d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -108,13 +108,13 @@ __metadata: linkType: hard "@aws-sdk/client-cloudwatch-logs@npm:^3.297.0": - version: 3.418.0 - resolution: "@aws-sdk/client-cloudwatch-logs@npm:3.418.0" + version: 3.421.0 + resolution: "@aws-sdk/client-cloudwatch-logs@npm:3.421.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.418.0 - "@aws-sdk/credential-provider-node": 3.418.0 + "@aws-sdk/client-sts": 3.421.0 + "@aws-sdk/credential-provider-node": 3.421.0 "@aws-sdk/middleware-host-header": 3.418.0 "@aws-sdk/middleware-logger": 3.418.0 "@aws-sdk/middleware-recursion-detection": 3.418.0 @@ -149,13 +149,13 @@ __metadata: "@smithy/util-utf8": ^2.0.0 tslib: ^2.5.0 uuid: ^8.3.2 - checksum: cf250588753d42dbbd4605425aeb0a7a1838a0b46f0054f0f2e18c7075abfde3509d7ca9f25236a4188bff6e640da44d47d4236b8d479e209e5633c04ab55665 + checksum: 8335143173c8c26e4c36af9ef323c42afe002acdeeade86b4ca2b0877e4382b0e18abdd66e9a8dcb2a5e308e450b0eb4002ef9d8f056cc612466337ae955b500 languageName: node linkType: hard -"@aws-sdk/client-sso@npm:3.418.0": - version: 3.418.0 - resolution: "@aws-sdk/client-sso@npm:3.418.0" +"@aws-sdk/client-sso@npm:3.421.0": + version: 3.421.0 + resolution: "@aws-sdk/client-sso@npm:3.421.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 @@ -191,17 +191,17 @@ __metadata: "@smithy/util-retry": ^2.0.2 "@smithy/util-utf8": ^2.0.0 tslib: ^2.5.0 - checksum: 54c17953ac1143f07fff8a462bddeb87a5cb10d5ffb269ed93e837da7b37d4d3ae81aa0986b23c0e687712c58320bb28b5ac0ab4a8cb2841fbb0ce2436df648d + checksum: 2dcfbd104668f49fc5b18af6a5bced3845e75c1facab7fd26e97c40ea447e9c29d4f3a5e14d90e66a008db251a6aa27fc8af7652e3c1d8916853ff37df28c72c languageName: node linkType: hard -"@aws-sdk/client-sts@npm:3.418.0": - version: 3.418.0 - resolution: "@aws-sdk/client-sts@npm:3.418.0" +"@aws-sdk/client-sts@npm:3.421.0": + version: 3.421.0 + resolution: "@aws-sdk/client-sts@npm:3.421.0" dependencies: "@aws-crypto/sha256-browser": 3.0.0 "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/credential-provider-node": 3.418.0 + "@aws-sdk/credential-provider-node": 3.421.0 "@aws-sdk/middleware-host-header": 3.418.0 "@aws-sdk/middleware-logger": 3.418.0 "@aws-sdk/middleware-recursion-detection": 3.418.0 @@ -237,7 +237,7 @@ __metadata: "@smithy/util-utf8": ^2.0.0 fast-xml-parser: 4.2.5 tslib: ^2.5.0 - checksum: 594ce1a04a1c5aa6b21e7ddf0342d9705e27a60bfba3b30b192152987aba6bd6227d8e98e6ccddae1ccf7090de9e12737f8cd996918302ca574b69fe6f93ad37 + checksum: c21a28288341a7f6749e61391846b7a6754de3594137a7c436b1d96d82a8043adc921b45f8d22e2894e50cdf3799f51143a42f214ae552bcd0e0088c85d2de09 languageName: node linkType: hard @@ -253,13 +253,13 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/credential-provider-ini@npm:3.418.0": - version: 3.418.0 - resolution: "@aws-sdk/credential-provider-ini@npm:3.418.0" +"@aws-sdk/credential-provider-ini@npm:3.421.0": + version: 3.421.0 + resolution: "@aws-sdk/credential-provider-ini@npm:3.421.0" dependencies: "@aws-sdk/credential-provider-env": 3.418.0 "@aws-sdk/credential-provider-process": 3.418.0 - "@aws-sdk/credential-provider-sso": 3.418.0 + "@aws-sdk/credential-provider-sso": 3.421.0 "@aws-sdk/credential-provider-web-identity": 3.418.0 "@aws-sdk/types": 3.418.0 "@smithy/credential-provider-imds": ^2.0.0 @@ -267,18 +267,18 @@ __metadata: "@smithy/shared-ini-file-loader": ^2.0.6 "@smithy/types": ^2.3.3 tslib: ^2.5.0 - checksum: c519d15523d872c3d3249df48e513e7c648f1668bb9baec6af895613fcdf14f552d917337585bed744260796b5f0bd033215102c1bbceb588d46738b7d5093d1 + checksum: 038e115250a966a28b43faa6a70ef4e9d543d51e0eed8446d0939c44a7a0c3333a3dcb0a80aa5c2b328cc225228aef8f8aa5b31ba1725316b3019b88ad091ad8 languageName: node linkType: hard -"@aws-sdk/credential-provider-node@npm:3.418.0": - version: 3.418.0 - resolution: "@aws-sdk/credential-provider-node@npm:3.418.0" +"@aws-sdk/credential-provider-node@npm:3.421.0": + version: 3.421.0 + resolution: "@aws-sdk/credential-provider-node@npm:3.421.0" dependencies: "@aws-sdk/credential-provider-env": 3.418.0 - "@aws-sdk/credential-provider-ini": 3.418.0 + "@aws-sdk/credential-provider-ini": 3.421.0 "@aws-sdk/credential-provider-process": 3.418.0 - "@aws-sdk/credential-provider-sso": 3.418.0 + "@aws-sdk/credential-provider-sso": 3.421.0 "@aws-sdk/credential-provider-web-identity": 3.418.0 "@aws-sdk/types": 3.418.0 "@smithy/credential-provider-imds": ^2.0.0 @@ -286,7 +286,7 @@ __metadata: "@smithy/shared-ini-file-loader": ^2.0.6 "@smithy/types": ^2.3.3 tslib: ^2.5.0 - checksum: bbaf9d9f4c7b7ab0c9b5772e3abcb6642021d8d10081f31ad4de34d1ba0522d9dc817aced7f28e2b8fff443177cfe90c295c0263f96be61d8c4e3849b44cb7c7 + checksum: 0b0249ebc58383e7149f8b5398ca8d6af93df397fae4c0feaea9debd4617a90d7c9a01486709173799edaf06c6c34e9c979969da1a41bdb681d01c6661e6e0e3 languageName: node linkType: hard @@ -303,18 +303,18 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/credential-provider-sso@npm:3.418.0": - version: 3.418.0 - resolution: "@aws-sdk/credential-provider-sso@npm:3.418.0" +"@aws-sdk/credential-provider-sso@npm:3.421.0": + version: 3.421.0 + resolution: "@aws-sdk/credential-provider-sso@npm:3.421.0" dependencies: - "@aws-sdk/client-sso": 3.418.0 + "@aws-sdk/client-sso": 3.421.0 "@aws-sdk/token-providers": 3.418.0 "@aws-sdk/types": 3.418.0 "@smithy/property-provider": ^2.0.0 "@smithy/shared-ini-file-loader": ^2.0.6 "@smithy/types": ^2.3.3 tslib: ^2.5.0 - checksum: e666dd10f50b9510b09f64bdac66f97ac18e1521bdb6fd642c1446740fbeaf5d7a609dfb5c7166561cb5cecd5149b0ae08d9b9df919641168c022657f297414d + checksum: 7fd59a74fe28602ac0877c9e845a95a95a3be91869be33776576c2cbe41cce8b5921480e769a1fdf02db41f67d792e9f1ea93d9e5f0256736cea8f24d8839b24 languageName: node linkType: hard @@ -3774,9 +3774,9 @@ __metadata: linkType: hard "@eslint-community/regexpp@npm:^4.6.1": - version: 4.8.1 - resolution: "@eslint-community/regexpp@npm:4.8.1" - checksum: 82d62c845ef42b810f268cfdc84d803a2da01735fb52e902fd34bdc09f92464a094fd8e4802839874b000b2f73f67c972859e813ba705233515d3e954f234bf2 + version: 4.9.0 + resolution: "@eslint-community/regexpp@npm:4.9.0" + checksum: 82411f0643ab9bfd271bf12c8c75031266b13595d9371585ee3b0d680d918d4abf37c7e94d0da22e45817c9bbc59b79dfcbd672050dfb00af88fb89c80fd420f languageName: node linkType: hard @@ -4254,12 +4254,12 @@ __metadata: linkType: hard "@grpc/grpc-js@npm:^1.3.2": - version: 1.9.3 - resolution: "@grpc/grpc-js@npm:1.9.3" + version: 1.9.4 + resolution: "@grpc/grpc-js@npm:1.9.4" dependencies: "@grpc/proto-loader": ^0.7.8 "@types/node": ">=12.12.47" - checksum: 09634de9f871a17c95db95338fe472522d5dca0f77b622e3a497ef806262813445ba1a1f3261f03461d84ac985e420317d5638b3314fcb524ee2e47e512ffa64 + checksum: 83616d3727f16d9caf57fb16f8525e6884856569624a244ef8dde2873b7d7a439d302fd0a82d28bbbf9ba9777d3b25a58c706e35daa30120abb04f1646aa0e11 languageName: node linkType: hard @@ -8386,11 +8386,11 @@ __metadata: linkType: hard "@types/istanbul-lib-report@npm:*": - version: 3.0.0 - resolution: "@types/istanbul-lib-report@npm:3.0.0" + version: 3.0.1 + resolution: "@types/istanbul-lib-report@npm:3.0.1" dependencies: "@types/istanbul-lib-coverage": "*" - checksum: 656398b62dc288e1b5226f8880af98087233cdb90100655c989a09f3052b5775bf98ba58a16c5ae642fb66c61aba402e07a9f2bff1d1569e3b306026c59f3f36 + checksum: cfc66de48577bb7b2636a6afded7056483693c3ea70916276518cdfaa0d4b51bf564ded88fb13e75716665c3af3d4d54e9c2de042c0219dcabad7e81c398688b languageName: node linkType: hard @@ -8405,11 +8405,11 @@ __metadata: linkType: hard "@types/istanbul-reports@npm:^3.0.0": - version: 3.0.1 - resolution: "@types/istanbul-reports@npm:3.0.1" + version: 3.0.2 + resolution: "@types/istanbul-reports@npm:3.0.2" dependencies: "@types/istanbul-lib-report": "*" - checksum: f1ad54bc68f37f60b30c7915886b92f86b847033e597f9b34f2415acdbe5ed742fa559a0a40050d74cdba3b6a63c342cac1f3a64dba5b68b66a6941f4abd7903 + checksum: f52028d6fe4d28f0085dd7ed66ccfa6af632579e9a4091b90928ffef93d4dbec0bacd49e9caf1b939d05df9eafc5ac1f5939413cdf8ac59fbe4b29602d4d0939 languageName: node linkType: hard @@ -8460,46 +8460,46 @@ __metadata: linkType: hard "@types/mdast@npm:^3.0.0": - version: 3.0.12 - resolution: "@types/mdast@npm:3.0.12" + version: 3.0.13 + resolution: "@types/mdast@npm:3.0.13" dependencies: "@types/unist": ^2 - checksum: 83adb8679b9d139f69f63554d120af921e9f1289e9903a2c99e0554a327c8524a6c0beccdc0721e4fdbccc606e81964fecb0d390d53df0f74360938e22f1a469 + checksum: f13fa17a2931ed1492a2f0012a3abd6de3a2d1128145981321909e03fedba80162668f284a4af92aca3732b27e933c5f4772336d96b9ae660bfde696d07abbe6 languageName: node linkType: hard "@types/mdurl@npm:^1.0.0": - version: 1.0.2 - resolution: "@types/mdurl@npm:1.0.2" - checksum: 79c7e523b377f53cf1f5a240fe23d0c6cae856667692bd21bf1d064eafe5ccc40ae39a2aa0a9a51e8c94d1307228c8f6b121e847124591a9a828c3baf65e86e2 + version: 1.0.3 + resolution: "@types/mdurl@npm:1.0.3" + checksum: 5bbed4f0eb9f60040fa26be77aa2158ca468b6423876cec0d2043e7f8298e83b8e5b95fb66056327b02d747c4d376aed16c11ff3fdc4cb3dca327a6931a71f18 languageName: node linkType: hard "@types/mdx@npm:^2.0.0": - version: 2.0.7 - resolution: "@types/mdx@npm:2.0.7" - checksum: c746659ebea471535d99a49cc935dc9f831fac22def3fa8c8a0883ad2cae71c4cca1d8563fa60d0e2730b14cb13e95c32af8dfda455a4937476a7d9e2748d641 + version: 2.0.8 + resolution: "@types/mdx@npm:2.0.8" + checksum: 4a7c2241c37e87aaab044c561f24874fabcd5cd2d6feb28dc665e2c80562afa7ddf94391a3b8ab3f76041199c8bafcff9131acf6d060f1aca45d763b171bbc29 languageName: node linkType: hard "@types/mime-types@npm:^2.1.0": - version: 2.1.1 - resolution: "@types/mime-types@npm:2.1.1" - checksum: 106b5d556add46446a579ad25ff15d6b421851790d887edcad558c90c1e64b1defc72bfbaf4b08f208916e21d9cc45cdb951d77be51268b18221544cfe054a3c + version: 2.1.2 + resolution: "@types/mime-types@npm:2.1.2" + checksum: 9e3c78f1c63211e0450901212566a046da68d4438a5e543333ec9b0be3259bd5d01532734dc51ead40104889b98d12c7663b65212a318aafad3e34c98204e9e1 languageName: node linkType: hard "@types/mime@npm:*": - version: 3.0.1 - resolution: "@types/mime@npm:3.0.1" - checksum: 4040fac73fd0cea2460e29b348c1a6173da747f3a87da0dbce80dd7a9355a3d0e51d6d9a401654f3e5550620e3718b5a899b2ec1debf18424e298a2c605346e7 + version: 3.0.2 + resolution: "@types/mime@npm:3.0.2" + checksum: 09cf74f6377d1b27f4a24512cb689ad30af59880ac473ed6f7bc5285ecde88bbe8fe500789340ad57810da9d6fe1704f86e8bfe147b9ea76d58925204a60b906 languageName: node linkType: hard "@types/mime@npm:^1": - version: 1.3.2 - resolution: "@types/mime@npm:1.3.2" - checksum: 0493368244cced1a69cb791b485a260a422e6fcc857782e1178d1e6f219f1b161793e9f87f5fae1b219af0f50bee24fcbe733a18b4be8fdd07a38a8fb91146fd + version: 1.3.3 + resolution: "@types/mime@npm:1.3.3" + checksum: 7e27dede6517c1d604821a8a5412d6b7131decc8397ad4bac9216fc90dea26c9571426623ebeea2a9b89dbfb89ad98f7370a3c62cd2be8896c6e897333b117c9 languageName: node linkType: hard @@ -8511,18 +8511,18 @@ __metadata: linkType: hard "@types/morgan@npm:^1.9.4": - version: 1.9.5 - resolution: "@types/morgan@npm:1.9.5" + version: 1.9.6 + resolution: "@types/morgan@npm:1.9.6" dependencies: "@types/node": "*" - checksum: f98deb4c7f2ad6049ad34ed7b0f0d427546bdf2358011070af9d597de1b0a03b38cc10cfe65ef2e7673e569c384303d949e76df701acefe288d547f614142973 + checksum: 6525248325a74342f929c958be69c0840c8f3a288e003a8904319cae92e531f17a8aa2700701e66775adcca7f9506dd630fec2f95dc04a3e73add04fde42aab8 languageName: node linkType: hard "@types/ms@npm:*": - version: 0.7.31 - resolution: "@types/ms@npm:0.7.31" - checksum: daadd354aedde024cce6f5aa873fefe7b71b22cd0e28632a69e8b677aeb48ae8caa1c60e5919bb781df040d116b01cb4316335167a3fc0ef6a63fa3614c0f6da + version: 0.7.32 + resolution: "@types/ms@npm:0.7.32" + checksum: 610744605c5924aa2657c8a62d307052af4f0e38e2aa015f154ef03391fabb4fd903f9c9baacb41f6e5798b8697e898463c351e5faf638738603ed29137b5254 languageName: node linkType: hard @@ -8537,9 +8537,9 @@ __metadata: linkType: hard "@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0": - version: 20.6.5 - resolution: "@types/node@npm:20.6.5" - checksum: b849e849cf7631458a65c5019c81962028e306d8c4455a48422277b240f5a7eb8a1f1dafa60306bd4c773b77263bb8b05c074b1026e868bd137bb2022cf63ea2 + version: 20.7.1 + resolution: "@types/node@npm:20.7.1" + checksum: 3140bd6c9130f1ed73a78ce7a1765ee43e155c1eea50eea45e18faeb31d11d97a84fffdc5e3a97582101d2f57d2652a50f510ede6c702780267bad74c822d56c languageName: node linkType: hard @@ -8551,9 +8551,9 @@ __metadata: linkType: hard "@types/node@npm:^18.15.5": - version: 18.17.19 - resolution: "@types/node@npm:18.17.19" - checksum: 6ab47127cd7534511aa199550659d56b44e5a6dbec9df054d0cde279926b4d43f0e6438f92c8392b039ab4e2a85aa0f698b95926430aff860e23bfc36c96576c + version: 18.18.0 + resolution: "@types/node@npm:18.18.0" + checksum: 61bcffa28eb713e7a4c66fd369df603369c3f834a783faeced95fe3e78903faa25f1a704d49e054f41d71b7915eeb066d10a37cc699421fcf5dd267f96ad5808 languageName: node linkType: hard @@ -8616,27 +8616,27 @@ __metadata: linkType: hard "@types/range-parser@npm:*": - version: 1.2.4 - resolution: "@types/range-parser@npm:1.2.4" - checksum: b7c0dfd5080a989d6c8bb0b6750fc0933d9acabeb476da6fe71d8bdf1ab65e37c136169d84148034802f48378ab94e3c37bb4ef7656b2bec2cb9c0f8d4146a95 + version: 1.2.5 + resolution: "@types/range-parser@npm:1.2.5" + checksum: db9aaa04a02d019395a9a4346475669a2864a32a6477ad0fc457bd2ef39a167cabe742f55a8a3fa8bc90abac795b716c22b37348bc3e19313ebe6c9310815233 languageName: node linkType: hard "@types/reach__router@npm:^1.2.3": - version: 1.3.11 - resolution: "@types/reach__router@npm:1.3.11" + version: 1.3.12 + resolution: "@types/reach__router@npm:1.3.12" dependencies: "@types/react": "*" - checksum: 6bcf40714e0dafff66cbf10a534320eae35dae8344f616d2ddf077937287a0df188dfbfb32fb8045cbc641139d1ab69ee5bb258a51642823cadefbcda020a044 + checksum: df838ba6bc3463c50ae35e704e11a9073bb0617c04676887a9ad87177b078d6593aa3cd8b63de07e791ac8b86183dbb0f20072c22dd40754487a47f0064c3fb7 languageName: node linkType: hard "@types/react-dom@npm:*, @types/react-dom@npm:^18.0.11": - version: 18.2.7 - resolution: "@types/react-dom@npm:18.2.7" + version: 18.2.8 + resolution: "@types/react-dom@npm:18.2.8" dependencies: "@types/react": "*" - checksum: e02ea908289a7ad26053308248d2b87f6aeafd73d0e2de2a3d435947bcea0422599016ffd1c3e38ff36c42f5e1c87c7417f05b0a157e48649e4a02f21727d54f + checksum: d36264631028d021b73cd9e015f10b95c4959ae1ce8f7a7419f318d1f05b1d063e6afffcd2a349a6bccd64ccc9ee9d2d976e1f0437643f0e7db621fa035bca65 languageName: node linkType: hard @@ -8659,13 +8659,13 @@ __metadata: linkType: hard "@types/react@npm:*, @types/react@npm:>=16, @types/react@npm:^18.0.28": - version: 18.2.22 - resolution: "@types/react@npm:18.2.22" + version: 18.2.23 + resolution: "@types/react@npm:18.2.23" dependencies: "@types/prop-types": "*" "@types/scheduler": "*" csstype: ^3.0.2 - checksum: 44289523dabaadcd3fd85689abb98f9ebcc8492d7e978348d1c986138acef4801030b279e89a19e38a6319e294bcea77559e37e0c803e4bacf2b8ae3a56ba587 + checksum: efb9d1ed1940c0e7ba08a21ffba5e266d8dbbb8fe618cfb97bc902dfc96385fdd8189e3f7f64b4aa13134f8e61947d60560deb23be151253c3a97b0d070897ca languageName: node linkType: hard @@ -8677,55 +8677,55 @@ __metadata: linkType: hard "@types/scheduler@npm:*": - version: 0.16.3 - resolution: "@types/scheduler@npm:0.16.3" - checksum: 2b0aec39c24268e3ce938c5db2f2e77f5c3dd280e05c262d9c2fe7d890929e4632a6b8e94334017b66b45e4f92a5aa42ba3356640c2a1175fa37bef2f5200767 + version: 0.16.4 + resolution: "@types/scheduler@npm:0.16.4" + checksum: a57b0f10da1b021e6bd5eeef8a1917dd3b08a8715bd8029e2ded2096d8f091bb1bb1fef2d66e139588a983c4bfbad29b59e48011141725fa83c76e986e1257d7 languageName: node linkType: hard "@types/semver@npm:^7.3.12, @types/semver@npm:^7.3.4": - version: 7.5.2 - resolution: "@types/semver@npm:7.5.2" - checksum: 743aa8a2b58e20b329c19bd2459152cb049d12fafab7279b90ac11e0f268c97efbcb606ea0c681cca03f79015381b40d9b1244349b354270bec3f939ed49f6e9 + version: 7.5.3 + resolution: "@types/semver@npm:7.5.3" + checksum: 349fdd1ab6c213bac5c991bac766bd07b8b12e63762462bb058740dcd2eb09c8193d068bb226f134661275f2022976214c0e727a4e5eb83ec1b131127c980d3e languageName: node linkType: hard "@types/send@npm:*": - version: 0.17.1 - resolution: "@types/send@npm:0.17.1" + version: 0.17.2 + resolution: "@types/send@npm:0.17.2" dependencies: "@types/mime": ^1 "@types/node": "*" - checksum: 10b620a5960058ef009afbc17686f680d6486277c62f640845381ec4baa0ea683fdd77c3afea4803daf5fcddd3fb2972c8aa32e078939f1d4e96f83195c89793 + checksum: 1ff5b1bd6a4f6fdc6402c7024781ff5dbd0e1f51a43c69529fb67c710943c7416d2f0d77c57c70fccf6616f25f838f32f960284526e408d4edae2e91e1fce95a languageName: node linkType: hard "@types/serve-index@npm:^1.9.1": - version: 1.9.1 - resolution: "@types/serve-index@npm:1.9.1" + version: 1.9.2 + resolution: "@types/serve-index@npm:1.9.2" dependencies: "@types/express": "*" - checksum: 026f3995fb500f6df7c3fe5009e53bad6d739e20b84089f58ebfafb2f404bbbb6162bbe33f72d2f2af32d5b8d3799c8e179793f90d9ed5871fb8591190bb6056 + checksum: 4fd0a8fcdd6e2b2d7704a539b7c1e0d143e9e00be4c3992394fe2ef7e9b67283d74b43db3f92b0e0717d779aa51184168bbae617d30456357cb95ec58aa59ea8 languageName: node linkType: hard "@types/serve-static@npm:*, @types/serve-static@npm:^1.13.10": - version: 1.15.2 - resolution: "@types/serve-static@npm:1.15.2" + version: 1.15.3 + resolution: "@types/serve-static@npm:1.15.3" dependencies: "@types/http-errors": "*" "@types/mime": "*" "@types/node": "*" - checksum: 15c261dbfc57890f7cc17c04d5b22b418dfa0330c912b46c5d8ae2064da5d6f844ef7f41b63c7f4bbf07675e97ebe6ac804b032635ec742ae45d6f1274259b3e + checksum: afa52252f0ba94cdb5391e80f23e17fd629bdf2a31be8876e2c4490312ed6b0570822dd7de7cea04c9002049e207709563568b7f4ee10bb9f456321db1e83e40 languageName: node linkType: hard "@types/sockjs@npm:^0.3.33": - version: 0.3.33 - resolution: "@types/sockjs@npm:0.3.33" + version: 0.3.34 + resolution: "@types/sockjs@npm:0.3.34" dependencies: "@types/node": "*" - checksum: b9bbb2b5c5ead2fb884bb019f61a014e37410bddd295de28184e1b2e71ee6b04120c5ba7b9954617f0bdf962c13d06249ce65004490889c747c80d3f628ea842 + checksum: 1d38b1976a97f5895a6be00cead1b2a59d842f023b6e35450b7eec8a3131c860c447aba3f7ea3c880c066d8277b0c76fae9d080be1aad475811b568ed6079d49 languageName: node linkType: hard @@ -8737,13 +8737,13 @@ __metadata: linkType: hard "@types/styled-components@npm:^5.1.26": - version: 5.1.27 - resolution: "@types/styled-components@npm:5.1.27" + version: 5.1.28 + resolution: "@types/styled-components@npm:5.1.28" dependencies: "@types/hoist-non-react-statics": "*" "@types/react": "*" csstype: ^3.0.2 - checksum: 6f8d99642f09f28a233a5bf93d3139389c3bf5e4c643620881cb286f581d6844f3c1c43be5bd9405d3b199b6a858bd8b0ec6ccbb17b2304a88d70e2a6f91a996 + checksum: 2e12be4fdc2d60f63e16f8f5cac98d8305e0686b3332a160fec39df2a277f16715b878e25384ddda29e48acf101110a29ca108b716296e9a06bf2740f1257385 languageName: node linkType: hard @@ -8798,9 +8798,9 @@ __metadata: linkType: hard "@types/webpack-env@npm:^1.16.0": - version: 1.18.1 - resolution: "@types/webpack-env@npm:1.18.1" - checksum: 3173c069763e51a96565d602af7e6dac9d772ae4aa6f26cac187cbf599a7f0b88f790b4b050b9dbdb0485daed3061b4a337863f3b8ce66f8a4e51f75ad387c6a + version: 1.18.2 + resolution: "@types/webpack-env@npm:1.18.2" + checksum: 883908ade827d35a10efc574fb6f2728a7c520d4296cf1507633ac7457204ccd697bc6c8cadac99bc5d96074a6109c658ebfde59f42ba5ba0fdfffc538892b0f languageName: node linkType: hard @@ -8814,11 +8814,11 @@ __metadata: linkType: hard "@types/ws@npm:*, @types/ws@npm:^8.5.1, @types/ws@npm:^8.5.4": - version: 8.5.5 - resolution: "@types/ws@npm:8.5.5" + version: 8.5.6 + resolution: "@types/ws@npm:8.5.6" dependencies: "@types/node": "*" - checksum: d00bf8070e6938e3ccf933010921c6ce78ac3606696ce37a393b27a9a603f7bd93ea64f3c5fa295a2f743575ba9c9a9fdb904af0f5fe2229bf2adf0630386e4a + checksum: 7addb0c5fa4e7713d5209afb8a90f1852b12c02cb537395adf7a05fbaf21205dc5f7c110fd5ad6f3dbf147112cbff33fb11d8633059cb344f0c14f595b1ea1fb languageName: node linkType: hard @@ -10813,16 +10813,16 @@ __metadata: linkType: hard "browserslist@npm:^4.14.5, browserslist@npm:^4.21.10, browserslist@npm:^4.21.9, browserslist@npm:^4.9.1": - version: 4.21.11 - resolution: "browserslist@npm:4.21.11" + version: 4.22.0 + resolution: "browserslist@npm:4.22.0" dependencies: - caniuse-lite: ^1.0.30001538 - electron-to-chromium: ^1.4.526 + caniuse-lite: ^1.0.30001539 + electron-to-chromium: ^1.4.530 node-releases: ^2.0.13 update-browserslist-db: ^1.0.13 bin: browserslist: cli.js - checksum: 89377745428d32c7bbec37055bc4b9e48aa859418ea3886b13218d825f8ea3053640f90d8652844ae855941fec0bffd2d69cf933035d6f9224b427d48d31eddf + checksum: 14fc119bbfb85b65e2ee4a82205fabf9327520d010c4c586f1176ceaf9136cfdb391397045a4eafaa9defe52b6dbdf875916714695826c69091a936d5838f9ec languageName: node linkType: hard @@ -11040,10 +11040,10 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001538": - version: 1.0.30001539 - resolution: "caniuse-lite@npm:1.0.30001539" - checksum: 8595905d6c7234173a4915439fdd69fa2c06b0272d56af63aee0df6114e1ee4727758af160c0db9844055f778e0ea27ed4714facf2e472a2e74d9f567f111f41 +"caniuse-lite@npm:^1.0.30001539": + version: 1.0.30001541 + resolution: "caniuse-lite@npm:1.0.30001541" + checksum: 972f6c223cf4ea2c6821b817b419249285006bbf67ebe415fe58097cf07551e3bae898586736d92f7c40b9f0ac28638dbf760631c23742b780affd0254f44d17 languageName: node linkType: hard @@ -13061,10 +13061,10 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.4.526": - version: 1.4.528 - resolution: "electron-to-chromium@npm:1.4.528" - checksum: e9b249929e012ce6c25f8d1e1965e7fb19a426cc72fdf72026a0ce010846ec1b0efe2730b1a1f3d1c98bd11200b94b73431bc5279211a9f4a28f2389e690c39c +"electron-to-chromium@npm:^1.4.530": + version: 1.4.532 + resolution: "electron-to-chromium@npm:1.4.532" + checksum: e9f77b5d6df84aa1f7598359ec2c988c3758e58106e63f2a0a6dc4756a6733b126316e61a79a2a6643aa2a0f9a1cf9ebe66c817dcb970a3fc9d8190342ef070a languageName: node linkType: hard @@ -14793,9 +14793,9 @@ __metadata: linkType: hard "fs-monkey@npm:^1.0.4": - version: 1.0.4 - resolution: "fs-monkey@npm:1.0.4" - checksum: 8b254c982905c0b7e028eab22b410dc35a5c0019c1c860456f5f54ae6a61666e1cb8c6b700d6c88cc873694c00953c935847b9959cc4dcf274aacb8673c1e8bf + version: 1.0.5 + resolution: "fs-monkey@npm:1.0.5" + checksum: 424b67f65b37fe66117ae2bb061f790fe6d4b609e1d160487c74b3d69fbf42f262c665ccfba32e8b5f113f96f92e9a58fcdebe42d5f6649bdfc72918093a3119 languageName: node linkType: hard @@ -15109,17 +15109,17 @@ __metadata: linkType: hard "glob@npm:^10.2.2": - version: 10.3.7 - resolution: "glob@npm:10.3.7" + version: 10.3.10 + resolution: "glob@npm:10.3.10" dependencies: foreground-child: ^3.1.0 - jackspeak: ^2.0.3 + jackspeak: ^2.3.5 minimatch: ^9.0.1 minipass: ^5.0.0 || ^6.0.2 || ^7.0.0 path-scurry: ^1.10.1 bin: glob: dist/esm/bin.mjs - checksum: 9a27f1fa8774c3a8ab8f05c26a77276edaf5418aac29aff70c5d847ef75dabf536554cb113e945193323fb769fbe32edde12559d2d52266f38662595cbc7a031 + checksum: 4f2fe2511e157b5a3f525a54092169a5f92405f24d2aed3142f4411df328baca13059f4182f1db1bf933e2c69c0bd89e57ae87edd8950cba8c7ccbe84f721cf3 languageName: node linkType: hard @@ -16941,16 +16941,16 @@ __metadata: languageName: node linkType: hard -"jackspeak@npm:^2.0.3": - version: 2.3.3 - resolution: "jackspeak@npm:2.3.3" +"jackspeak@npm:^2.3.5": + version: 2.3.6 + resolution: "jackspeak@npm:2.3.6" dependencies: "@isaacs/cliui": ^8.0.2 "@pkgjs/parseargs": ^0.11.0 dependenciesMeta: "@pkgjs/parseargs": optional: true - checksum: 4313a7c0cc44c7753c4cb9869935f0b06f4cf96827515f63f58ff46b3d2f6e29aba6b3b5151778397c3f5ae67ef8bfc48871967bd10343c27e90cff198ec7808 + checksum: 57d43ad11eadc98cdfe7496612f6bbb5255ea69fe51ea431162db302c2a11011642f50cfad57288bd0aea78384a0612b16e131944ad8ecd09d619041c8531b54 languageName: node linkType: hard @@ -21563,8 +21563,8 @@ __metadata: linkType: hard "rc-virtual-list@npm:^3.5.1": - version: 3.11.1 - resolution: "rc-virtual-list@npm:3.11.1" + version: 3.11.2 + resolution: "rc-virtual-list@npm:3.11.2" dependencies: "@babel/runtime": ^7.20.0 classnames: ^2.2.6 @@ -21573,7 +21573,7 @@ __metadata: peerDependencies: react: "*" react-dom: "*" - checksum: bd6b41b2ca452313534d4d5d1dc0efcb5baf1e26b815acf3553e69f96bb7eeb0980319284adf389f4bcc3f934265d08e8d7e193fb4677a3643bbd896080bced6 + checksum: b642e55ca2c4b56fb2048b5151a17e2f0f1c15cf3f4d8a26a4f1f370c95a6f4476743886b6633f083f107c82fbe86c5548bbf37b7e2f8b79615d90b62777824e languageName: node linkType: hard @@ -21794,15 +21794,15 @@ __metadata: linkType: hard "react-draggable@npm:^4.4.5": - version: 4.4.5 - resolution: "react-draggable@npm:4.4.5" + version: 4.4.6 + resolution: "react-draggable@npm:4.4.6" dependencies: clsx: ^1.1.1 prop-types: ^15.8.1 peerDependencies: react: ">= 16.3.0" react-dom: ">= 16.3.0" - checksum: 21c3775db086e13020967627c20acd41d1ddbc7c7d7fca51491a5bbb54a0aa7e1730a4bc9af17141eb50a4954e547a5e25b2368f5f54b70db6f2686a897bacf2 + checksum: 9b15aac59244873ac4561c5a2bead43a56e18d406e0a5f242bd4f9d151c074530c02b99387983104bf43417292f9cf8d063e554ed08d88792235e3fbc965f1b8 languageName: node linkType: hard @@ -22283,15 +22283,15 @@ __metadata: linkType: hard "react-tooltip@npm:^5.21.1": - version: 5.21.4 - resolution: "react-tooltip@npm:5.21.4" + version: 5.21.5 + resolution: "react-tooltip@npm:5.21.5" dependencies: "@floating-ui/dom": ^1.0.0 classnames: ^2.3.0 peerDependencies: react: ">=16.14.0" react-dom: ">=16.14.0" - checksum: 9b663c5d9a1472b96d3ed708ecce01052aed267dc739520c9b55389a5e42c195a6bcd0717219f260bf0dadee2452d80a7c5fa14c5c3f8b5cdc6a9174d7f42d4a + checksum: 067d6a5bc7c6b12b2ae054f855416b068bcc9fe59864b83c655e775066d6f81728b2ec13e80565968da7ce35c7c0286f8e154c635a1e79bb7a8233df8a356c51 languageName: node linkType: hard @@ -23300,7 +23300,14 @@ __metadata: languageName: node linkType: hard -"sax@npm:^1.2.4, sax@npm:~1.2.1, sax@npm:~1.2.4": +"sax@npm:^1.2.4": + version: 1.3.0 + resolution: "sax@npm:1.3.0" + checksum: 238ab3a9ba8c8f8aaf1c5ea9120386391f6ee0af52f1a6a40bbb6df78241dd05d782f2359d614ac6aae08c4c4125208b456548a6cf68625aa4fe178486e63ecd + languageName: node + linkType: hard + +"sax@npm:~1.2.1, sax@npm:~1.2.4": version: 1.2.4 resolution: "sax@npm:1.2.4" checksum: d3df7d32b897a2c2f28e941f732c71ba90e27c24f62ee918bd4d9a8cfb3553f2f81e5493c7f0be94a11c1911b643a9108f231dd6f60df3fa9586b5d2e3e9e1fe