Merge pull request #580 from ONLYOFFICE/bugfix/huge-group-issues
Bugfix/huge group issues
This commit is contained in:
commit
7f2b4c9686
@ -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;
|
||||
}
|
||||
`;
|
||||
|
@ -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));
|
||||
|
@ -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));
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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;
|
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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]);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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));
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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,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>
|
||||
);
|
||||
|
@ -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));
|
||||
})
|
||||
|
248
packages/client/src/store/EditGroupStore.ts
Normal file
248
packages/client/src/store/EditGroupStore.ts
Normal 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;
|
@ -637,7 +637,7 @@ class GroupsStore {
|
||||
updateGroup = async (
|
||||
groupId: string,
|
||||
groupName: string,
|
||||
groupManagerId: string,
|
||||
groupManagerId: string | undefined,
|
||||
membersToAdd: string[],
|
||||
membersToRemove: string[],
|
||||
) => {
|
||||
|
@ -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;
|
||||
|
@ -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[],
|
||||
) => {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user