Client: InfoPanel Groups: Fix getting all members in one request. Add infinite loader
This commit is contained in:
parent
74d72b3b70
commit
fdc06cf61c
@ -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));
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
|
@ -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));
|
||||
})
|
||||
|
@ -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 (
|
||||
|
@ -34,6 +34,7 @@ export type TGroup = {
|
||||
name: string;
|
||||
parent: string;
|
||||
isGroup?: boolean;
|
||||
members?: TUser[];
|
||||
membersCount: number;
|
||||
shared?: boolean;
|
||||
isLDAP: boolean;
|
||||
|
Loading…
Reference in New Issue
Block a user