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

Merge branch 'master' into imprv/modal-for-renaming-and-duplicating

Yuki Takei 4 лет назад
Родитель
Сommit
ee2991b88c

+ 1 - 1
.github/workflows/reusable-app-prod.yml

@@ -185,7 +185,7 @@ jobs:
       fail-fast: false
       matrix:
         # List string expressions that is comma separated ids of tests in "test/cypress/integration"
-        spec-group: ['1', '2', '3']
+        spec-group: ['1', '2', '3', '4', '5', '6']
 
     services:
       mongodb:

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

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

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

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

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

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

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

@@ -21,7 +21,7 @@ import {
   IUserGroup, IUserGroupHasId,
 } from '~/interfaces/user';
 import {
-  useSWRxUserGroupPages, useSWRxUserGroupRelationList, useSWRxChildUserGroupList, useSWRxSelectableUserGroups,
+  useSWRxUserGroupPages, useSWRxUserGroupRelationList, useSWRxChildUserGroupList, useSWRxSelectableUserGroups, useSWRxAncestorUserGroups,
 } from '~/stores/user-group';
 import { useIsAclEnabled } from '~/stores/context';
 
@@ -56,6 +56,8 @@ const UserGroupDetailPage: FC = () => {
 
   const { data: selectableUserGroups, mutate: mutateSelectableUserGroups } = useSWRxSelectableUserGroups(userGroup._id);
 
+  const { data: ancestorUserGroups } = useSWRxAncestorUserGroups(userGroup._id);
+
   const { data: isAclEnabled } = useIsAclEnabled();
 
   /*
@@ -194,7 +196,33 @@ const UserGroupDetailPage: FC = () => {
         <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
         {t('admin:user_group_management.back_to_list')}
       </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">
         <UserGroupForm
           userGroup={userGroup}

+ 1 - 1
packages/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -256,7 +256,7 @@ export const PageItemControlSubstance = (props: PageItemControlSubstanceProps):
   return (
     <Dropdown isOpen={isOpen} toggle={() => setIsOpen(!isOpen)} data-testid="open-page-item-control-btn">
       { children ?? (
-        <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control">
+        <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control d-flex align-items-center justify-content-center">
           <i className="icon-options text-muted"></i>
         </DropdownToggle>
       ) }

+ 1 - 1
packages/app/src/components/PageList/PageListItemL.tsx

@@ -203,7 +203,7 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
               </div>
 
               {/* doropdown icon includes page control buttons */}
-              <div className="item-control ml-auto">
+              <div className="ml-auto">
                 <PageItemControl
                   pageId={pageData._id}
                   pageInfo={isIPageInfoForListing(pageMeta) ? pageMeta : undefined}

+ 24 - 20
packages/app/src/components/SearchPage/SearchControl.tsx

