Client: InfoPanel Groups: Fix getting all members in one request. Add infinite loader

This commit is contained in:
Aleksandr Lushkin 2024-07-24 13:14:34 +02:00
parent 74d72b3b70
commit fdc06cf61c
7 changed files with 246 additions and 21 deletions

View File

@ -143,7 +143,7 @@ const EditGroupDialog = ({
if (groupParams.groupMembers) return;
setFetchMembersIsLoading(true);
getGroupById(group.id)!
getGroupById(group.id, true)!
.then((data: any) => {
prevGroupParams.current.groupMembers = data.members;
setInitialMembersIds(data.members.map((gm) => gm.id));

View File

@ -84,7 +84,7 @@ const GroupMember = ({
className="avatar"
role={groupMember.role || "user"}
size={"min"}
source={groupMember.avatarSmall}
source={groupMember.avatarSmall || groupMember.avatar}
/>
<div className="main-wrapper">

View File

@ -0,0 +1,155 @@
// (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, { useCallback, useEffect, useState } from "react";
import {
ListRowProps,
Index,
IndexRange,
InfiniteLoader,
List,
WindowScroller,
} from "react-virtualized";
import styled from "styled-components";
import { TUser } from "@docspace/shared/api/people/types";
import { RowLoader } from "@docspace/shared/skeletons/selector";
import GroupMember from "../GroupMember";
const ROW_HEIGHT = 50;
export const StyledList = styled(List)`
width: ${({ width }) => `${width - 40}px`} !important;
.group-member-row-loader {
padding: 0;
}
`;
interface GroupMembersListProps {
members: TUser[];
loadNextPage: (startIndex: number) => Promise<void>;
hasNextPage: boolean;
total: number;
}
export const GroupMembersList = ({
members,
loadNextPage,
hasNextPage,
total,
}: GroupMembersListProps) => {
const [scrollElement, setScrollElement] = useState<HTMLDivElement | null>(
null,
);
const [isNextPageLoading, setIsNextPageLoading] = useState(false);
const itemsCount = hasNextPage ? members.length + 1 : members.length;
const isItemLoaded = useCallback(
({ index }: Index) => {
return !hasNextPage || index < itemsCount;
},
[hasNextPage, itemsCount],
);
const loadMoreItems = useCallback(
async ({ startIndex }: IndexRange) => {
setIsNextPageLoading(true);
if (!isNextPageLoading) {
await loadNextPage(startIndex - 1);
}
setIsNextPageLoading(false);
},
[isNextPageLoading, loadNextPage],
);
const renderRow = ({ key, index, style }: ListRowProps) => {
const item = members[index];
return (
<div key={key} style={style}>
{item ? (
<GroupMember groupMember={item} />
) : (
<RowLoader
className="group-member-row-loader"
isMultiSelect={false}
isUser
count={1}
/>
)}
</div>
);
};
useEffect(() => {
const scrollEl = document.querySelector(".info-panel-scroll");
if (scrollEl) {
setScrollElement(scrollEl as HTMLDivElement);
}
}, []);
if (!scrollElement) {
return null;
}
return (
<InfiniteLoader
loadMoreRows={loadMoreItems}
isRowLoaded={isItemLoaded}
rowCount={total}
>
{({ onRowsRendered, registerChild }) => (
<WindowScroller scrollElement={scrollElement}>
{({ height, isScrolling, scrollTop }) => {
const scrollRect = scrollElement.getBoundingClientRect();
return (
<StyledList
autoHeight
height={height || scrollRect.height}
onRowsRendered={onRowsRendered}
ref={registerChild}
rowCount={itemsCount}
rowHeight={ROW_HEIGHT}
rowRenderer={renderRow}
width={scrollRect.width}
isScrolling={isScrolling}
overscanRowCount={3}
scrollTop={scrollTop}
// React virtualized sets "LTR" by default.
style={{ direction: "inherit" }}
/>
);
}}
</WindowScroller>
)}
</InfiniteLoader>
);
};

View File

@ -26,14 +26,20 @@
import { inject, observer } from "mobx-react";
import { withTranslation } from "react-i18next";
import * as Styled from "../../styles/groups.styled";
import withLoader from "@docspace/client/src/HOCs/withLoader";
import InfoPanelViewLoader from "@docspace/shared/skeletons/info-panel/body";
import GroupMember from "./GroupMember";
import useFetchGroup from "./useFetchGroup";
import { useParams } from "react-router-dom";
import { useState, useEffect } from "react";
import withLoader from "@docspace/client/src/HOCs/withLoader";
import InfoPanelViewLoader from "@docspace/shared/skeletons/info-panel/body";
import api from "@docspace/shared/api";
import AccountsFilter from "@docspace/shared/api/people/filter";
import { MIN_LOADER_TIMER } from "@docspace/shared/selectors/Files/FilesSelector.constants";
import GroupMember from "./GroupMember";
import * as Styled from "../../styles/groups.styled";
import useFetchGroup from "./useFetchGroup";
import { GroupMembersList } from "./GroupMembersList/GroupMembersList";
const Groups = ({
infoPanelSelection,
currentGroup,
@ -42,6 +48,9 @@ const Groups = ({
setInfoPanelSelectedGroup,
}) => {
const [isShowLoader, setIsShowLoader] = useState(false);
const [areMembersLoading, setAreMembersLoading] = useState(false);
const [groupMembers, setGroupMembers] = useState(null);
const [total, setTotal] = useState(0);
const { groupId: paramsGroupId } = useParams();
const isInsideGroup = !!paramsGroupId;
@ -51,32 +60,88 @@ const Groups = ({
const groupId = isInsideGroup ? paramsGroupId : infoPanelSelection?.id;
const setGroup = isInsideGroup ? setCurrentGroup : setInfoPanelSelectedGroup;
const groupManager = group?.manager;
const loadNextPage = async (startIndex) => {
const startLoadingTime = new Date();
try {
if (startIndex === 0) {
setAreMembersLoading(true);
}
const pageCount = 100;
const filter = AccountsFilter.getDefault();
filter.group = groupId;
filter.page = startIndex / pageCount;
filter.pageCount = pageCount;
const res = await api.people.getUserList(filter);
const membersWithoutManager = groupManager
? res.items.filter((item) => item.id !== groupManager.id)
: res.items;
setTotal(res.total);
if (startIndex === 0 || !groupMembers) {
setGroupMembers(membersWithoutManager);
} else {
setGroupMembers([...groupMembers, ...membersWithoutManager]);
}
} catch (e) {
console.log(e);
} finally {
const nowDate = new Date();
const diff = Math.abs(nowDate.getTime() - startLoadingTime.getTime());
if (diff < MIN_LOADER_TIMER) {
setTimeout(() => {
setAreMembersLoading(false);
}, MIN_LOADER_TIMER - diff);
} else {
setAreMembersLoading(false);
}
}
};
useFetchGroup(groupId, group?.id, setGroup);
useEffect(() => {
if (group) {
loadNextPage(0);
}
}, [group]);
useEffect(() => {
const showLoaderTimer = setTimeout(() => setIsShowLoader(true), 500);
return () => clearTimeout(showLoaderTimer);
}, []);
const groupManager = group?.manager;
const groupMembers = group?.members?.filter(
(user) => user.id !== groupManager?.id,
);
if (!group) {
if (isShowLoader) return <InfoPanelViewLoader view="groups" />;
if (isShowLoader)
return (
<Styled.GroupsContent>
<InfoPanelViewLoader view="groups" />
</Styled.GroupsContent>
);
return null;
}
const totalWithoutManager = groupManager ? total - 1 : total;
return (
<Styled.GroupsContent>
{groupManager && <GroupMember groupMember={groupManager} isManager />}
{!groupMembers ? (
{!groupMembers || areMembersLoading ? (
<InfoPanelViewLoader view="groups" />
) : (
groupMembers?.map((groupMember) => (
<GroupMember key={groupMember.id} groupMember={groupMember} />
))
<GroupMembersList
members={groupMembers}
hasNextPage={groupMembers.length < totalWithoutManager}
loadNextPage={loadNextPage}
total={totalWithoutManager}
managerId={groupManager?.id}
/>
)}
</Styled.GroupsContent>
);

View File

@ -45,7 +45,7 @@ const useFetchGroup = (groupId, fetchedGroupId, setGroup) => {
setIsLoading(true);
}
getGroupById(groupId, abortControllerRef.current?.signal)
getGroupById(groupId, false, abortControllerRef.current?.signal)
.then((data) => {
if (isMount.current) startTransition(() => setGroup(data));
})

View File

@ -69,12 +69,16 @@ export const getGroups = (filter = Filter.getDefault()) => {
});
};
export const getGroupById = (groupId: string, signal: AbortSignal) => {
export const getGroupById = (
groupId: string,
includeMembers: boolean = false,
signal?: AbortSignal,
) => {
return request({
method: "get",
url: `/group/${groupId}`,
url: `/group/${groupId}?includeMembers=${includeMembers}`,
signal,
});
}) as Promise<TGroup>;
};
export const getGroupsByName = async (

View File

@ -34,6 +34,7 @@ export type TGroup = {
name: string;
parent: string;
isGroup?: boolean;
members?: TUser[];
membersCount: number;
shared?: boolean;
isLDAP: boolean;