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

Merge pull request #5480 from weseek/feat/85240-link-back-to-ancestor-group

feat: Link back to ancestor group
Yuki Takei 4 лет назад
Родитель
Сommit
54eccbc5d7

+ 1 - 0
packages/app/resource/locales/en_US/admin/admin.json

@@ -469,6 +469,7 @@
     "group_list": "Group list",
     "group_list": "Group list",
     "child_group_list": "Child group list",
     "child_group_list": "Child group list",
     "back_to_list": "Go back to group list",
     "back_to_list": "Go back to group list",
+    "back_to_ancestors_group": "Go back to ancestors group",
     "basic_info": "Basic info",
     "basic_info": "Basic info",
     "user_list": "User list",
     "user_list": "User list",
     "created_group": "Group was created",
     "created_group": "Group was created",

+ 1 - 0
packages/app/resource/locales/ja_JP/admin/admin.json

@@ -468,6 +468,7 @@
     "group_list": "グループ一覧",
     "group_list": "グループ一覧",
     "child_group_list": "子グループ一覧",
     "child_group_list": "子グループ一覧",
     "back_to_list": "グループ一覧に戻る",
     "back_to_list": "グループ一覧に戻る",
+    "back_to_ancestors_group": "祖先グループに戻る",
     "basic_info": "基本情報",
     "basic_info": "基本情報",
     "user_list": "ユーザー一覧",
     "user_list": "ユーザー一覧",
     "created_group": "グループを作成しました",
     "created_group": "グループを作成しました",

+ 1 - 0
packages/app/resource/locales/zh_CN/admin/admin.json

@@ -478,6 +478,7 @@
     "group_list": "组列表",
     "group_list": "组列表",
     "child_group_list": "儿童组名单",
     "child_group_list": "儿童组名单",
     "back_to_list": "返回组列表",
     "back_to_list": "返回组列表",
+    "back_to_ancestors_group": "返回到祖先组",
     "basic_info": "基本信息",
     "basic_info": "基本信息",
     "user_list": "用户列表",
     "user_list": "用户列表",
     "created_group": "已创建组",
     "created_group": "已创建组",

+ 30 - 2
packages/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx

@@ -21,7 +21,7 @@ import {
   IUserGroup, IUserGroupHasId,
   IUserGroup, IUserGroupHasId,
 } from '~/interfaces/user';
 } from '~/interfaces/user';
 import {
 import {
-  useSWRxUserGroupPages, useSWRxUserGroupRelationList, useSWRxChildUserGroupList, useSWRxSelectableUserGroups,
+  useSWRxUserGroupPages, useSWRxUserGroupRelationList, useSWRxChildUserGroupList, useSWRxSelectableUserGroups, useSWRxAncestorUserGroups,
 } from '~/stores/user-group';
 } from '~/stores/user-group';
 import { useIsAclEnabled } from '~/stores/context';
 import { useIsAclEnabled } from '~/stores/context';
 
 
@@ -56,6 +56,8 @@ const UserGroupDetailPage: FC = () => {
 
 
   const { data: selectableUserGroups, mutate: mutateSelectableUserGroups } = useSWRxSelectableUserGroups(userGroup._id);
   const { data: selectableUserGroups, mutate: mutateSelectableUserGroups } = useSWRxSelectableUserGroups(userGroup._id);
 
 
+  const { data: ancestorUserGroups } = useSWRxAncestorUserGroups(userGroup._id);
+
   const { data: isAclEnabled } = useIsAclEnabled();
   const { data: isAclEnabled } = useIsAclEnabled();
 
 
   /*
   /*
@@ -194,7 +196,33 @@ const UserGroupDetailPage: FC = () => {
         <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
         <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
         {t('admin:user_group_management.back_to_list')}
         {t('admin:user_group_management.back_to_list')}
       </a>
       </a>
-      {/* TODO 85062: Link to the ancestors group */}
+
+      {
+        userGroup?.parent != null && ancestorUserGroups != null && ancestorUserGroups.length > 0 && (
+          <div className="btn-group ml-2">
+            <a className="btn btn-outline-secondary" href={`/admin/user-group-detail/${userGroup.parent}`}>
+              <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
+              {t('admin:user_group_management.back_to_ancestors_group')}
+            </a>
+            <button
+              type="button"
+              className="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split"
+              data-toggle="dropdown"
+              aria-haspopup="true"
+              ria-expanded="false"
+            >
+            </button>
+            <div className="dropdown-menu">
+              {
+                ancestorUserGroups.map(userGroup => (
+                  <a className="dropdown-item" key={userGroup._id} href={`/admin/user-group-detail/${userGroup._id}`}>{userGroup.name}</a>
+                ))
+              }
+            </div>
+          </div>
+        )
+      }
+
       <div className="mt-4 form-box">
       <div className="mt-4 form-box">
         <UserGroupForm
         <UserGroupForm
           userGroup={userGroup}
           userGroup={userGroup}