@@ -105,30 +105,34 @@ const SearchControl: FC <Props> = React.memo((props: Props) => {
           </button>
         </div>
         <div className="d-none d-lg-flex align-items-center ml-auto search-control-include-options">
-          <div className="card mr-3 mb-0">
-            <div className="card-body">
-              <label className="search-include-label mb-0 d-flex align-items-center text-secondary with-no-font-weight" htmlFor="flexCheckDefault">
-                <input
-                  className="mr-2"
-                  type="checkbox"
-                  id="flexCheckDefault"
-                  defaultChecked={includeUserPages}
-                  onChange={e => setIncludeUserPages(e.target.checked)}
-                />
+          <div className="border rounded px-2 py-1 mr-3">
+            <div className="custom-control custom-checkbox custom-checkbox-primary">
+              <input
+                className="custom-control-input mr-2"
+                type="checkbox"
+                id="flexCheckDefault"
+                defaultChecked={includeUserPages}
+                onChange={e => setIncludeUserPages(e.target.checked)}
+              />
+              <label className="custom-control-label mb-0 d-flex align-items-center text-secondary with-no-font-weight" htmlFor="flexCheckDefault">
                 {t('Include Subordinated Target Page', { target: '/user' })}
               </label>
             </div>
           </div>
-          <div className="card mb-0">
-            <div className="card-body">
-              <label className="search-include-label mb-0 d-flex align-items-center text-secondary with-no-font-weight" htmlFor="flexCheckChecked">
-                <input
-                  className="mr-2"
-                  type="checkbox"
-                  id="flexCheckChecked"
-                  defaultChecked={includeTrashPages}
-                  onChange={e => setIncludeTrashPages(e.target.checked)}
-                />
+          <div className="border rounded px-2 py-1">
+            <div className="custom-control custom-checkbox custom-checkbox-primary">
+              <input
+                className="custom-control-input mr-2"
+                type="checkbox"
+                id="flexCheckChecked"
+                checked={includeTrashPages}
+                onChange={e => setIncludeTrashPages(e.target.checked)}
+              />
+              <label
+                className="custom-control-label
+              d-flex align-items-center text-secondary with-no-font-weight"
+                htmlFor="flexCheckChecked"
+              >
                 {t('Include Subordinated Target Page', { target: '/trash' })}
               </label>
             </div>

+ 3 - 3
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -102,7 +102,7 @@ type ItemCountProps = {
 const ItemCount: FC<ItemCountProps> = (props:ItemCountProps) => {
   return (
     <>
-      <span className="grw-pagetree-count badge badge-pill badge-light text-muted">
+      <span className="grw-pagetree-count px-0 badge badge-pill badge-light text-muted">
         {props.descendantCount}
       </span>
     </>
@@ -218,7 +218,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
           if (monitor.isOver()) {
             setIsOpen(true);
           }
-        }, 1000);
+        }, 600);
       }
     },
     canDrop: (item) => {
@@ -414,7 +414,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
               className={`grw-pagetree-button btn ${isOpen ? 'grw-pagetree-open' : ''}`}
               onClick={onClickLoadChildren}
             >
-              <div className="grw-triangle-icon d-flex justify-content-center">
+              <div className="d-flex justify-content-center">
                 <TriangleIcon />
               </div>
             </button>

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

@@ -21,3 +21,7 @@ export type UserGroupPagesResult = {
 export type SelectableUserGroupsResult = {
   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('includeGrandChildren', 'parentIds must be boolean').optional().isBoolean(),
     ],
+    ancestorGroup: [
+      query('groupId', 'groupId must be a string').optional().isString(),
+    ],
     selectableGroups: [
       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
   router.get('/children', loginRequiredStrictly, adminRequired, validator.listChildren, async(req, res) => {
     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 { IUserGroupHasId, IUserGroupRelationHasId } from '~/interfaces/user';
 import {
-  UserGroupListResult, ChildUserGroupListResult, UserGroupRelationListResult, UserGroupPagesResult, SelectableUserGroupsResult,
+  UserGroupListResult, ChildUserGroupListResult, UserGroupRelationListResult, UserGroupPagesResult, SelectableUserGroupsResult, AncestorUserGroupsResult,
 } 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),
   );
 };
+
+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),
+  );
+};

+ 3 - 1
packages/app/src/styles/_page-tree.scss

@@ -50,7 +50,9 @@ $grw-pagetree-item-padding-left: 10px;
       }
 
       .grw-pagetree-count {
-        padding: 0.1rem 0.3rem;
+        width: 26px;
+        padding: 0.1rem 0;
+        font-size: 12px;
       }
     }
   }

+ 6 - 0
packages/app/src/styles/_page_list.scss

@@ -4,6 +4,12 @@ body .page-list {
     line-height: 1.6em;
   }
 
+  .btn-page-item-control {
+    width: 20px;
+    height: 20px;
+    padding: 0px;
+  }
+
   .page-list-ul {
     padding-left: 0;
     margin: 0;

+ 0 - 1
packages/app/src/styles/_subnav.scss

@@ -131,7 +131,6 @@
     .btn-page-item-control {
       width: 32px;
       height: 32px;
-      padding: 4px;
       font-size: 12px;
     }
   }

+ 33 - 0
packages/app/test/cypress/integration/5-switch-sidebar-mode/switching-sidebar-mode.spec.ts

@@ -0,0 +1,33 @@
+context('Switch sidebar mode', () => {
+  const ssPrefix = 'switch-sidebar-mode-';
+
+  let connectSid: string | undefined;
+
+  before(() => {
+    // login
+    cy.fixture("user-admin.json").then(user => {
+      cy.login(user.username, user.password);
+    });
+    cy.getCookie('connect.sid').then(cookie => {
+      connectSid = cookie?.value;
+    });
+  });
+
+  beforeEach(() => {
+    if (connectSid != null) {
+      cy.setCookie('connect.sid', connectSid);
+    }
+  });
+
+  it('Switching sidebar mode', () => {
+    cy.visit('/');
+    cy.get('.grw-personal-dropdown').click();
+
+    cy.get('[for="swSidebarModeOnEditor"]').click();
+    cy.screenshot(`${ssPrefix}-switch-sidebar-mode`, { capture: 'viewport' });
+
+    cy.get('[for="swSidebarModeOnEditor"]').click();
+    cy.screenshot(`${ssPrefix}-switch-sidebar-mode-back`, { capture: 'viewport' });
+  });
+
+});

