Merge branch 'release/v2.6.0' of https://github.com/ONLYOFFICE/DocSpace-client into release/v2.6.0

This commit is contained in:
Nikita Gopienko 2024-07-23 19:21:28 +03:00
commit 4d059d3067
9 changed files with 385 additions and 99 deletions

View File

@ -0,0 +1,38 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import styled from "styled-components";
import { ModalDialog } from "@docspace/shared/components/modal-dialog";
export const StyledModalDialog = styled(ModalDialog)`
.modal-body {
padding: 0;
}
.search-input {
margin: 16px 16px 12px;
}
`;

View File

@ -24,23 +24,32 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { inject, observer } from "mobx-react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import {
ModalDialog,
ModalDialogType,
} from "@docspace/shared/components/modal-dialog";
import { observer, inject } from "mobx-react";
import { useState, useEffect, useTransition } from "react";
import { getGroupMembersInRoom } from "@docspace/shared/api/groups";
import { useTranslation } from "react-i18next";
import { InputSize } from "@docspace/shared/components/text-input";
import { SearchInput } from "@docspace/shared/components/search-input";
import GroupMember from "./GroupMember";
import {
TGroup,
TGroupMemberInvitedInRoom,
} from "@docspace/shared/api/groups/types";
import EmptyContainer from "./EmptyContainer";
import GroupMembersList from "./sub-components/GroupMembersList/GroupMembersList";
import { StyledModalDialog } from "./EditGroupMembersDialog.styled";
import { ModalBodyLoader } from "./sub-components/ModalBodyLoader/ModalBodyLoader";
import { MIN_LOADER_TIMER } from "@docspace/shared/selectors/Files/FilesSelector.constants";
interface EditGroupMembersProps {
visible: boolean;
setVisible: (visible: boolean) => void;
group: any;
group: TGroup;
infoPanelSelection: any;
}
@ -53,33 +62,63 @@ const EditGroupMembers = ({
const { t } = useTranslation(["Common"]);
const [searchValue, setSearchValue] = useState<string>("");
const onChangeSearchValue = (newValue: string) => {
setSearchValue(newValue);
};
const onClearSearch = () => onChangeSearchValue("");
const [total, setTotal] = useState(0);
const [groupMembers, setGroupMembers] = useState<
TGroupMemberInvitedInRoom[] | null
>(null);
const [isSearchResultLoading, setIsSearchResultLoading] = useState(false);
const [isNextPageLoading, setIsNextPageLoading] = useState(false);
const [groupMembers, setGroupMembers] = useState<any[] | null>(null);
const filteredGroupMembers = groupMembers?.filter((groupMember) =>
groupMember.user.displayName.includes(searchValue),
);
const [, startTransition] = useTransition();
const onChangeSearchValue = (value: string) => {
setIsSearchResultLoading(true);
setSearchValue(value.trim());
};
const onClearSearch = () => onChangeSearchValue("");
const onClose = () => setVisible(false);
const isSearchListEmpty =
filteredGroupMembers && !filteredGroupMembers.length;
const hasMembers = filteredGroupMembers && filteredGroupMembers.length !== 0;
const loadNextPage = async (startIndex: number) => {
const startLoadingTime = new Date();
try {
setIsNextPageLoading(true);
const filter = { startIndex, count: 100, filterValue: searchValue };
const data = await getGroupMembersInRoom(
infoPanelSelection.id,
group.id,
filter,
);
setTotal(data.total);
if (startIndex === 0 || !groupMembers) {
setGroupMembers(data.items);
} else {
setGroupMembers([...groupMembers, ...data.items]);
}
} catch (e) {
console.error(e);
} finally {
const nowDate = new Date();
const diff = Math.abs(nowDate.getTime() - startLoadingTime.getTime());
if (diff < MIN_LOADER_TIMER) {
setTimeout(() => {
setIsSearchResultLoading(false);
}, MIN_LOADER_TIMER - diff);
} else {
setIsSearchResultLoading(false);
}
setIsNextPageLoading(false);
// setIsSearchResultLoading(false);
}
};
useEffect(() => {
const fetchGroup = async () => {
if (!group) return;
getGroupMembersInRoom(infoPanelSelection.id, group.id)!
.then((data: any) => startTransition(() => setGroupMembers(data.items)))
.catch((err: any) => console.error(err));
};
fetchGroup();
}, [group, infoPanelSelection.id]);
loadNextPage(0);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchValue]);
if (!infoPanelSelection?.isRoom) {
onClose();
@ -87,7 +126,7 @@ const EditGroupMembers = ({
}
return (
<ModalDialog
<StyledModalDialog
visible={visible}
onClose={onClose}
displayType={ModalDialogType.aside}
@ -95,29 +134,40 @@ const EditGroupMembers = ({
<ModalDialog.Header>{group.name}</ModalDialog.Header>
<ModalDialog.Body>
<SearchInput
className="search-input"
placeholder={t("PeopleTranslations:SearchByGroupMembers")}
value={searchValue}
onChange={onChangeSearchValue}
onClearSearch={onClearSearch}
size={InputSize.base}
/>
{!groupMembers ? (
<ModalBodyLoader withSearch />
) : (
<>
<SearchInput
className="search-input"
placeholder={t("PeopleTranslations:SearchByGroupMembers")}
value={searchValue}
onChange={onChangeSearchValue}
onClearSearch={onClearSearch}
size={InputSize.base}
/>
<div style={{ height: "12px", width: "100%" }} />
{isSearchListEmpty && <EmptyContainer />}
{hasMembers &&
filteredGroupMembers.map(({ user, ...rest }) => (
<GroupMember t={t} key={user.id} user={{ ...user, ...rest }} />
))}
{isSearchResultLoading ? (
<ModalBodyLoader withSearch={false} />
) : !groupMembers.length ? (
<EmptyContainer />
) : (
<GroupMembersList
members={groupMembers}
loadNextPage={loadNextPage}
hasNextPage={groupMembers.length < total}
total={total}
isNextPageLoading={isNextPageLoading}
/>
)}
</>
)}
</ModalDialog.Body>
</ModalDialog>
</StyledModalDialog>
);
};
export default inject(({ infoPanelStore, userStore, dialogsStore }) => ({
export default inject(({ infoPanelStore, userStore, dialogsStore }: any) => ({
infoPanelSelection: infoPanelStore.infoPanelSelection,
selfId: userStore.user.id,
group: dialogsStore.editMembersGroup,

View File

@ -31,7 +31,7 @@ export const GroupMember = styled.div<{ isExpect: boolean }>`
display: flex;
align-items: center;
gap: 8px;
padding: 8px 0;
padding: 8px 16px;
.avatar {
min-width: 32px;

View File

@ -24,39 +24,47 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { useState } from "react";
import { inject, observer } from "mobx-react";
import { useTranslation } from "react-i18next";
import { isMobile, isMobileOnly } from "react-device-detect";
import AtReactSvgUrl from "PUBLIC_DIR/images/@.react.svg?url";
// import { StyledUser } from "../../styles/members";
import { Avatar } from "@docspace/shared/components/avatar";
import { ComboBox } from "@docspace/shared/components/combobox";
import DefaultUserPhotoUrl from "PUBLIC_DIR/images/default_user_photo_size_82-82.png";
import { isMobileOnly, isMobile } from "react-device-detect";
import { decode } from "he";
import { filterUserRoleOptions } from "SRC_DIR/helpers";
import { Text } from "@docspace/shared/components/text";
import * as Styled from "./index.styled";
import { getUserRoleOptionsByUserAccess } from "@docspace/shared/utils/room-members/getUserRoleOptionsByUserAccess";
import { getUserRoleOptionsByRoomType } from "@docspace/shared/utils/room-members/getUserRoleOptionsByRoomType";
import { updateRoomMemberRole } from "@docspace/shared/api/rooms";
import { toastr } from "@docspace/shared/components/toast";
import { useState } from "react";
import { HelpButton } from "@docspace/shared/components/help-button";
import { getUserRoleOptions } from "@docspace/shared/utils/room-members/getUserRoleOptions";
import { ShareAccessRights } from "@docspace/shared/enums";
import { getUserRole, getUserTypeLabel } from "@docspace/shared/utils/common";
import { getUserRole } from "@docspace/shared/utils/common";
import { TGroupMemberInvitedInRoom } from "@docspace/shared/api/groups/types";
import * as Styled from "./index.styled";
interface GroupMemberProps {
t: any;
user: any;
member: TGroupMemberInvitedInRoom;
infoPanelSelection: any;
}
const GroupMember = ({ t, user, infoPanelSelection }: GroupMemberProps) => {
const GroupMember = ({ member, infoPanelSelection }: GroupMemberProps) => {
const { user } = member;
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation("Common");
const userRole = user.isOwner
? getUserRoleOptions(t).portalAdmin
: getUserRoleOptionsByUserAccess(t, user.userAccess || user.groupAccess);
: getUserRoleOptionsByUserAccess(
t,
member.userAccess || member.groupAccess,
);
const fullRoomRoleOptions = getUserRoleOptionsByRoomType(
t,
@ -86,7 +94,7 @@ const GroupMember = ({ t, user, infoPanelSelection }: GroupMemberProps) => {
else
selectedUserRoleCBOption = getUserRoleOptionsByUserAccess(
t,
user.userAccess || user.groupAccess,
member.userAccess || member.groupAccess,
);
const availableUserRoleCBOptions = filterUserRoleOptions(
@ -101,7 +109,7 @@ const GroupMember = ({ t, user, infoPanelSelection }: GroupMemberProps) => {
notify: false,
sharingMessage: "",
})
.then(() => (user.userAccess = userRoleOption.access))
.then(() => (member.userAccess = userRoleOption.access))
.catch((err) => toastr.error(err))
.finally(() => setIsLoading(false));
};
@ -134,8 +142,8 @@ const GroupMember = ({ t, user, infoPanelSelection }: GroupMemberProps) => {
</div>
<div className="individual-rights-tooltip">
{user.userAccess &&
user.userAccess !== user.groupAccess &&
{member.userAccess &&
member.userAccess !== member.groupAccess &&
!user.isOwner && (
<HelpButton
place="left"
@ -152,7 +160,7 @@ const GroupMember = ({ t, user, infoPanelSelection }: GroupMemberProps) => {
{userRole && userRoleOptions && (
<div className="role-wrapper">
{user.canEditAccess && !user.isOwner ? (
{member.canEditAccess && !user.isOwner ? (
<ComboBox
className="role-combobox"
selectedOption={userRole}
@ -180,6 +188,6 @@ const GroupMember = ({ t, user, infoPanelSelection }: GroupMemberProps) => {
);
};
export default inject(({ infoPanelStore }) => ({
export default inject(({ infoPanelStore }: any) => ({
infoPanelSelection: infoPanelStore.infoPanelSelection,
}))(observer(GroupMember));

View File

@ -0,0 +1,126 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React, { memo, useCallback } from "react";
import { useTheme } from "styled-components";
import { areEqual, FixedSizeList as List } from "react-window";
import AutoSizer from "react-virtualized-auto-sizer";
import InfiniteLoader from "react-window-infinite-loader";
import { Scrollbar } from "@docspace/shared/components/scrollbar";
import { RowLoader } from "@docspace/shared/skeletons/selector";
import { TGroupMemberInvitedInRoom } from "@docspace/shared/api/groups/types";
import GroupMember from "../GroupMember";
const ROW_HEIGHT = 48;
const SEARCH_WITH_PADDING_HEIGHT = 60;
const Row = memo(
({
data,
index,
style,
}: {
data: TGroupMemberInvitedInRoom[];
index: number;
style: React.CSSProperties;
}) => {
const member = data[index];
return (
<div style={style}>
{member ? (
<GroupMember member={member} />
) : (
<RowLoader isMultiSelect={false} isContainer isUser />
)}
</div>
);
},
areEqual,
);
Row.displayName = "Row";
interface GroupMembersListProps {
members: TGroupMemberInvitedInRoom[];
loadNextPage: (startIndex: number) => void;
hasNextPage: boolean;
total: number;
isNextPageLoading: boolean;
}
const GroupMembersList = ({
members,
loadNextPage,
hasNextPage,
total,
isNextPageLoading,
}: GroupMembersListProps) => {
const { interfaceDirection } = useTheme();
const itemCount = hasNextPage ? members.length + 1 : members.length;
const isItemLoaded = useCallback(
(index: number) => {
return !hasNextPage || index < itemCount - 1;
},
[hasNextPage, itemCount],
);
const loadMoreItems = isNextPageLoading ? () => {} : loadNextPage;
return (
<AutoSizer>
{({ height, width }) => (
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={total}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<List
ref={ref}
direction={interfaceDirection}
height={height - SEARCH_WITH_PADDING_HEIGHT}
width={width}
itemCount={itemCount}
itemSize={ROW_HEIGHT}
itemData={members}
outerElementType={Scrollbar}
onItemsRendered={onItemsRendered}
>
{Row}
</List>
)}
</InfiniteLoader>
)}
</AutoSizer>
);
};
export default GroupMembersList;

View File

@ -0,0 +1,44 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { SearchLoader } from "@docspace/shared/skeletons/selector";
import React from "react";
import { MemberLoader } from "@docspace/shared/skeletons/info-panel/body/views/MembersLoader";
interface ModalBodyLoaderProps {
withSearch: boolean;
}
export const ModalBodyLoader = ({ withSearch }: ModalBodyLoaderProps) => {
return (
<div style={{ paddingTop: withSearch ? "16px" : "0" }}>
{withSearch && <SearchLoader />}
<div style={{ paddingInline: "16px" }}>
<MemberLoader count={25} />
</div>
</div>
);
};

View File

@ -27,8 +27,12 @@
import Filter from "./filter";
import { request } from "../client";
import { checkFilterInstance } from "../../utils/common";
import { TGroup } from "./types";
import { checkFilterInstance, toUrlParams } from "../../utils/common";
import {
TGetGroupMembersInRoom,
TGetGroupMembersInRoomFilter,
TGroup,
} from "./types";
// * Create
@ -104,11 +108,14 @@ export const getGroupsByUserId = (userId: string) => {
export const getGroupMembersInRoom = (
folderId: string | number,
groupId: string,
filter: TGetGroupMembersInRoomFilter,
) => {
const url = `/files/folder/${folderId}/group/${groupId}/share?${toUrlParams(filter, false)}`;
return request({
method: "get",
url: `/files/folder/${folderId}/group/${groupId}/share`,
});
url,
}) as Promise<TGetGroupMembersInRoom>;
};
// * Update

View File

@ -25,6 +25,7 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { TUser } from "../people/types";
import { ShareAccessRights } from "../../enums";
export type TGroup = {
category: string;
@ -37,3 +38,23 @@ export type TGroup = {
shared?: boolean;
isLDAP: boolean;
};
export type TGroupMemberInvitedInRoom = {
user: TUser;
canEditAccess: boolean;
overridden: boolean;
owner: boolean;
groupAccess: number;
userAccess: ShareAccessRights;
};
export type TGetGroupMembersInRoom = {
items: TGroupMemberInvitedInRoom[];
total: number;
};
export type TGetGroupMembersInRoomFilter = {
startIndex?: number;
count?: number;
filterValue?: string;
};

View File

@ -33,6 +33,30 @@ import {
StyledMembersLoader,
} from "../body.styled";
export const MemberLoader = ({ count = 1 }: { count?: number }) => {
return (
<>
{[...Array(count).keys()].map((i) => (
<StyledMemberLoader key={i}>
<RectangleSkeleton
className="avatar"
width="32px"
height="32px"
borderRadius="50%"
/>
<RectangleSkeleton width="212px" height="16px" borderRadius="3px" />
<RectangleSkeleton
className="role-selector"
width="64px"
height="20px"
borderRadius="3px"
/>
</StyledMemberLoader>
))}
</>
);
};
const MembersLoader = () => {
return (
<StyledMembersLoader>
@ -41,46 +65,14 @@ const MembersLoader = () => {
<RectangleSkeleton width="16px" height="16px" borderRadius="3px" />
</StyledMemberSubtitleLoader>
{[...Array(4).keys()].map((i) => (
<StyledMemberLoader key={i}>
<RectangleSkeleton
className="avatar"
width="32px"
height="32px"
borderRadius="50%"
/>
<RectangleSkeleton width="212px" height="16px" borderRadius="3px" />
<RectangleSkeleton
className="role-selector"
width="64px"
height="20px"
borderRadius="3px"
/>
</StyledMemberLoader>
))}
<MemberLoader count={4} />
<StyledMemberSubtitleLoader className="pending_users">
<RectangleSkeleton width="111px" height="16px" borderRadius="3px" />
<RectangleSkeleton width="16px" height="16px" borderRadius="3px" />
</StyledMemberSubtitleLoader>
{[...Array(4).keys()].map((i) => (
<StyledMemberLoader key={i}>
<RectangleSkeleton
className="avatar"
width="32px"
height="32px"
borderRadius="50%"
/>
<RectangleSkeleton width="212px" height="16px" borderRadius="3px" />
<RectangleSkeleton
className="role-selector"
width="64px"
height="20px"
borderRadius="3px"
/>
</StyledMemberLoader>
))}
<MemberLoader count={4} />
</StyledMembersLoader>
);
};