Просмотр исходного кода

enable selecting external user group for page grant

Futa Arai 2 лет назад
Родитель
Сommit
8aee364c82

+ 53 - 1
apps/app/src/client/services/user-group.ts

@@ -1,13 +1,19 @@
+import {
+  type GrantedGroup, GroupType, isPopulated, type IUserGroupHasId,
+} from '@growi/core';
+
 import {
   useSWRxAncestorExternalUserGroups,
   useSWRxChildExternalUserGroupList,
   useSWRxExternalUserGroup,
   useSWRxExternalUserGroupRelationList,
   useSWRxExternalUserGroupRelations,
+  useSWRxMyExternalUserGroupRelations,
 } from '~/features/external-user-group/client/stores/external-user-group';
+import { IExternalUserGroupHasId } from '~/features/external-user-group/interfaces/external-user-group';
 import {
   useSWRxAncestorUserGroups,
-  useSWRxChildUserGroupList, useSWRxUserGroup, useSWRxUserGroupRelationList, useSWRxUserGroupRelations,
+  useSWRxChildUserGroupList, useSWRxMyUserGroupRelations, useSWRxUserGroup, useSWRxUserGroupRelationList, useSWRxUserGroupRelations,
 } from '~/stores/user-group';
 
 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@@ -48,3 +54,49 @@ export const useAncestorUserGroups = (userGroupId: string, isExternalGroup: bool
   const externalUserGroupRes = useSWRxAncestorExternalUserGroups(isExternalGroup ? userGroupId : null);
   return isExternalGroup ? externalUserGroupRes : userGroupRes;
 };
+
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+export const useMyUserGroups = (shouldFetch: boolean) => {
+  const { data: myUserGroupRelations, mutate: mutateMyUserGroupRelations } = useSWRxMyUserGroupRelations(shouldFetch);
+  const { data: myExternalUserGroupRelations, mutate: mutateMyExternalUserGroupRelations } = useSWRxMyExternalUserGroupRelations(shouldFetch);
+
+  const mutate = () => {
+    mutateMyUserGroupRelations();
+    mutateMyExternalUserGroupRelations();
+  };
+
+  if (myUserGroupRelations == null || myExternalUserGroupRelations == null) {
+    return { data: null, mutate };
+  }
+
+  const myUserGroups = myUserGroupRelations
+    .map((relation) => {
+      // relation.relatedGroup should be populated by server
+      return isPopulated(relation.relatedGroup) ? relation.relatedGroup : undefined;
+    })
+    // exclude undefined elements
+    .filter((elem): elem is IUserGroupHasId => elem != null)
+    .map((group) => {
+      return {
+        item: group,
+        type: GroupType.userGroup,
+      };
+    });
+  const myExternalUserGroups = myExternalUserGroupRelations
+    .map((relation) => {
+    // relation.relatedGroup should be populated by server
+      return isPopulated(relation.relatedGroup) ? relation.relatedGroup : undefined;
+    })
+    // exclude undefined elements
+    .filter((elem): elem is IExternalUserGroupHasId => elem != null)
+    .map((group) => {
+      return {
+        item: group,
+        type: GroupType.externalUserGroup,
+      };
+    });
+
+  const data = [...myUserGroups, ...myExternalUserGroups];
+
+  return { data, mutate };
+};

+ 14 - 25
apps/app/src/components/SavePageControls/GrantSelector.tsx

@@ -1,7 +1,6 @@
 import React, { useCallback, useState } from 'react';
 
