Browse Source

imprv: User group delete (#5078)

* CC to FC

* Improved userGroupTable

* SWRized

* Updated table & home page component

* Removed hard code

* Implemented listing api

* Fixed lint error

* Implemented key serializer for swr

* Impl update

* Omit serialize middleware

* Upgraded swr ^1.0.1 => ^1.1.2

* Improved sync

* Fixed lint errors

* Removed unnecessary code

* Improved nukey

* Improved types

* Improved interface

* Improved type validation

* Implemented delete

* Improved

* Removed unnecessary code

* Removed unnecessary method

* Added a comment

* Typescriptized

* Omitted crowi

* Fixed

* Fixed ci

* Fixed

* Fixed model import

* Deleted user-group.js
Haku Mizuki 4 years ago
parent
commit
44dce1dc98

+ 5 - 5
packages/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx

@@ -102,7 +102,7 @@ const UserGroupDeleteModal: FC<Props> = (props: Props) => {
   const handleActionChange = useCallback((e) => {
     const actionName = e.target.value;
     setActionName(actionName);
-  }, []);
+  }, [setActionName]);
 
   const handleGroupChange = useCallback((e) => {
     const transferToUserGroupId = e.target.value;
@@ -121,10 +121,10 @@ const UserGroupDeleteModal: FC<Props> = (props: Props) => {
       actionName,
       transferToUserGroupId,
     );
-  }, [props.onDelete, props.deleteUserGroup]);
+  }, [props.onDelete, props.deleteUserGroup, actionName, transferToUserGroupId]);
 
   const renderPageActionSelector = useCallback(() => {
-    const optoins = availableOptions.map((opt) => {
+    const options = availableOptions.map((opt) => {
       const dataContent = `<i class="icon icon-fw ${opt.iconClass} ${opt.styleClass}"></i> <span class="action-name ${opt.styleClass}">${opt.label}</span>`;
       return <option key={opt.id} value={opt.actionForPages} data-content={dataContent}>{opt.label}</option>;
     });
@@ -138,10 +138,10 @@ const UserGroupDeleteModal: FC<Props> = (props: Props) => {
         onChange={handleActionChange}
       >
         <option value="" disabled>{t('admin:user_group_management.delete_modal.dropdown_desc')}</option>
-        {optoins}
+        {options}
       </select>
     );
-  }, [handleActionChange]);
+  }, [handleActionChange, actionName, availableOptions]);
 
   const renderGroupSelector = useCallback(() => {
     const { deleteUserGroup } = props;

+ 2 - 2
packages/app/src/components/Admin/UserGroup/UserGroupPage.tsx

@@ -97,10 +97,10 @@ const UserGroupPage: FC<Props> = (props: Props) => {
       setSelectedUserGroup(undefined);
       setDeleteModalShown(false);
 
-      toastSuccess(`Deleted a group "${xss.process(res.data.userGroup.name)}"`);
+      toastSuccess(`Deleted ${xss.process(res.data.userGroups.length)} groups.`);
     }
     catch (err) {
-      toastError(new Error('Unable to delete the group'));
+      toastError(new Error('Unable to delete the groups'));
     }
   }, [mutateUserGroups, mutateUserGroupRelations]);
 

+ 4 - 4
packages/app/src/components/Admin/UserGroup/UserGroupTable.tsx

@@ -133,15 +133,15 @@ const UserGroupTable: FC<Props> = (props: Props) => {
                   <ul className="list-inline">
                     {groupIdToChildGroupsMap[group._id] != null && groupIdToChildGroupsMap[group._id].map((group) => {
                       return (
-                        <li key={group._id} className="list-inline-item badge badge-pill badge-warning">
+                        <li key={group._id} className="list-inline-item badge badge-success">
                           {props.isAclEnabled
                             ? (
-                              <td><a href={`/admin/user-group-detail/${group._id}`}>{xss.process(group.name)}</a></td>
+                              <a href={`/admin/user-group-detail/${group._id}`}>{xss.process(group.name)}</a>
                             )
                             : (
-                              <td>{xss.process(group.name)}</td>
+                              <p>{xss.process(group.name)}</p>
                             )
-                          },&nbsp;
+                          }
                         </li>
                       );
                     })}