+ 32 - 0
packages/app/test/cypress/integration/6-home-settings/access-home.spec.ts

@@ -0,0 +1,32 @@
+/* eslint-disable cypress/no-unnecessary-waiting */
+context('Access Home', () => {
+  const ssPrefix = 'access-home-';
+
+  let connectSid: string | undefined;
+
+  before(() => {
+    // login
+    cy.fixture("user-admin.json").then(user => {
+      cy.login(user.username, user.password);
+    });
+    cy.getCookie('connect.sid').then(cookie => {
+      connectSid = cookie?.value;
+    });
+  });
+
+  beforeEach(() => {
+    if (connectSid != null) {
+      cy.setCookie('connect.sid', connectSid);
+    }
+  });
+
+  it('Visit home', () => {
+    cy.visit('/');
+    cy.get('.grw-personal-dropdown').click();
+    cy.get('.grw-personal-dropdown .dropdown-menu .btn-group > .btn-outline-secondary:eq(0)').click();
+
+    cy.wait(1500);
+    cy.screenshot(`${ssPrefix}-visit-home`, { capture: 'viewport' });
+  });
+
+});

+ 85 - 0
packages/app/test/cypress/integration/6-home-settings/access-user-settings.spec.ts

@@ -0,0 +1,85 @@
+/* eslint-disable cypress/no-unnecessary-waiting */
+context('Access User settings', () => {
+  const ssPrefix = 'access-user-settings-';
+
+  let connectSid: string | undefined;
+
+  before(() => {
+    // login
+    cy.fixture("user-admin.json").then(user => {
+      cy.login(user.username, user.password);
+    });
+    cy.getCookie('connect.sid').then(cookie => {
+      connectSid = cookie?.value;
+    });
+
+    cy.visit('/');
+    cy.get('.grw-personal-dropdown').click();
+    cy.get('[href="/me"]').click();
+
+    cy.wait(1500);
+  });
+
+  beforeEach(() => {
+    if (connectSid != null) {
+      cy.setCookie('connect.sid', connectSid);
+    }
+  });
+
+  it('Update settings', () => {
+    // Access User information
+    cy.get('#personal-setting .tab-pane.active > div:first button').click(); // Click basic info update button
+
+    cy.wait(500);
+
+    cy.screenshot(`${ssPrefix}-user-information`, { capture: 'viewport' });
+    cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
+
+    // Access External account
+    cy.get('#personal-setting .nav-title.nav li:eq(1) a').click(); // click
+    cy.get('#personal-setting .tab-pane.active h2 button').click(); // click add button
+    cy.get('.modal-footer button').click(); // click add button in modal form
+    cy.get('.close[aria-label="Close"]').click(); // close modal form
+
+    cy.wait(500);
+
+    cy.screenshot(`${ssPrefix}-external-account`, { capture: 'viewport' });
+    cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
+
+    // Access Password setting
+    cy.get('#personal-setting .nav-title.nav li:eq(2) a').click();
+    cy.get('#personal-setting .tab-pane.active button').click(); // click update button
+
+    cy.wait(500);
+
+    cy.screenshot(`${ssPrefix}-password-setting`, { capture: 'viewport' });
+    cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
+
+    // Access API setting
+    cy.get('#personal-setting .nav-title.nav li:eq(2) a').click();
+    cy.get('#personal-setting .tab-pane.active button').click(); // click update API token button
+
+    cy.wait(500);
+
+    cy.screenshot(`${ssPrefix}-api-setting`, { capture: 'viewport' });
+    cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
+
+    // Access Editor setting
+    cy.get('#personal-setting .nav-title.nav li:eq(3) a').click();
+    cy.get('#personal-setting .tab-pane.active button').click(); // click update button
+
+    cy.wait(500);
+
+    cy.screenshot(`${ssPrefix}-editor-setting`, { capture: 'viewport' });
+    cy.get('.toast-close-button').click({ multiple: true }); // close toast alert
+
+    // Access In-app notification setting
+    cy.get('#personal-setting .nav-title.nav li:eq(4) a').click();
+    cy.get('#personal-setting .tab-pane.active button').click(); // click update button
+
+    cy.wait(500);
+
+    cy.screenshot(`${ssPrefix}-in-app-notification-setting`, { capture: 'viewport' });
+  });
+
+});