Merge pull request #580 from ONLYOFFICE/bugfix/huge-group-issues

Bugfix/huge group issues
This commit is contained in:
Alexey Safronov 2024-08-12 18:29:37 +04:00 committed by GitHub
commit 7f2b4c9686
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 1258 additions and 389 deletions

View File

@ -34,3 +34,35 @@ export const StyledModal = styled(ModalDialog)`
padding-top: 20px;
}
`;
export const StyledBodyLoader = styled.div`
box-sizing: border-box;
display: flex;
flex-direction: column;
.title-section {
margin-bottom: 14px;
}
.group-title {
margin-bottom: 4px;
height: 16px;
padding-top: 4px;
}
.manager-title,
.members-title {
padding-block: 8px;
}
.add-member-container {
display: flex;
align-items: center;
gap: 8px;
margin-block: 8px;
}
.member-row {
padding-inline: 0;
}
`;

View File

@ -25,24 +25,28 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { useState, ChangeEvent } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { observer, inject } from "mobx-react";
import {
ModalDialog,
ModalDialogType,
} from "@docspace/shared/components/modal-dialog";
import { Button, ButtonSize } from "@docspace/shared/components/button";
import { toastr } from "@docspace/shared/components/toast";
import { observer, inject } from "mobx-react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { createGroup } from "@docspace/shared/api/groups";
import { TUser } from "@docspace/shared/api/people/types";
import PeopleStore from "SRC_DIR/store/PeopleStore";
import GroupsStore from "SRC_DIR/store/GroupsStore";
import { StyledModal } from "./CreateEditGroupDialog.styled";
import { GroupParams } from "./types";
import GroupNameParam from "./sub-components/GroupNameParam";
import HeadOfGroup from "./sub-components/HeadOfGroupParam";
import MembersParam from "./sub-components/MembersParam";
import SelectGroupManagerPanel from "./sub-components/HeadOfGroupParam/SelectGroupManagerPanel";
import SelectGroupMembersPanel from "./sub-components/MembersParam/SelectGroupMembersPanel";
import { SelectMembersPanel } from "./sub-components/create-components/SelectMembersPanel";
interface CreateGroupDialogProps {
visible: boolean;
@ -69,10 +73,10 @@ const CreateGroupDialog = ({
const onChangeGroupName = (e: ChangeEvent<HTMLInputElement>) =>
setGroupParams({ ...groupParams, groupName: e.target.value });
const setGroupManager = (groupManager: object | null) =>
const setGroupManager = (groupManager: TUser | null) =>
setGroupParams({ ...groupParams, groupManager });
const setGroupMembers = (groupMembers: object[]) =>
const setGroupMembers = (groupMembers: TUser[]) =>
setGroupParams((prevState) => ({ ...prevState, groupMembers }));
const [selectGroupMangerPanelIsVisible, setSelectGroupMangerPanelIsVisible] =
@ -89,6 +93,44 @@ const CreateGroupDialog = ({
const onShowSelectMembersPanel = () => setSelectMembersPanelIsVisible(true);
const onHideSelectMembersPanel = () => setSelectMembersPanelIsVisible(false);
const removeManager = () => {
setGroupManager(null);
setGroupMembers(
groupParams.groupMembers?.filter(
(gm) => gm.id !== groupParams.groupManager!.id,
) || [],
);
};
const addMembers = (newGroupMembers: TUser[]) => {
const resultGroupMembers: TUser[] = [...groupParams.groupMembers];
let showErrorWasSelected = false;
newGroupMembers.forEach((groupMember) => {
if (
groupParams.groupMembers.findIndex((gm) => gm.id === groupMember.id) !==
-1
) {
showErrorWasSelected = true;
return;
}
resultGroupMembers.push(groupMember);
});
if (showErrorWasSelected) {
toastr.warning("Some users have already been added");
}
setGroupMembers(resultGroupMembers);
};
const removeMember = (member: TUser) => {
const newGroupMembers = groupParams.groupMembers?.filter(
(gm) => gm.id !== member.id,
);
setGroupMembers(newGroupMembers || []);
};
const onCreateGroup = async () => {
setIsLoading(true);
@ -127,15 +169,13 @@ const CreateGroupDialog = ({
/>
<HeadOfGroup
groupManager={groupParams.groupManager}
setGroupManager={setGroupManager}
groupMembers={groupParams.groupMembers}
setGroupMembers={setGroupMembers}
removeManager={removeManager}
onShowSelectGroupManagerPanel={onShowSelectGroupManagerPanel}
/>
<MembersParam
groupManager={groupParams.groupManager}
groupMembers={groupParams.groupMembers}
setGroupMembers={setGroupMembers}
removeMember={removeMember}
onShowSelectMembersPanel={onShowSelectMembersPanel}
/>
</ModalDialog.Body>
@ -177,19 +217,23 @@ const CreateGroupDialog = ({
)}
{selectMembersPanelIsVisible && (
<SelectGroupMembersPanel
<SelectMembersPanel
isVisible={selectMembersPanelIsVisible}
onClose={onHideSelectMembersPanel}
onParentPanelClose={onClose}
groupManager={groupParams.groupManager}
groupMembers={groupParams.groupMembers}
setGroupMembers={setGroupMembers}
addMembers={addMembers}
/>
)}
</>
);
};
export default inject(({ peopleStore }) => ({
getGroups: peopleStore.groupsStore.getGroups,
}))(observer(CreateGroupDialog));
export default inject<{ peopleStore: PeopleStore }>(({ peopleStore }) => {
const { getGroups } = peopleStore.groupsStore! as GroupsStore;
return {
getGroups,
};
})(observer(CreateGroupDialog));

View File

@ -25,171 +25,200 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { ChangeEvent, useEffect, useRef, useState } from "react";
import { ModalDialog } from "@docspace/shared/components/modal-dialog";
import { Button } from "@docspace/shared/components/button";
import { inject, observer } from "mobx-react";
import { useTranslation } from "react-i18next";
import { getGroupById } from "@docspace/shared/api/groups";
import { compareGroupParams } from "./utils";
import { EditGroupParams } from "./types";
import { Button, ButtonSize } from "@docspace/shared/components/button";
import {
ModalDialog,
ModalDialogType,
} from "@docspace/shared/components/modal-dialog";
import { TGroup } from "@docspace/shared/api/groups/types";
import {
MIN_LOADER_TIMER,
SHOW_LOADER_TIMER,
} from "@docspace/shared/selectors/Files/FilesSelector.constants";
import EditGroupStore from "SRC_DIR/store/EditGroupStore";
import { StyledModal } from "./CreateEditGroupDialog.styled";
import GroupNameParam from "./sub-components/GroupNameParam";
import HeadOfGroup from "./sub-components/HeadOfGroupParam";
import MembersParam from "./sub-components/MembersParam";
import SelectGroupManagerPanel from "./sub-components/HeadOfGroupParam/SelectGroupManagerPanel";
import SelectGroupMembersPanel from "./sub-components/MembersParam/SelectGroupMembersPanel";
import { SelectMembersPanel } from "./sub-components/edit-components/SelectMembersPanel";
import { BodyLoader } from "./sub-components/BodyLoader/BodyLoader";
interface EditGroupDialogProps {
group: {
members: object[];
[key: string]: any;
};
type InjectedProps = Pick<
EditGroupStore,
| "initGroupData"
| "resetGroupData"
| "isInit"
| "loadMembers"
| "manager"
| "addManager"
| "removeManager"
| "members"
| "addMembers"
| "removeMember"
| "currentTotal"
| "submitChanges"
| "title"
| "setTitle"
| "hasChanges"
>;
type EditGroupDialogProps = {
group: TGroup;
visible: boolean;
onClose: () => void;
updateGroup: (
groupId: string,
groupName: string,
groupManager: string,
membersToAdd: string[],
membersToRemove: string[],
) => Promise<void>;
}
injectedProps?: InjectedProps;
};
const EditGroupDialog = ({
group,
visible,
onClose,
updateGroup,
setInfoPanelSelectedGroup,
injectedProps,
}: EditGroupDialogProps) => {
const {
initGroupData,
resetGroupData,
isInit,
loadMembers,
manager,
addManager,
removeManager,
members,
addMembers,
removeMember,
currentTotal,
submitChanges,
title,
setTitle,
hasChanges,
} = injectedProps!;
const { t } = useTranslation(["PeopleTranslations", "Common"]);
const [initialMembersIds, setInitialMembersIds] = useState<string[]>([]);
const [isCreateGroupLoading, setCreateGroupIsLoading] =
useState<boolean>(false);
const [isFetchMembersLoading, setFetchMembersIsLoading] =
useState<boolean>(false);
const [groupParams, setGroupParams] = useState<EditGroupParams>({
groupName: group.name,
groupManager: group.manager,
groupMembers: null,
});
const prevGroupParams = useRef({ ...groupParams });
const onChangeGroupName = (e: ChangeEvent<HTMLInputElement>) =>
setGroupParams((prev) => ({ ...prev, groupName: e.target.value }));
const setGroupManager = (groupManager: object | null) =>
setGroupParams((prev) => ({ ...prev, groupManager }));
const setGroupMembers = (groupMembers: object[]) =>
setGroupParams((prev) => ({ ...prev, groupMembers }));
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [selectGroupMangerPanelIsVisible, setSelectGroupMangerPanelIsVisible] =
useState<boolean>(false);
const [selectMembersPanelIsVisible, setSelectMembersPanelIsVisible] =
useState<boolean>(false);
const [showLoader, setShowLoader] = useState(false);
const loaderTimeout = useRef<NodeJS.Timeout | null>(null);
const startLoaderTime = useRef<Date | null>(null);
const onChangeGroupName = (e: ChangeEvent<HTMLInputElement>) => {
setTitle(e.target.value);
};
const closeModal = () => {
resetGroupData();
onClose();
};
const onShowSelectGroupManagerPanel = () =>
setSelectGroupMangerPanelIsVisible(true);
const onHideSelectGroupManagerPanel = () =>
setSelectGroupMangerPanelIsVisible(false);
const [selectMembersPanelIsVisible, setSelectMembersPanelIsVisible] =
useState<boolean>(false);
const onShowSelectMembersPanel = () => setSelectMembersPanelIsVisible(true);
const onHideSelectMembersPanel = () => setSelectMembersPanelIsVisible(false);
const onEditGroup = async () => {
setCreateGroupIsLoading(true);
setIsSubmitting(true);
const groupManagerId = groupParams.groupManager?.id || undefined;
await submitChanges();
const newMembersIds =
groupParams.groupMembers?.map((gm: any) => gm.id) || [];
const membersToAdd = newMembersIds.filter(
(gm) => !initialMembersIds.includes(gm),
);
const membersToDelete = initialMembersIds.filter(
(gm) => !newMembersIds.includes(gm),
);
await updateGroup(
group.id,
groupParams.groupName,
groupManagerId,
membersToAdd,
membersToDelete,
);
setCreateGroupIsLoading(false);
onClose();
setIsSubmitting(false);
closeModal();
};
const notEnoughGroupParamsToEdit =
!groupParams.groupName ||
(!groupParams.groupManager && !groupParams.groupMembers?.length);
useEffect(() => {
initGroupData(group);
const groupParamsNotChanged = compareGroupParams(
groupParams,
prevGroupParams.current,
);
return () => {
resetGroupData();
};
}, []);
useEffect(() => {
if (groupParams.groupMembers) return;
setFetchMembersIsLoading(true);
if (!isInit) {
loaderTimeout.current = setTimeout(() => {
startLoaderTime.current = new Date();
setShowLoader(true);
}, SHOW_LOADER_TIMER);
} else if (startLoaderTime.current) {
const currentDate = new Date();
getGroupById(group.id)!
.then((data: any) => {
prevGroupParams.current.groupMembers = data.members;
setInitialMembersIds(data.members.map((gm) => gm.id));
setGroupMembers(data.members);
})
.then((data) => {
setInfoPanelSelectedGroup(data);
})
.catch((err) => console.error(err))
.finally(() => setFetchMembersIsLoading(false));
}, [group.id]);
const ms = Math.abs(
startLoaderTime.current.getTime() - currentDate.getTime(),
);
if (ms >= MIN_LOADER_TIMER) {
startLoaderTime.current = null;
return setShowLoader(false);
}
setTimeout(() => {
if (isInit) {
startLoaderTime.current = null;
setShowLoader(false);
}
}, MIN_LOADER_TIMER - ms);
loaderTimeout.current = null;
} else if (loaderTimeout.current) {
clearTimeout(loaderTimeout.current);
loaderTimeout.current = null;
}
}, [isInit]);
const notEnoughParamsToEdit = !title || (!manager && !members?.length);
return (
<>
<StyledModal
displayType="aside"
displayType={ModalDialogType.aside}
withBodyScroll
visible={visible}
onClose={onClose}
onClose={closeModal}
withFooterBorder
// isScrollLocked={isScrollLocked}
// isOauthWindowOpen={isOauthWindowOpen}
>
<ModalDialog.Header>
{t("PeopleTranslations:EditGroup")}
</ModalDialog.Header>
<ModalDialog.Body>
<GroupNameParam
groupName={groupParams.groupName}
onChangeGroupName={onChangeGroupName}
/>
<HeadOfGroup
groupManager={groupParams.groupManager}
setGroupManager={setGroupManager}
groupMembers={groupParams.groupMembers}
setGroupMembers={setGroupMembers}
onShowSelectGroupManagerPanel={onShowSelectGroupManagerPanel}
/>
{!isFetchMembersLoading && (
<MembersParam
groupManager={groupParams.groupManager}
groupMembers={groupParams.groupMembers}
setGroupMembers={setGroupMembers}
onShowSelectMembersPanel={onShowSelectMembersPanel}
/>
{showLoader ? (
<BodyLoader />
) : (
isInit && (
<>
<GroupNameParam
groupName={title}
onChangeGroupName={onChangeGroupName}
/>
<HeadOfGroup
groupManager={manager}
onShowSelectGroupManagerPanel={onShowSelectGroupManagerPanel}
removeManager={removeManager}
/>
<MembersParam
groupManager={manager}
groupMembers={members}
removeMember={removeMember}
onShowSelectMembersPanel={onShowSelectMembersPanel}
withInfiniteLoader
total={currentTotal}
loadNextPage={loadMembers}
hasNextPage={!!members && members.length < currentTotal}
/>
</>
)
)}
</ModalDialog.Body>
@ -198,21 +227,21 @@ const EditGroupDialog = ({
id="edit-group-modal_submit"
tabIndex={5}
label={t("Common:SaveButton")}
size="normal"
size={ButtonSize.normal}
primary
scale
onClick={onEditGroup}
isDisabled={notEnoughGroupParamsToEdit || groupParamsNotChanged}
isLoading={isCreateGroupLoading}
isDisabled={!hasChanges || notEnoughParamsToEdit}
isLoading={isSubmitting}
/>
<Button
id="edit-group-modal_cancel"
tabIndex={5}
label={t("Common:CancelButton")}
size="normal"
size={ButtonSize.normal}
scale
isDisabled={isCreateGroupLoading}
onClick={onClose}
isDisabled={isSubmitting}
onClick={closeModal}
/>
</ModalDialog.Footer>
</StyledModal>
@ -222,25 +251,60 @@ const EditGroupDialog = ({
isVisible={selectGroupMangerPanelIsVisible}
onClose={onHideSelectGroupManagerPanel}
onParentPanelClose={onClose}
setGroupManager={setGroupManager}
setGroupManager={addManager}
/>
)}
{selectMembersPanelIsVisible && (
<SelectGroupMembersPanel
<SelectMembersPanel
isVisible={selectMembersPanelIsVisible}
onClose={onHideSelectMembersPanel}
onParentPanelClose={onClose}
groupManager={groupParams.groupManager}
groupMembers={groupParams.groupMembers}
setGroupMembers={setGroupMembers}
addMembers={addMembers}
/>
)}
</>
);
};
export default inject(({ peopleStore, infoPanelStore }) => ({
updateGroup: peopleStore.groupsStore.updateGroup,
setInfoPanelSelectedGroup: infoPanelStore.setInfoPanelSelectedGroup,
}))(observer(EditGroupDialog));
export default inject<{ editGroupStore: EditGroupStore }>(
({ editGroupStore }) => {
const {
initGroupData,
resetGroupData,
isInit,
loadMembers,
manager,
addManager,
removeManager,
members,
addMembers,
removeMember,
currentTotal,
submitChanges,
title,
setTitle,
hasChanges,
} = editGroupStore;
return {
injectedProps: {
initGroupData,
resetGroupData,
isInit,
loadMembers,
manager,
addManager,
removeManager,
members,
addMembers,
removeMember,
currentTotal,
submitChanges,
title,
setTitle,
hasChanges,
},
};
},
)(observer(EditGroupDialog));

View File

@ -0,0 +1,43 @@
import React from "react";
import { RectangleSkeleton } from "@docspace/shared/skeletons";
import { RowLoader } from "@docspace/shared/skeletons/selector";
import { StyledBodyLoader } from "../../CreateEditGroupDialog.styled";
export const BodyLoader = () => {
return (
<StyledBodyLoader>
<div className="title-section">
<RectangleSkeleton className="group-title" width="50px" />
<RectangleSkeleton height="32px" />
</div>
<div className="manager-section">
<RectangleSkeleton
className="manager-title"
height="16px"
width="100px"
/>
<RowLoader className="member-row" isMultiSelect isUser count={1} />
</div>
<div className="members-section">
<RectangleSkeleton
className="members-title"
height="16px"
width="100px"
/>
<div className="add-member-container">
<RectangleSkeleton height="32px" width="32px" />
<RectangleSkeleton height="14px" width="100px" />
</div>
<RowLoader
className="member-row"
isContainer
isMultiSelect
isUser
count={10}
/>
</div>
</StyledBodyLoader>
);
};

View File

@ -1,74 +0,0 @@
// (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 { GroupParams } from "../types";
import GroupNameParam from "./GroupNameParam";
import { Dispatch, ChangeEvent } from "react";
import HeadOfGroup from "./HeadOfGroupParam";
import MembersParam from "./MembersParam";
interface CreateGroupDialogBodyProps {
groupParams: GroupParams;
setGroupParams: Dispatch<React.SetStateAction<GroupParams>>;
onClose: () => void;
}
const CreateGroupDialogBody = ({
groupParams,
setGroupParams,
onClose,
}: CreateGroupDialogBodyProps) => {
const onChangeGroupName = (e: ChangeEvent<HTMLInputElement>) =>
setGroupParams({ ...groupParams, groupName: e.target.value });
const setGroupManager = (groupManager: object) =>
setGroupParams({ ...groupParams, groupManager });
const setGroupMembers = (groupMembers: object[]) =>
setGroupParams({ ...groupParams, groupMembers });
return (
<>
<GroupNameParam
groupName={groupParams.groupName}
onChangeGroupName={onChangeGroupName}
/>
<HeadOfGroup
groupManager={groupParams.groupManager}
setGroupManager={setGroupManager}
onClose={onClose}
/>
<MembersParam
groupManager={groupParams.groupManager}
groupMembers={groupParams.groupMembers}
setGroupMembers={setGroupMembers}
onClose={onClose}
/>
</>
);
};
export default CreateGroupDialogBody;

View File

@ -24,11 +24,10 @@
// 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 { memo } from "react";
import { ReactSVG } from "react-svg";
import { useTranslation } from "react-i18next";
import RemoveReactSvgUrl from "PUBLIC_DIR/images/remove.react.svg?url";
import {
Avatar,
AvatarRole,
@ -36,18 +35,16 @@ import {
} from "@docspace/shared/components/avatar";
import { getUserRole, getUserTypeLabel } from "@docspace/shared/utils/common";
import { TUser } from "@docspace/shared/api/people/types";
import RemoveReactSvgUrl from "PUBLIC_DIR/images/remove.react.svg?url";
import * as Styled from "./index.styled";
interface GroupMemberRowProps {
groupMember: TUser;
onClickRemove: () => void;
removeMember: (member: TUser) => void;
}
const GroupMemberRow = ({
groupMember,
onClickRemove,
}: GroupMemberRowProps) => {
const GroupMemberRow = ({ groupMember, removeMember }: GroupMemberRowProps) => {
const { t } = useTranslation(["Common"]);
const role = getUserRole(groupMember);
@ -68,6 +65,10 @@ const GroupMemberRow = ({
default:
}
const onRemove = () => {
removeMember(groupMember);
};
return (
<Styled.GroupMemberRow>
<Avatar
@ -83,10 +84,10 @@ const GroupMemberRow = ({
<ReactSVG
className="remove-icon"
src={RemoveReactSvgUrl}
onClick={onClickRemove}
onClick={onRemove}
/>
</Styled.GroupMemberRow>
);
};
export default GroupMemberRow;
export default memo(GroupMemberRow);

View File

@ -0,0 +1,152 @@
// (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 {
Index,
IndexRange,
InfiniteLoader,
List,
ListRowProps,
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 GroupMemberRow from "SRC_DIR/components/dialogs/CreateEditGroupDialog/sub-components/GroupMemberRow";
const ROW_HEIGHT = 50;
export const StyledList = styled(List)`
width: ${({ width }) => `${width - 16}px`} !important;
.group-member-row-loader {
padding: 0;
}
`;
interface GroupMembersListProps {
members: TUser[];
removeMember: (member: TUser) => void;
loadNextPage: (startIndex: number) => Promise<void>;
hasNextPage: boolean;
total: number;
}
export const GroupMembersList = (props: GroupMembersListProps) => {
const { members, removeMember, loadNextPage, hasNextPage, total } = props;
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 = isNextPageLoading
? async () => {}
: async ({ startIndex }: IndexRange) => {
setIsNextPageLoading(true);
await loadNextPage(startIndex - 1);
setIsNextPageLoading(false);
};
const renderRow = ({ key, index, style }: ListRowProps) => {
const item = members[index];
return (
<div key={key} style={style}>
{item ? (
<GroupMemberRow groupMember={item} removeMember={removeMember} />
) : (
<RowLoader
className="group-member-row-loader"
isMultiSelect={false}
isUser
count={1}
/>
)}
</div>
);
};
useEffect(() => {
const scrollEl = document.querySelector(
"#modal-scroll > .scroll-wrapper > .scroller",
);
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 scrollBodyRect =
scrollElement.children[0].getBoundingClientRect();
return (
<StyledList
autoHeight
height={height || scrollBodyRect.height}
onRowsRendered={onRowsRendered}
ref={registerChild}
rowCount={itemsCount}
rowHeight={ROW_HEIGHT}
rowRenderer={renderRow}
width={scrollBodyRect.width}
isScrolling={isScrolling}
overscanRowCount={3}
scrollTop={scrollTop}
// React virtualized sets "LTR" by default.
style={{ direction: "inherit" }}
/>
);
}}
</WindowScroller>
)}
</InfiniteLoader>
);
};

View File

@ -25,8 +25,11 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { useTranslation } from "react-i18next";
import { ShareAccessRights } from "@docspace/shared/enums";
import { Portal } from "@docspace/shared/components/portal";
import { TUser } from "@docspace/shared/api/people/types";
import AddUsersPanel from "../../../../panels/AddUsersPanel";
import { getAccessOptions } from "../../../../panels/InvitePanel/utils";
@ -34,7 +37,7 @@ interface SelectGroupManagerPanelProps {
isVisible: boolean;
onClose: () => void;
onParentPanelClose: () => void;
setGroupManager: (groupManager: object) => void;
setGroupManager: (groupManager: TUser) => void;
}
const SelectGroupManagerPanel = ({
@ -46,7 +49,7 @@ const SelectGroupManagerPanel = ({
const { t } = useTranslation(["InviteDialog"]);
const accessOptions = getAccessOptions(t);
const onSelectGroupManager = (newGroupManager: object[]) => {
const onSelectGroupManager = (newGroupManager: TUser[]) => {
setGroupManager(newGroupManager[0]);
};

View File

@ -24,37 +24,28 @@
// 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 { useTranslation } from "react-i18next";
import { SelectorAddButton } from "@docspace/shared/components/selector-add-button";
import * as Styled from "./index.styled";
import { TUser } from "@docspace/shared/api/people/types";
import PlusSvgUrl from "PUBLIC_DIR/images/icons/16/button.plus.react.svg?url";
import SelectGroupManagerPanel from "./SelectGroupManagerPanel";
import * as Styled from "./index.styled";
import GroupMemberRow from "../GroupMemberRow";
interface HeadOfGroupProps {
groupManager: object | null;
setGroupManager: (groupManager: object | null) => void;
groupMembers: object[] | null;
setGroupMembers: (groupMembers: object[]) => void;
groupManager: TUser | null;
removeManager: () => void;
onShowSelectGroupManagerPanel: () => void;
}
const HeadOfGroup = ({
groupManager,
setGroupManager,
groupMembers,
setGroupMembers,
removeManager,
onShowSelectGroupManagerPanel,
}: HeadOfGroupProps) => {
const { t } = useTranslation(["Common"]);
const onRemoveGroupManager = () => {
setGroupManager(null);
setGroupMembers(
groupMembers?.filter((gm) => gm.id !== groupManager!.id) || [],
);
};
return (
<div>
<Styled.Header>{t("Common:HeadOfGroup")}</Styled.Header>
@ -67,7 +58,7 @@ const HeadOfGroup = ({
) : (
<GroupMemberRow
groupMember={groupManager}
onClickRemove={onRemoveGroupManager}
removeMember={removeManager}
/>
)}
</div>

View File

@ -24,40 +24,49 @@
// 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 { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { SelectorAddButton } from "@docspace/shared/components/selector-add-button";
import { TUser } from "@docspace/shared/api/people/types";
import PlusSvgUrl from "PUBLIC_DIR/images/icons/16/button.plus.react.svg?url";
import * as Styled from "./index.styled";
import SelectGroupMembersPanel from "./SelectGroupMembersPanel";
import GroupMemberRow from "../GroupMemberRow";
import { GroupMembersList } from "../GroupMembersList/GroupMembersList";
interface GroupMember {
id: string;
}
type InfiniteLoaderProps =
| {
withInfiniteLoader: true;
loadNextPage: (startIndex: number) => Promise<void>;
hasNextPage: boolean;
total: number;
}
| Partial<{
withInfiniteLoader: undefined;
loadNextPage: undefined;
hasNextPage: undefined;
total: undefined;
}>;
interface MembersParamProps {
groupManager: GroupMember | null;
groupMembers: GroupMember[] | null;
setGroupMembers: (groupMembers: GroupMember[]) => void;
type MembersParamProps = {
groupManager: TUser | null;
groupMembers: TUser[] | null;
onShowSelectMembersPanel: () => void;
}
removeMember: (member: TUser) => void;
} & InfiniteLoaderProps;
const MembersParam = ({
groupManager,
groupMembers,
setGroupMembers,
onShowSelectMembersPanel,
withInfiniteLoader,
hasNextPage,
loadNextPage,
total,
removeMember,
}: MembersParamProps) => {
const { t } = useTranslation(["Common", "PeopleTranslation"]);
const onRemoveUserById = useCallback(
(id: string) => {
const newGroupMembers = groupMembers?.filter((gm) => gm.id !== id);
setGroupMembers(newGroupMembers || []);
},
[groupMembers, setGroupMembers],
);
return (
<div>
<Styled.Header>{t("Common:Members")}</Styled.Header>
@ -67,16 +76,27 @@ const MembersParam = ({
<div className="label">{t("PeopleTranslations:AddMembers")}</div>
</Styled.AddMembersButton>
{groupMembers &&
groupMembers
.filter((member) => member.id !== groupManager?.id)
.map((member) => (
<GroupMemberRow
key={member.id}
groupMember={member}
onClickRemove={() => onRemoveUserById(member.id)}
/>
))}
{groupMembers ? (
withInfiniteLoader ? (
<GroupMembersList
members={groupMembers}
removeMember={removeMember}
hasNextPage={hasNextPage}
loadNextPage={loadNextPage}
total={total}
/>
) : (
groupMembers
.filter((member) => member.id !== groupManager?.id)
.map((member) => (
<GroupMemberRow
key={member.id}
groupMember={member}
removeMember={removeMember}
/>
))
)
) : null}
</div>
);
};

View File

@ -26,59 +26,38 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { ShareAccessRights } from "@docspace/shared/enums";
import { toastr } from "@docspace/shared/components/toast";
import { Portal } from "@docspace/shared/components/portal";
import { TUser } from "@docspace/shared/api/people/types";
import { TSelectorItem } from "@docspace/shared/components/selector";
import AddUsersPanel from "../../../../panels/AddUsersPanel";
import { getAccessOptions } from "../../../../panels/InvitePanel/utils";
interface SelectGroupMembersPanelProps {
type MembersSelectorProps = {
isVisible: boolean;
onClose: () => void;
onParentPanelClose: () => void;
groupManager?: TUser;
groupMembers: TUser[];
setGroupMembers: (groupMembers: (TUser | TSelectorItem)[]) => void;
}
const SelectGroupMembersPanel = ({
addMembers: (members: TUser[]) => void;
} & (
| { checkIfUserInvited: (user: TUser) => boolean; invitedUsers?: undefined }
| { invitedUsers: string[]; checkIfUserInvited?: undefined }
);
export const MembersSelector = ({
isVisible,
onClose,
addMembers,
onParentPanelClose,
groupManager,
groupMembers,
setGroupMembers,
}: SelectGroupMembersPanelProps) => {
onClose,
checkIfUserInvited,
invitedUsers,
}: MembersSelectorProps) => {
const { t } = useTranslation(["InviteDialog"]);
const accessOptions = getAccessOptions(t, 5, false, true);
const onAddGroupMembers = (newGroupMembers: TSelectorItem[]) => {
const resultGroupMembers: (TUser | TSelectorItem)[] = [...groupMembers];
let showErrorWasSelected = false;
newGroupMembers.forEach((groupMember) => {
if (groupMembers.findIndex((gm) => gm.id === groupMember.id) !== -1) {
showErrorWasSelected = true;
return;
}
resultGroupMembers.push(groupMember);
});
if (showErrorWasSelected) {
toastr.warning("Some users have already been added");
}
setGroupMembers(resultGroupMembers);
};
const invitedUsers = React.useMemo(
() => [...groupMembers].map((g) => g?.id),
[groupMembers],
);
if (groupManager) invitedUsers.push(groupManager.id);
const invitedProps = checkIfUserInvited
? { checkIfUserInvited }
: { invitedUsers };
return (
<Portal
@ -88,19 +67,17 @@ const SelectGroupMembersPanel = ({
onClose={onClose}
onParentPanelClose={onParentPanelClose}
isMultiSelect
setDataItems={onAddGroupMembers}
setDataItems={addMembers}
accessOptions={accessOptions}
withAccessRights={false}
isEncrypted
defaultAccess={ShareAccessRights.FullAccess}
withoutBackground
withBlur={false}
invitedUsers={invitedUsers}
disableDisabledUsers
{...invitedProps}
/>
}
/>
);
};
export default SelectGroupMembersPanel;

View File

@ -24,41 +24,43 @@
// 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 from "react";
import { TUser } from "@docspace/shared/api/people/types";
import { EditGroupParams, GroupMembers } from "../types";
const compareMembers = (a: GroupMembers, b: GroupMembers): boolean => {
if (!a && !b) return true;
if (!Array.isArray(a) || !Array.isArray(b)) return false;
if (a.length !== b.length) return false;
import { MembersSelector } from "../MembersSelector/MembersSelector";
const sortCb = (first: TUser, second: TUser) =>
first.id < second.id ? -1 : 1;
const sortedA = [...a].sort(sortCb);
const sortedB = [...b].sort(sortCb);
return !sortedA.some((el, i) => el.id !== sortedB[i].id);
type SelectMembersPanelProps = {
isVisible: boolean;
onClose: () => void;
onParentPanelClose: () => void;
groupManager: TUser | null;
groupMembers: TUser[];
addMembers: (members: TUser[]) => void;
};
const removeManagerFromMembers = (members: GroupMembers, managerId: string) => {
return members?.filter((g) => g.id !== managerId) || null;
};
export const compareGroupParams = (
prev: EditGroupParams,
current: EditGroupParams,
): boolean => {
const equalTitle = prev.groupName === current.groupName;
const equalManager = prev.groupManager?.id === current.groupManager?.id;
const prevGroupMembers = prev.groupManager?.id
? removeManagerFromMembers(prev.groupMembers, prev.groupManager.id)
: prev.groupMembers;
const currentGroupMembers = current.groupManager?.id
? removeManagerFromMembers(current.groupMembers, current.groupManager.id)
: current.groupMembers;
const equalMembers = compareMembers(prevGroupMembers, currentGroupMembers);
return equalTitle && equalManager && equalMembers;
export const SelectMembersPanel = ({
isVisible,
onClose,
onParentPanelClose,
groupManager,
groupMembers,
addMembers,
}: SelectMembersPanelProps) => {
const invitedUsers = React.useMemo(
() => [...groupMembers].map((g) => g?.id),
[groupMembers],
);
if (groupManager) invitedUsers.push(groupManager.id);
return (
<MembersSelector
isVisible={isVisible}
onClose={onClose}
onParentPanelClose={onParentPanelClose}
addMembers={addMembers}
invitedUsers={invitedUsers}
/>
);
};

View File

@ -0,0 +1,62 @@
import React from "react";
import { inject, observer } from "mobx-react";
import { TUser } from "@docspace/shared/api/people/types";
import EditGroupStore from "SRC_DIR/store/EditGroupStore";
import { MembersSelector } from "../MembersSelector/MembersSelector";
type InjectedProps = Pick<
EditGroupStore,
"group" | "removedMembersMap" | "addedMembersMap"
>;
type SelectMembersPanelProps = {
isVisible: boolean;
onClose: () => void;
onParentPanelClose: () => void;
addMembers: (members: TUser[]) => void;
injectedProps?: InjectedProps;
};
const Panel = ({
isVisible,
onClose,
onParentPanelClose,
addMembers,
injectedProps,
}: SelectMembersPanelProps) => {
const { addedMembersMap, removedMembersMap, group } = injectedProps!;
const checkIfUserInvited = (user: TUser) => {
if (removedMembersMap.has(user.id)) {
return false;
}
if (addedMembersMap.has(user.id)) {
return true;
}
return Boolean(user.groups?.find((g) => g.id === group?.id));
};
return (
<MembersSelector
isVisible={isVisible}
onClose={onClose}
onParentPanelClose={onParentPanelClose}
addMembers={addMembers}
checkIfUserInvited={checkIfUserInvited}
/>
);
};
export const SelectMembersPanel = inject<{ editGroupStore: EditGroupStore }>(
({ editGroupStore }) => {
const { group, removedMembersMap, addedMembersMap } = editGroupStore;
return { injectedProps: { group, removedMembersMap, addedMembersMap } };
},
)(observer(Panel));

View File

@ -28,8 +28,8 @@ import { TUser } from "@docspace/shared/api/people/types";
export type GroupParams = {
groupName: string;
groupManager: object | null;
groupMembers: object[];
groupManager: TUser | null;
groupMembers: TUser[];
};
export type GroupMembers = TUser[] | null;

View File

@ -42,11 +42,15 @@ export const GroupMember = styled.div<{ isExpect: boolean }>`
overflow: auto;
}
.name-wrapper,
.info,
.role-email {
display: flex;
}
.info {
flex-direction: column;
}
.name {
font-weight: 600;
font-size: 14px;
@ -59,6 +63,15 @@ export const GroupMember = styled.div<{ isExpect: boolean }>`
props.isExpect && `color: ${props.theme.infoPanel.members.isExpectName}`};
}
.email {
color: ${({ theme }) => theme.sideBarRow.metaDataColor};
font-size: 12px;
line-height: 16px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.me-label {
font-weight: 600;
font-size: 14px;

View File

@ -131,13 +131,17 @@ const GroupMember = ({ member, infoPanelSelection }: GroupMemberProps) => {
/>
<div className="user_body-wrapper">
<div className="name-wrapper">
<div className="info">
<Text
className="name"
data-tooltip-id={`userTooltip_${Math.random()}`}
noSelect
>
{decode(user.displayName)}
</Text>
<Text className="email" noSelect>
{user.email}
</Text>
</div>
</div>

View File

@ -71,6 +71,7 @@ const toListItem = (
invitedUsers?: string[],
disableDisabledUsers?: boolean,
isRoom?: boolean,
checkIfUserInvited?: (user: TUser) => void,
) => {
if ("displayName" in item) {
const {
@ -87,13 +88,16 @@ const toListItem = (
isRoomAdmin,
status,
shared,
groups,
} = item;
const role = getUserRole(item);
const userAvatar = hasAvatar ? avatar : DefaultUserPhoto;
const isInvited = invitedUsers?.includes(id) || (isRoom && shared);
const isInvited = checkIfUserInvited
? checkIfUserInvited(item)
: invitedUsers?.includes(id) || (isRoom && shared);
const isDisabled =
disableDisabledUsers && status === EmployeeStatus.Disabled;
@ -117,6 +121,7 @@ const toListItem = (
isRoomAdmin,
isDisabled: isInvited || isDisabled,
disabledText,
groups,
} as TSelectorItem;
}
@ -162,6 +167,7 @@ type AddUsersPanelProps = {
invitedUsers?: string[];
disableDisabledUsers?: boolean;
checkIfUserInvited?: (user: TUser) => boolean;
roomId?: string | number;
withGroups?: boolean;
@ -189,6 +195,7 @@ const AddUsersPanel = ({
invitedUsers,
disableDisabledUsers,
checkIfUserInvited,
}: AddUsersPanelProps) => {
const theme = useTheme();
const { t } = useTranslation([
@ -285,6 +292,7 @@ const AddUsersPanel = ({
newItem.isCollaborator = user.isCollaborator;
newItem.isRoomAdmin = user.isRoomAdmin;
newItem.email = user.email;
newItem.groups = user.groups;
}
items.push(newItem);
@ -366,7 +374,14 @@ const AddUsersPanel = ({
const totalDifferent = startIndex ? response.total - totalRef.current : 0;
const items = response.items.map((item) =>
toListItem(item, t, invitedUsers, disableDisabledUsers, !!roomId),
toListItem(
item,
t,
invitedUsers,
disableDisabledUsers,
!!roomId,
checkIfUserInvited,
),
);
const newTotal = response.total - totalDifferent;

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,13 +26,22 @@
import { inject, observer } from "mobx-react";
import { withTranslation } from "react-i18next";
import * as Styled from "../../styles/groups.styled";
import { useParams } from "react-router-dom";
import { useState, useEffect, useRef } 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,
SHOW_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 { useParams } from "react-router-dom";
import { useState, useEffect } from "react";
import { GroupMembersList } from "./GroupMembersList/GroupMembersList";
const Groups = ({
infoPanelSelection,
@ -41,7 +50,14 @@ const Groups = ({
infoPanelSelectedGroup,
setInfoPanelSelectedGroup,
}) => {
const [isShowLoader, setIsShowLoader] = useState(false);
const [isFirstLoad, setIsFirstLoad] = useState(true);
const [showLoader, setShowLoader] = useState(false);
const [groupMembers, setGroupMembers] = useState(null);
const [total, setTotal] = useState(0);
const abortControllerRef = useRef(new AbortController());
const startLoader = useRef(null);
const loaderTimeout = useRef(null);
const { groupId: paramsGroupId } = useParams();
const isInsideGroup = !!paramsGroupId;
@ -51,32 +67,121 @@ const Groups = ({
const groupId = isInsideGroup ? paramsGroupId : infoPanelSelection?.id;
const setGroup = isInsideGroup ? setCurrentGroup : setInfoPanelSelectedGroup;
const groupManager = group?.manager;
const loadNextPage = async (startIndex) => {
try {
abortControllerRef.current = new AbortController();
const pageCount = 100;
const filter = AccountsFilter.getDefault();
filter.group = groupId;
filter.page = Math.ceil(startIndex / pageCount);
filter.pageCount = pageCount;
const res = await api.people.getUserList(
filter,
abortControllerRef.current.signal,
);
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 {
if (startIndex === 0) {
setIsFirstLoad(false);
}
}
};
const calculateLoader = () => {
if (isFirstLoad) {
loaderTimeout.current = setTimeout(() => {
startLoader.current = new Date();
setShowLoader(true);
}, SHOW_LOADER_TIMER);
} else if (startLoader.current) {
const currentDate = new Date();
const ms = Math.abs(
startLoader.current.getTime() - currentDate.getTime(),
);
if (ms >= MIN_LOADER_TIMER) {
startLoader.current = null;
return setShowLoader(false);
}
setTimeout(() => {
startLoader.current = null;
setShowLoader(false);
}, MIN_LOADER_TIMER - ms);
loaderTimeout.current = null;
} else if (loaderTimeout.current) {
clearTimeout(loaderTimeout.current);
loaderTimeout.current = null;
}
};
useFetchGroup(groupId, group?.id, setGroup);
useEffect(() => {
const showLoaderTimer = setTimeout(() => setIsShowLoader(true), 500);
return () => clearTimeout(showLoaderTimer);
setIsFirstLoad(true);
}, [infoPanelSelection.id]);
useEffect(() => {
if (group) {
loadNextPage(0);
}
return () => {
abortControllerRef.current.abort();
};
}, [group]);
useEffect(() => {
calculateLoader();
}, [isFirstLoad]);
useEffect(() => {
return () => {
loaderTimeout.current = null;
};
}, []);
const groupManager = group?.manager;
const groupMembers = group?.members?.filter(
(user) => user.id !== groupManager?.id,
);
if (!group) {
if (isShowLoader) return <InfoPanelViewLoader view="groups" />;
return null;
if (showLoader) {
return (
<Styled.GroupsContent>
<InfoPanelViewLoader view="groups" />
</Styled.GroupsContent>
);
}
const totalWithoutManager = groupManager ? total - 1 : total;
return (
<Styled.GroupsContent>
{groupManager && <GroupMember groupMember={groupManager} isManager />}
{!groupMembers ? (
<InfoPanelViewLoader view="groups" />
) : (
groupMembers?.map((groupMember) => (
<GroupMember key={groupMember.id} groupMember={groupMember} />
))
{isFirstLoad || !groupMembers ? null : (
<>
{groupManager && <GroupMember groupMember={groupManager} isManager />}
<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

@ -0,0 +1,248 @@
// (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 { makeAutoObservable } from "mobx";
import { TUser } from "@docspace/shared/api/people/types";
import { TGroup } from "@docspace/shared/api/groups/types";
import AccountsFilter from "@docspace/shared/api/people/filter";
import api from "@docspace/shared/api";
import PeopleStore from "SRC_DIR/store/PeopleStore";
import GroupsStore from "SRC_DIR/store/GroupsStore";
class EditGroupStore {
isInit = false;
group: TGroup | null = null;
title: string = "";
manager: TUser | null = null;
members: TUser[] | null = null;
addedMembersMap: Map<string, TUser> = new Map();
removedMembersMap: Map<string, TUser> = new Map();
initialTotal: number = 0;
filter = AccountsFilter.getDefault();
peopleStore: PeopleStore;
constructor(peopleStore: PeopleStore) {
this.peopleStore = peopleStore;
makeAutoObservable(this);
}
initGroupData = async (group: TGroup) => {
try {
this.setGroup(group);
this.setTitle(group.name);
if (group.manager) {
this.setManager(group.manager);
}
this.filter.group = group.id;
this.filter.pageCount = 100;
await this.loadMembers(0);
this.setIsInit(true);
} catch (e) {
console.log(e);
}
};
resetGroupData = () => {
this.isInit = false;
this.group = null;
this.title = "";
this.manager = null;
this.members = null;
this.addedMembersMap = new Map();
this.removedMembersMap = new Map();
this.initialTotal = 0;
this.filter = AccountsFilter.getDefault();
};
loadMembers = async (startIndex: number) => {
try {
if (!this.group?.id) return;
this.filter.page = !startIndex ? 0 : this.filter.page + 1;
const res = await api.people.getUserList(this.filter);
const membersWithoutManager = res.items.filter(
(item) =>
item.id !== this.manager?.id || item.id !== this.group?.manager?.id,
);
this.setInitialTotal(res.total);
if (startIndex === 0 || !this.members) {
this.setMembers(membersWithoutManager);
} else {
this.setMembers([...this.members, ...membersWithoutManager]);
}
} catch (e) {
console.log(e);
}
};
submitChanges = async () => {
try {
if (!this.group) return;
const { updateGroup } = this.peopleStore.groupsStore! as GroupsStore;
const addedIds = Array.from(this.addedMembersMap.keys());
const removedIds = Array.from(this.removedMembersMap.keys());
await updateGroup(
this.group?.id,
this.title.trim(),
this.manager?.id,
addedIds,
removedIds,
);
} catch (e) {
console.log(e);
}
};
addManager = (manager: TUser) => {
this.removedMembersMap.delete(manager.id);
const alreadyMember = manager.groups?.find((g) => g.id === this.group?.id);
if (!alreadyMember) {
this.addedMembersMap.set(manager.id, manager);
}
if (this.members?.length) {
this.members = this.members.filter((member) => member.id !== manager.id);
}
this.manager = manager;
};
removeManager = () => {
if (!this.manager) return;
const wasAdded = this.addedMembersMap.delete(this.manager.id);
if (!wasAdded) {
this.removedMembersMap.set(this.manager.id, this.manager);
}
this.manager = null;
};
setIsInit = (value: boolean) => {
this.isInit = value;
};
setGroup = (group: TGroup) => {
this.group = group;
};
setTitle = (title: string) => {
this.title = title;
};
setManager = (manager: TUser | null) => {
this.manager = manager;
};
setMembers = (members: TUser[] | null) => {
this.members = members;
};
addMembers = (members: TUser[]) => {
members.forEach((member) => {
const wasRemoved = this.removedMembersMap.delete(member.id);
if (!wasRemoved) {
this.addedMembersMap.set(member.id, member);
}
});
this.members = this.members ? [...this.members, ...members] : members;
};
removeMember = (member: TUser) => {
const wasAdded = this.addedMembersMap.delete(member.id);
if (!wasAdded) {
this.removedMembersMap.set(member.id, member);
}
this.members = this.members?.filter((m) => m.id !== member.id) || null;
};
setInitialTotal = (total: number) => {
this.initialTotal = total;
};
get currentTotal() {
let total =
this.initialTotal +
this.addedMembersMap.size -
this.removedMembersMap.size;
const prevManager = this.group?.manager;
const newManager = this.manager;
const managerWasChanged = prevManager?.id !== newManager?.id;
if (prevManager && !managerWasChanged) {
total -= 1;
}
if (newManager && managerWasChanged) {
total -= 1;
}
return total;
}
get hasChanges() {
const titleWasChanged = this.title.trim() !== this.group?.name;
const managerWasChanged = this.group?.manager?.id !== this.manager?.id;
return (
titleWasChanged ||
managerWasChanged ||
this.addedMembersMap.size ||
this.removedMembersMap.size
);
}
}
export default EditGroupStore;

View File

@ -637,7 +637,7 @@ class GroupsStore {
updateGroup = async (
groupId: string,
groupName: string,
groupManagerId: string,
groupManagerId: string | undefined,
membersToAdd: string[],
membersToRemove: string[],
) => {

View File

@ -80,6 +80,7 @@ import ImportAccountsStore from "./ImportAccountsStore";
import PluginStore from "./PluginStore";
import InfoPanelStore from "./InfoPanelStore";
import CampaignsStore from "./CampaignsStore";
import EditGroupStore from "./EditGroupStore";
const selectedFolderStore = new SelectedFolderStore(settingsStore);
@ -300,6 +301,8 @@ const storageManagement = new StorageManagement(
const campaignsStore = new CampaignsStore(settingsStore, userStore);
const editGroupStore = new EditGroupStore(peopleStore);
const store = {
authStore,
userStore,
@ -354,6 +357,7 @@ const store = {
pluginStore,
storageManagement,
campaignsStore,
editGroupStore,
};
export default store;

View File

@ -38,7 +38,7 @@ import {
export const createGroup = (
groupName: string,
groupManager: string,
groupManager: string | undefined,
members: string[],
) => {
return request({
@ -49,7 +49,7 @@ export const createGroup = (
groupManager,
members,
},
});
}) as Promise<TGroup>;
};
// * Read
@ -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 (
@ -123,7 +127,7 @@ export const getGroupMembersInRoom = (
export const updateGroup = (
groupId: string,
groupName: string,
groupManager: string,
groupManager: string | undefined,
membersToAdd: string[],
membersToRemove: string[],
) => {

View File

@ -30,10 +30,11 @@ import { ShareAccessRights } from "../../enums";
export type TGroup = {
category: string;
id: string;
manager: TUser;
manager?: TUser;
name: string;
parent: string;
isGroup?: boolean;
members?: TUser[];
membersCount: number;
shared?: boolean;
isLDAP: boolean;

View File

@ -30,6 +30,7 @@ import { MergeTypes, Nullable } from "../../types";
import { TFileSecurity, TFolderSecurity } from "../../api/files/types";
import { TRoomSecurity } from "../../api/rooms/types";
import { TGroup } from "../../api/groups/types";
import { AvatarRole } from "../avatar";
import { TTabItem } from "../tabs";
@ -394,6 +395,7 @@ type TSelectorItemEmpty = {
iconOriginal?: undefined;
role?: undefined;
email?: undefined;
groups?: TGroup[];
isOwner?: undefined;
isAdmin?: undefined;
isVisitor?: undefined;
@ -438,6 +440,7 @@ export type TSelectorItemUser = MergeTypes<
avatar: string;
hasAvatar: boolean;
role: AvatarRole;
groups?: TGroup[];
access?: ShareAccessRights | string | number;
}