+ 4 - 0
packages/app/src/interfaces/user-group-response.ts

@@ -21,3 +21,7 @@ export type UserGroupPagesResult = {
 export type SelectableUserGroupsResult = {
 export type SelectableUserGroupsResult = {
   selectableUserGroups: IUserGroupHasId[],
   selectableUserGroups: IUserGroupHasId[],
 }
 }
+
+export type AncestorUserGroupsResult = {
+  ancestorUserGroups: IUserGroupHasId[],
+}

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

@@ -61,6 +61,9 @@ module.exports = (crowi) => {
       query('parentIds', 'parentIds must be an array').optional().isArray(),
       query('parentIds', 'parentIds must be an array').optional().isArray(),
       query('includeGrandChildren', 'parentIds must be boolean').optional().isBoolean(),
       query('includeGrandChildren', 'parentIds must be boolean').optional().isBoolean(),
     ],
     ],
+    ancestorGroup: [
+      query('groupId', 'groupId must be a string').optional().isString(),
+    ],
     selectableGroups: [
     selectableGroups: [
       query('groupId', 'groupId must be a string').optional().isString(),
       query('groupId', 'groupId must be a string').optional().isString(),
     ],
     ],
@@ -127,6 +130,51 @@ module.exports = (crowi) => {
     }
     }
   });
   });
 
 
+  /**
+   * @swagger
+   *
+   *  paths:
+   *    /ancestors:
+   *      get:
+   *        tags: [UserGroup]
+   *        operationId: getAncestorUserGroups
+   *        summary: /ancestors
+   *        description: Get ancestor user groups.
+   *        parameters:
+   *          - name: groupId
+   *            in: query
+   *            required: true
+   *            description: id of userGroup
+   *            schema:
+   *              type: string
+   *        responses:
+   *          200:
+   *            description: userGroups are fetched
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    userGroups:
+   *                      type: array
+   *                      items:
+   *                        type: object
+   *                      description: userGroup objects
+   */
+  router.get('/ancestors', loginRequiredStrictly, adminRequired, validator.ancestorGroup, async(req, res) => {
+    const { groupId } = req.query;
+
+    try {
+      const userGroup = await UserGroup.findById(groupId);
+      const ancestorUserGroups = await UserGroup.findGroupsWithAncestorsRecursively(userGroup, []);
+      return res.apiv3({ ancestorUserGroups });
+    }
+    catch (err) {
+      const msg = 'Error occurred while searching user groups';
+      logger.error(msg, err);
+      return res.apiv3Err(new ErrorV3(msg, 'user-groups-search-failed'));
+    }
+  });
+
   // TODO 85062: improve sort
   // TODO 85062: improve sort
   router.get('/children', loginRequiredStrictly, adminRequired, validator.listChildren, async(req, res) => {
   router.get('/children', loginRequiredStrictly, adminRequired, validator.listChildren, async(req, res) => {
     try {
     try {

+ 8 - 1
packages/app/src/stores/user-group.tsx

@@ -6,7 +6,7 @@ import { apiv3Get } from '~/client/util/apiv3-client';
 import { IPageHasId } from '~/interfaces/page';
 import { IPageHasId } from '~/interfaces/page';
 import { IUserGroupHasId, IUserGroupRelationHasId } from '~/interfaces/user';
 import { IUserGroupHasId, IUserGroupRelationHasId } from '~/interfaces/user';
 import {
 import {
-  UserGroupListResult, ChildUserGroupListResult, UserGroupRelationListResult, UserGroupPagesResult, SelectableUserGroupsResult,
+  UserGroupListResult, ChildUserGroupListResult, UserGroupRelationListResult, UserGroupPagesResult, SelectableUserGroupsResult, AncestorUserGroupsResult,
 } from '~/interfaces/user-group-response';
 } from '~/interfaces/user-group-response';
 
 
 
 
@@ -66,3 +66,10 @@ export const useSWRxSelectableUserGroups = (groupId: string | undefined): SWRRes
     endpoint => apiv3Get<SelectableUserGroupsResult>(endpoint, { groupId }).then(result => result.data.selectableUserGroups),
     endpoint => apiv3Get<SelectableUserGroupsResult>(endpoint, { groupId }).then(result => result.data.selectableUserGroups),
   );
   );
 };
 };
+
+export const useSWRxAncestorUserGroups = (groupId: string | undefined): SWRResponse<IUserGroupHasId[], Error> => {
+  return useSWRImmutable(
+    groupId != null ? ['/user-groups/ancestors'] : null,
+    endpoint => apiv3Get<AncestorUserGroupsResult>(endpoint, { groupId }).then(result => result.data.ancestorUserGroups),
+  );
+};