+ 18 - 13
packages/app/src/server/models/obsolete-page.js

@@ -1121,24 +1121,29 @@ export const getPageSchema = (crowi) => {
     return await queryBuilder.query.exec();
   };
 
-  pageSchema.statics.publicizePage = async function(page) {
-    page.grantedGroup = null;
-    page.grant = GRANT_PUBLIC;
-    await page.save();
+  pageSchema.statics.publicizePages = async function(pages) {
+    const operationsToPublicize = pages.map((page) => {
+      return {
+        updateOne: {
+          filter: { _id: page._id },
+          update: {
+            grantedGroup: null,
+            grant: this.GRANT_PUBLIC,
+          },
+        },
+      };
+    });
+    await this.bulkWrite(operationsToPublicize);
   };
 
-  pageSchema.statics.transferPageToGroup = async function(page, transferToUserGroupId) {
+  pageSchema.statics.transferPagesToGroup = async function(pages, transferToUserGroupId) {
     const UserGroup = mongoose.model('UserGroup');
 
-    // check page existence
-    const isExist = await UserGroup.count({ _id: transferToUserGroupId }) > 0;
-    if (isExist) {
-      page.grantedGroup = transferToUserGroupId;
-      await page.save();
-    }
-    else {
-      throw new Error('Cannot find the group to which private pages belong to. _id: ', transferToUserGroupId);
+    if ((await UserGroup.count({ _id: transferToUserGroupId })) === 0) {
+      throw Error('Cannot find the group to which private pages belong to. _id: ', transferToUserGroupId);
     }
+
+    await this.updateMany({ _id: { $in: pages.map(p => p._id) } }, { grantedGroup: transferToUserGroupId });
   };
 
   /**

+ 6 - 2
packages/app/src/server/models/user-group-relation.js

@@ -248,8 +248,12 @@ class UserGroupRelation {
    * @returns {Promise<any>}
    * @memberof UserGroupRelation
    */
-  static removeAllByUserGroup(userGroup) {
-    return this.deleteMany({ relatedGroup: userGroup });
+  static removeAllByUserGroups(groupsToDelete) {
+    if (!Array.isArray(groupsToDelete)) {
+      throw Error('groupsToDelete must be an array.');
+    }
+
+    return this.deleteMany({ relatedGroup: { $in: groupsToDelete } });
   }
 
   /**

+ 10 - 17
packages/app/src/server/models/user-group.ts

@@ -66,23 +66,6 @@ schema.statics.findChildUserGroupsByParentIds = async function(parentIds, includ
   };
 };
 
-// schema.statics.removeCompletelyById = async function(deleteGroupId, action, transferToUserGroupId, user) { // TODO 85062: move this to the service layer
-//   const UserGroupRelation = mongoose.model('UserGroupRelation') as any; // TODO 85062: Typescriptize model
-
-//   const groupToDelete = await this.findById(deleteGroupId);
-//   if (groupToDelete == null) {
-//     throw Error(`UserGroup data is not exists. id: ${deleteGroupId}`);
-//   }
-//   const deletedGroup = await groupToDelete.remove();
-
-//   await Promise.all([
-//     UserGroupRelation.removeAllByUserGroup(deletedGroup),
-//     crowi.pageService.handlePrivatePagesForDeletedGroup(deletedGroup, action, transferToUserGroupId, user),
-//   ]);
-
-//   return deletedGroup;
-// };
-
 schema.statics.countUserGroups = function() {
   return this.estimatedDocumentCount();
 };
@@ -116,4 +99,14 @@ schema.statics.findAllAncestorGroups = async function(parent, ancestors = [paren
   return this.findAllAncestorGroups(nextParent, ancestors);
 };
 
+schema.statics.findGroupsWithDescendantsRecursively = async function(groups, descendants = groups) {
+  const nextGroups = await this.find({ parent: { $in: groups.map(g => g._id) } });
+
+  if (nextGroups.length === 0) {
+    return descendants;
+  }
+
+  return this.findGroupsWithDescendantsRecursively(nextGroups, descendants.concat(nextGroups));
+};
+
 export default getOrCreateModel<UserGroupDocument, UserGroupModel>('UserGroup', schema);

+ 4 - 4
packages/app/src/server/routes/apiv3/user-group.js

@@ -209,14 +209,14 @@ module.exports = (crowi) => {
     const { actionName, transferToUserGroupId } = req.query;
 
     try {
-      const userGroup = await UserGroup.removeCompletelyById(deleteGroupId, actionName, transferToUserGroupId, req.user);
+      const userGroups = await crowi.userGroupService.removeCompletelyByRootGroupId(deleteGroupId, actionName, transferToUserGroupId, req.user);
 
-      return res.apiv3({ userGroup });
+      return res.apiv3({ userGroups });
     }
     catch (err) {
-      const msg = 'Error occurred in deleting a user group';
+      const msg = 'Error occurred while deleting user groups';
       logger.error(msg, err);
-      return res.apiv3Err(new ErrorV3(msg, 'user-group-delete-failed'));
+      return res.apiv3Err(new ErrorV3(msg, 'user-groups-delete-failed'));
     }
   });
 

+ 5 - 8
packages/app/src/server/service/page.js

@@ -829,22 +829,19 @@ class PageService {
   }
 
 
-  async handlePrivatePagesForDeletedGroup(deletedGroup, action, transferToUserGroupId, user) {
+  async handlePrivatePagesForGroupsToDelete(groupsToDelete, action, transferToUserGroupId, user) {
     const Page = this.crowi.model('Page');
-    const pages = await Page.find({ grantedGroup: deletedGroup });
+    const pages = await Page.find({ grantedGroup: { $in: groupsToDelete } });
 
+    let operationsToPublicize;
     switch (action) {
       case 'public':
-        await Promise.all(pages.map((page) => {
-          return Page.publicizePage(page);
-        }));
+        await Page.publicizePages(pages);
         break;
       case 'delete':
         return this.deleteMultipleCompletely(pages, user);
       case 'transfer':
-        await Promise.all(pages.map((page) => {
-          return Page.transferPageToGroup(page, transferToUserGroupId);
-        }));
+        await Page.transferPagesToGroup(pages, transferToUserGroupId);
         break;
       default:
         throw new Error('Unknown action for private pages');

+ 18 - 0
packages/app/src/server/service/user-group.ts

@@ -74,6 +74,24 @@ class UserGroupService {
     return userGroup.save();
   }
 
+  async removeCompletelyByRootGroupId(deleteRootGroupId, action, transferToUserGroupId, user) {
+    const rootGroup = await UserGroup.findById(deleteRootGroupId);
+    if (rootGroup == null) {
+      throw new Error(`UserGroup data does not exist. id: ${deleteRootGroupId}`);
+    }
+
+    const groupsToDelete = await UserGroup.findGroupsWithDescendantsRecursively([rootGroup]);
+
+    // 1. update page & remove all groups
+    await this.crowi.pageService.handlePrivatePagesForGroupsToDelete(groupsToDelete, action, transferToUserGroupId, user);
+    // 2. remove all groups
+    const deletedGroups = await UserGroup.deleteMany({ _id: { $in: groupsToDelete.map(g => g._id) } });
+    // 3. remove all relations
+    await UserGroupRelation.removeAllByUserGroups(groupsToDelete);
+
+    return deletedGroups;
+  }
+
 }
 
 module.exports = UserGroupService;

+ 2 - 2
packages/app/src/stores/user-group.tsx

@@ -35,8 +35,8 @@ export const useSWRxUserGroupRelationList = (
 ): SWRResponse<IUserGroupRelationHasId[], Error> => {
   return useSWRImmutable<IUserGroupRelationHasId[], Error>(
     groupIds != null ? ['/user-group-relations', groupIds, childGroupIds] : null,
-    (endpoint, parentIds, childGroupIds) => apiv3Get<UserGroupRelationListResult>(
-      endpoint, { parentIds, childGroupIds },
+    (endpoint, groupIds, childGroupIds) => apiv3Get<UserGroupRelationListResult>(
+      endpoint, { groupIds, childGroupIds },
     ).then(result => result.data.userGroupRelations),
     {
       fallbackData: initialData,