-import { isPopulated } from '@growi/core';
-import type { IUserGroupHasId } from '@growi/core';
+import type { GrantedGroup } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import {
   UncontrolledDropdown,
@@ -10,10 +9,9 @@ import {
   Modal, ModalHeader, ModalBody,
 } from 'reactstrap';
 
+import { useMyUserGroups } from '~/client/services/user-group';
 import type { IPageGrantData } from '~/interfaces/page';
 import { useCurrentUser } from '~/stores/context';
-import { useSWRxMyUserGroupRelations } from '~/stores/user-group';
-
 
 const AVAILABLE_GRANTS = [
   {
@@ -63,12 +61,12 @@ const GrantSelector = (props: Props): JSX.Element => {
   const { data: currentUser } = useCurrentUser();
 
   const shouldFetch = isSelectGroupModalShown;
-  const { data: myUserGroupRelations, mutate: mutateMyUserGroupRelations } = useSWRxMyUserGroupRelations(shouldFetch);
+  const { data: myUserGroups, mutate: mutateMyUserGroups } = useMyUserGroups(shouldFetch);
 
   const showSelectGroupModal = useCallback(() => {
-    mutateMyUserGroupRelations();
+    mutateMyUserGroups();
     setIsSelectGroupModalShown(true);
-  }, [mutateMyUserGroupRelations]);
+  }, [mutateMyUserGroups]);
 
   /**
    * change event handler for grant selector
@@ -85,9 +83,9 @@ const GrantSelector = (props: Props): JSX.Element => {
     }
   }, [onUpdateGrant, showSelectGroupModal]);
 
-  const groupListItemClickHandler = useCallback((grantGroup: IUserGroupHasId) => {
-    if (onUpdateGrant != null) {
-      onUpdateGrant({ grant: 5, grantedGroups: [{ id: grantGroup._id, name: grantGroup.name, type: 'UserGroup' }] });
+  const groupListItemClickHandler = useCallback((grantGroup: GrantedGroup) => {
+    if (onUpdateGrant != null && typeof grantGroup.item !== 'string') {
+      onUpdateGrant({ grant: 5, grantedGroups: [{ id: grantGroup.item._id, name: grantGroup.item.name, type: grantGroup.type }] });
     }
 
     // hide modal
@@ -161,7 +159,7 @@ const GrantSelector = (props: Props): JSX.Element => {
     }
 
     // show spinner
-    if (myUserGroupRelations == null) {
+    if (myUserGroups == null) {
       return (
         <div className="my-3 text-center">
           <i className="fa fa-lg fa-spinner fa-pulse mx-auto text-muted"></i>
@@ -169,16 +167,7 @@ const GrantSelector = (props: Props): JSX.Element => {
       );
     }
 
-    // extract IUserGroupHasId
-    const userRelatedGroups: IUserGroupHasId[] = myUserGroupRelations
-      .map((relation) => {
-        // relation.relatedGroup should be populated by server
-        return isPopulated(relation.relatedGroup) ? relation.relatedGroup : undefined;
-      })
-      // exclude undefined elements
-      .filter((elem): elem is IUserGroupHasId => elem != null);
-
-    if (userRelatedGroups.length === 0) {
+    if (myUserGroups.length === 0) {
       return (
         <div>
           <h4>{t('user_group.belonging_to_no_group')}</h4>
@@ -191,10 +180,10 @@ const GrantSelector = (props: Props): JSX.Element => {
 
     return (
       <div className="list-group">
-        { userRelatedGroups.map((group) => {
+        { myUserGroups.map((group) => {
           return (
-            <button key={group._id} type="button" className="list-group-item list-group-item-action" onClick={() => groupListItemClickHandler(group)}>
-              <h5>{group.name}</h5>
+            <button key={group.item._id} type="button" className="list-group-item list-group-item-action" onClick={() => groupListItemClickHandler(group)}>
+              <h5>{group.item.name}</h5>
               {/* TODO: Replace <div className="small">(TBD) List group members</div> */}
             </button>
           );
@@ -202,7 +191,7 @@ const GrantSelector = (props: Props): JSX.Element => {
       </div>
     );
 
-  }, [currentUser?.admin, groupListItemClickHandler, myUserGroupRelations, shouldFetch, t]);
+  }, [currentUser?.admin, groupListItemClickHandler, myUserGroups, shouldFetch, t]);
 
   return (
     <>

+ 11 - 0
apps/app/src/features/external-user-group/client/stores/external-user-group.ts

@@ -2,6 +2,7 @@ import { type SWRResponseWithUtils, withUtils } from '@growi/core/dist/swr';
 import useSWR, { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
+import { apiGet } from '~/client/util/apiv1-client';
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 import { IExternalUserGroupHasId, IExternalUserGroupRelationHasId, LdapGroupSyncSettings } from '~/features/external-user-group/interfaces/external-user-group';
 import { ChildUserGroupListResult, IUserGroupRelationHasIdPopulatedUser, UserGroupRelationListResult } from '~/interfaces/user-group-response';
@@ -15,6 +16,16 @@ export const useSWRxLdapGroupSyncSettings = (): SWRResponse<LdapGroupSyncSetting
   );
 };
 
+type MyExternalUserGroupRelationsResult = {
+  userGroupRelations: IExternalUserGroupRelationHasId[],
+}
+export const useSWRxMyExternalUserGroupRelations = (shouldFetch: boolean): SWRResponse<IExternalUserGroupRelationHasId[], Error> => {
+  return useSWR(
+    shouldFetch ? '/me/external-user-group-relations' : null,
+    endpoint => apiGet(endpoint).then(result => (result as MyExternalUserGroupRelationsResult).userGroupRelations),
+  );
+};
+
 export const useSWRxExternalUserGroup = (groupId: string | null): SWRResponse<IExternalUserGroupHasId, Error> => {
   return useSWRImmutable(
     groupId != null ? `/external-user-groups/${groupId}` : null,

+ 40 - 27
apps/app/src/features/external-user-group/server/models/external-user-group-relation.integ.ts

@@ -21,6 +21,10 @@ describe('ExternalUserGroupRelation model', () => {
   let user2;
   const userId2 = new mongoose.Types.ObjectId();
 
+  const groupId1 = new mongoose.Types.ObjectId();
+  const groupId2 = new mongoose.Types.ObjectId();
+  const groupId3 = new mongoose.Types.ObjectId();
+
   beforeAll(async() => {
     user1 = await User.create({
       _id: userId1, name: 'user1', username: 'user1', email: 'user1@example.com',
@@ -29,28 +33,25 @@ describe('ExternalUserGroupRelation model', () => {
     user2 = await User.create({
       _id: userId2, name: 'user2', username: 'user2', email: 'user2@example.com',
     });
+
+    await ExternalUserGroup.insertMany([
+      {
+        _id: groupId1, name: 'test group 1', externalId: 'testExternalId', provider: 'testProvider',
+      },
+      {
+        _id: groupId2, name: 'test group 2', externalId: 'testExternalId2', provider: 'testProvider',
+      },
+      {
+        _id: groupId3, name: 'test group 3', externalId: 'testExternalId3', provider: 'testProvider',
+      },
+    ]);
   });
 
   afterEach(async() => {
-    await ExternalUserGroup.deleteMany();
     await ExternalUserGroupRelation.deleteMany();
   });
 
   describe('createRelations', () => {
-    const groupId1 = new mongoose.Types.ObjectId();
-    const groupId2 = new mongoose.Types.ObjectId();
-
-    beforeAll(async() => {
-      await ExternalUserGroup.insertMany([
-        {
-          _id: groupId1, name: 'test group 1', externalId: 'testExternalId', provider: 'testProvider',
-        },
-        {
-          _id: groupId2, name: 'test group 2', externalId: 'testExternalId2', provider: 'testProvider',
-        },
-      ]);
-    });
-
     it('creates relation for user', async() => {
       await ExternalUserGroupRelation.createRelations([groupId1, groupId2], user1);
       const relations = await ExternalUserGroupRelation.find();
@@ -62,11 +63,10 @@ describe('ExternalUserGroupRelation model', () => {
   });
 
   describe('removeAllInvalidRelations', () => {
-    const groupId1 = new mongoose.Types.ObjectId();
-    const groupId2 = new mongoose.Types.ObjectId();
-
     beforeAll(async() => {
-      await ExternalUserGroupRelation.createRelations([groupId1, groupId2], user1);
+      const nonExistentGroupId1 = new mongoose.Types.ObjectId();
+      const nonExistentGroupId2 = new mongoose.Types.ObjectId();
+      await ExternalUserGroupRelation.createRelations([nonExistentGroupId1, nonExistentGroupId2], user1);
     });
 
     it('removes invalid relations', async() => {
@@ -81,10 +81,6 @@ describe('ExternalUserGroupRelation model', () => {
   });
 
   describe('findAllUserIdsForUserGroups', () => {
-    const groupId1 = new mongoose.Types.ObjectId();
-    const groupId2 = new mongoose.Types.ObjectId();
-    const groupId3 = new mongoose.Types.ObjectId();
-
     beforeAll(async() => {
       await ExternalUserGroupRelation.createRelations([groupId1, groupId2], user1);
       await ExternalUserGroupRelation.create({ relatedGroup: groupId3, relatedUser: user2._id });
@@ -97,10 +93,6 @@ describe('ExternalUserGroupRelation model', () => {
   });
 
   describe('findAllUserGroupIdsRelatedToUser', () => {
-    const groupId1 = new mongoose.Types.ObjectId();
-    const groupId2 = new mongoose.Types.ObjectId();
-    const groupId3 = new mongoose.Types.ObjectId();
-
     beforeAll(async() => {
       await ExternalUserGroupRelation.createRelations([groupId1, groupId2], user1);
       await ExternalUserGroupRelation.create({ relatedGroup: groupId3, relatedUser: user2._id });
@@ -114,4 +106,25 @@ describe('ExternalUserGroupRelation model', () => {
       expect(groupIds2).toStrictEqual([groupId3]);
     });
   });
+
+  describe('findAllRelationForUser', () => {
+    beforeAll(async() => {
+      await ExternalUserGroupRelation.createRelations([groupId1, groupId2], user1);
+      await ExternalUserGroupRelation.create({ relatedGroup: groupId3, relatedUser: user2._id });
+    });
+
+    it('finds all relations for user with group populated', async() => {
+      const relations = await ExternalUserGroupRelation.findAllRelationForUser(user1);
+      const populatedGroupIds = relations.map((relation) => {
+        return typeof relation.relatedGroup !== 'string' ? relation.relatedGroup._id : null;
+      });
+      expect(populatedGroupIds).toStrictEqual([groupId1, groupId2]);
+
+      const relations2 = await ExternalUserGroupRelation.findAllRelationForUser(user2);
+      const populatedGroupIds2 = relations2.map((relation) => {
+        return typeof relation.relatedGroup !== 'string' ? relation.relatedGroup._id : null;
+      });
+      expect(populatedGroupIds2).toStrictEqual([groupId3]);
+    });
+  });
 });

+ 4 - 0
apps/app/src/features/external-user-group/server/models/external-user-group-relation.ts

@@ -22,6 +22,8 @@ export interface ExternalUserGroupRelationModel extends Model<ExternalUserGroupR
   findGroupsWithDescendantsByGroupAndUser: (group: ExternalUserGroupDocument, user) => Promise<ExternalUserGroupDocument[]>,
 
   countByGroupIdsAndUser: (userGroupIds: ObjectIdLike[], userData) => Promise<number>
+
+  findAllRelationForUser: (user) => Promise<ExternalUserGroupRelationDocument[]>
 }
 
 const schema = new Schema<ExternalUserGroupRelationDocument, ExternalUserGroupRelationModel>({
@@ -47,4 +49,6 @@ schema.statics.findAllUserIdsForUserGroups = UserGroupRelation.findAllUserIdsFor
 
 schema.statics.findAllUserGroupIdsRelatedToUser = UserGroupRelation.findAllUserGroupIdsRelatedToUser;
 
+schema.statics.findAllRelationForUser = UserGroupRelation.findAllRelationForUser;
+
 export default getOrCreateModel<ExternalUserGroupRelationDocument, ExternalUserGroupRelationModel>('ExternalUserGroupRelation', schema);

+ 3 - 1
apps/app/src/server/models/user-group-relation.ts

@@ -26,6 +26,8 @@ export interface UserGroupRelationModel extends Model<UserGroupRelationDocument>
   findGroupsWithDescendantsByGroupAndUser: (group: UserGroupDocument, user) => Promise<UserGroupDocument[]>,
 
   countByGroupIdsAndUser: (userGroupIds: ObjectIdLike[], userData) => Promise<number>
+
+  findAllRelationForUser: (user) => Promise<UserGroupRelationDocument[]>
 }
 
 /*
@@ -60,7 +62,7 @@ schema.statics.removeAllInvalidRelations = function() {
 /**
    * find all user and group relation
    *
-   * @static
+   * @staticfindAllRelationForUser
    * @returns {Promise<UserGroupRelation[]>}
    * @memberof UserGroupRelation
    */

+ 1 - 0
apps/app/src/server/routes/index.js

@@ -124,6 +124,7 @@ module.exports = function(crowi, app) {
   apiV1Router.get('/search'                        , accessTokenParser , loginRequired , search.api.search);
 
   apiV1Router.get('/me/user-group-relations'  , accessTokenParser , loginRequiredStrictly , me.api.userGroupRelations);
+  apiV1Router.get('/me/external-user-group-relations'  , accessTokenParser , loginRequiredStrictly , me.api.externalUserGroupRelations);
 
   // HTTP RPC Styled API (に徐々に移行していいこうと思う)
   apiV1Router.get('/pages.list'          , accessTokenParser , loginRequired , page.api.list);

+ 14 - 0
apps/app/src/server/routes/me.js

@@ -1,3 +1,5 @@
+import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
+
 import UserGroupRelation from '../models/user-group-relation';
 
 /**
@@ -99,5 +101,17 @@ module.exports = function(crowi, app) {
       });
   };
 
+  /**
+   * retrieve external-user-group-relation documents
+   * @param {object} req
+   * @param {object} res
+   */
+  api.externalUserGroupRelations = function(req, res) {
+    ExternalUserGroupRelation.findAllRelationForUser(req.user)
+      .then((userGroupRelations) => {
+        return res.json(ApiResponse.success({ userGroupRelations }));
+      });
+  };
+
   return actions;
 };

+ 0 - 1
apps/app/src/stores/user-group.tsx

@@ -15,7 +15,6 @@ import {
 type MyUserGroupRelationsResult = {
   userGroupRelations: IUserGroupRelationHasId[],
 }
-
 export const useSWRxMyUserGroupRelations = (shouldFetch: boolean): SWRResponse<IUserGroupRelationHasId[], Error> => {
   return useSWR(
     shouldFetch ? '/me/user-group-relations' : null,