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

Merge branch 'master' into fix/87983-88132-parent-empty-page-link

NEEDLEMAN3\tatsu 4 лет назад
Родитель
Сommit
dbd535b4dd
44 измененных файлов с 328 добавлено и 177 удалено
  1. 6 3
      .github/workflows/ci-app.yml
  2. 6 3
      .github/workflows/reusable-app-prod.yml
  3. 3 2
      .github/workflows/reusable-app-reg-suit.yml
  4. 1 1
      lerna.json
  5. 1 1
      package.json
  6. 7 7
      packages/app/package.json
  7. 3 3
      packages/app/resource/locales/en_US/translation.json
  8. 3 3
      packages/app/resource/locales/ja_JP/translation.json
  9. 3 3
      packages/app/resource/locales/zh_CN/translation.json
  10. 2 1
      packages/app/src/components/Admin/ElasticsearchManagement/StatusTable.jsx
  11. 1 1
      packages/app/src/components/Admin/Notification/NotificationSetting.jsx
  12. 4 4
      packages/app/src/components/Common/Dropdown/PageItemControl.tsx
  13. 5 2
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  14. 2 3
      packages/app/src/components/PageDuplicateModal.jsx
  15. 11 2
      packages/app/src/components/PageList/PageListItemL.tsx
  16. 1 1
      packages/app/src/components/PageRenameModal.jsx
  17. 1 1
      packages/app/src/components/Sidebar.tsx
  18. 3 2
      packages/app/src/components/Sidebar/PageTree/Item.tsx
  19. 4 1
      packages/app/src/components/Sidebar/SidebarNav.tsx
  20. 2 2
      packages/app/src/components/Sidebar/Tag.tsx
  21. 26 13
      packages/app/src/server/models/page.ts
  22. 29 8
      packages/app/src/server/routes/apiv3/bookmarks.js
  23. 2 2
      packages/app/src/server/routes/apiv3/pages.js
  24. 21 9
      packages/app/src/server/service/page.ts
  25. 4 1
      packages/app/src/stores/page.tsx
  26. 68 79
      packages/app/src/styles/_sidebar.scss
  27. 4 0
      packages/app/test/cypress/integration/2-basic-features/access-to-admin-page.spec.ts
  28. 2 0
      packages/app/test/cypress/integration/2-basic-features/access-to-me-page.spec.ts
  29. 2 6
      packages/app/test/cypress/integration/2-basic-features/access-to-page.spec.ts
  30. 9 0
      packages/app/test/cypress/integration/2-basic-features/access-to-special-page.spec.ts
  31. 2 0
      packages/app/test/cypress/integration/2-basic-features/open-page-create-modal.spec.ts
  32. 61 0
      packages/app/test/cypress/integration/2-basic-features/open-presentation-modal.spec.ts
  33. 2 0
      packages/app/test/cypress/integration/3-search/access-to-private-legacy-pages-directly.spec.ts
  34. 4 2
      packages/app/test/cypress/integration/3-search/access-to-result-page-directly.spec.ts
  35. 11 1
      packages/app/test/cypress/support/commands.ts
  36. 4 2
      packages/app/test/integration/global-setup.js
  37. 1 1
      packages/codemirror-textlint/package.json
  38. 1 1
      packages/core/package.json
  39. 1 1
      packages/plugin-attachment-refs/package.json
  40. 1 1
      packages/plugin-lsx/package.json
  41. 1 1
      packages/plugin-pukiwiki-like-linker/package.json
  42. 1 1
      packages/slack/package.json
  43. 1 1
      packages/slackbot-proxy/package.json
  44. 1 1
      packages/ui/package.json

+ 6 - 3
.github/workflows/ci-app.yml

@@ -31,8 +31,9 @@ jobs:
         with:
           path: |
             **/node_modules
-          key: node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
+          key: node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('packages/app/package.json') }}
           restore-keys: |
+            node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-
             node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-
 
       - name: lerna bootstrap
@@ -84,8 +85,9 @@ jobs:
         with:
           path: |
             **/node_modules
-          key: node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
+          key: node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('packages/app/package.json') }}
           restore-keys: |
+            node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-
             node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-
 
       - name: lerna bootstrap
@@ -143,8 +145,9 @@ jobs:
         with:
           path: |
             **/node_modules
-          key: node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}
+          key: node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('packages/app/package.json') }}
           restore-keys: |
+            node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-
             node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-
 
       - name: lerna bootstrap

+ 6 - 3
.github/workflows/reusable-app-prod.yml

@@ -37,9 +37,10 @@ jobs:
       with:
         path: |
           **/node_modules
-        key: node_modules-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
+        key: node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('packages/app/package.json') }}
         restore-keys: |
-          node_modules-${{ runner.OS }}-node${{ inputs.node-version }}-
+          node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-
+          node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-
 
     - name: lerna bootstrap
       run: |
@@ -124,6 +125,7 @@ jobs:
           **/node_modules
         key: node_modules-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ steps.get-date.outputs.dateYmdHM }}
         restore-keys: |
+          node_modules-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('packages/app/package.json') }}
           node_modules-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
           node_modules-${{ runner.OS }}-node${{ inputs.node-version }}-
           node_modules-build-prod-${{ runner.OS }}-node${{ inputs.node-version }}-${{ steps.get-date.outputs.dateYm }}
@@ -212,8 +214,9 @@ jobs:
           **/node_modules
           ~/.cache/Cypress
           ${{ steps.yarn-cache-dir.outputs.value }}
-        key: deps-for-cypress-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
+        key: deps-for-cypress-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('packages/app/package.json') }}
         restore-keys: |
+          deps-for-cypress-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}-
           deps-for-cypress-${{ runner.OS }}-node${{ inputs.node-version }}
 
     - name: lerna bootstrap

+ 3 - 2
.github/workflows/reusable-app-reg-suit.yml

@@ -61,9 +61,10 @@ jobs:
       with:
         path: |
           **/node_modules
-        key: node_modules-${{ runner.OS }}-node${{ inputs.node-version }}-${{ hashFiles('**/yarn.lock') }}
+        key: node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('packages/app/package.json') }}
         restore-keys: |
-          node_modules-${{ runner.OS }}-node${{ inputs.node-version }}-
+          node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-
+          node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-
 
     - name: lerna bootstrap
       run: |

+ 1 - 1
lerna.json

@@ -1,7 +1,7 @@
 {
   "npmClient": "yarn",
   "useWorkspaces": true,
-  "version": "5.0.0-RC.7",
+  "version": "5.0.0-RC.8",
   "packages": [
     "packages/*"
   ]

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "5.0.0-RC.7",
+  "version": "5.0.0-RC.8",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

+ 7 - 7
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "5.0.0-RC.7",
+  "version": "5.0.0-RC.8",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -60,11 +60,11 @@
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^5.0.0-RC.7",
-    "@growi/plugin-attachment-refs": "^5.0.0-RC.7",
-    "@growi/plugin-lsx": "^5.0.0-RC.7",
-    "@growi/plugin-pukiwiki-like-linker": "^5.0.0-RC.7",
-    "@growi/slack": "^5.0.0-RC.7",
+    "@growi/codemirror-textlint": "^5.0.0-RC.8",
+    "@growi/plugin-attachment-refs": "^5.0.0-RC.8",
+    "@growi/plugin-lsx": "^5.0.0-RC.8",
+    "@growi/plugin-pukiwiki-like-linker": "^5.0.0-RC.8",
+    "@growi/slack": "^5.0.0-RC.8",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
     "@slack/events-api": "^3.0.0",
@@ -167,7 +167,7 @@
   },
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.1.4",
-    "@growi/ui": "^5.0.0-RC.7",
+    "@growi/ui": "^5.0.0-RC.8",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",

+ 3 - 3
packages/app/resource/locales/en_US/translation.json

@@ -416,8 +416,8 @@
     "label": {
       "Move/Rename page": "Move/Rename page",
       "New page name": "New page name",
-      "Fail to get subordinated pages": "Fail to get subordinated pages",
-      "Fail to get exist path": "Fail to get exist path",
+      "Failed to get subordinated pages": "Failed to get subordinated pages",
+      "Failed to get exist path": "Failed to get exist path",
       "Rename without exist path": "Rename without exist path",
       "Current page name": "Current page name",
       "Recursively": "Recursively",
@@ -451,7 +451,7 @@
     "label": {
       "Duplicate page": "Duplicate page",
       "New page name": "New page name",
-      "Fail to get subordinated pages": "Fail to get subordinated pages",
+      "Failed to get subordinated pages": "Failed to get subordinated pages",
       "Current page name": "Current page name",
       "Recursively": "Recursively",
       "Duplicate without exist path": "Duplicate without exist path",

+ 3 - 3
packages/app/resource/locales/ja_JP/translation.json

@@ -415,8 +415,8 @@
     "label": {
       "Move/Rename page": "ページを移動/名前変更する",
       "New page name": "移動先のページ名",
-      "Fail to get subordinated pages": "配下ページの取得に失敗しました",
-      "Fail to get exist path": "存在するパスの取得に失敗しました",
+      "Failed to get subordinated pages": "配下ページの取得に失敗しました",
+      "Failed to get exist path": "存在するパスの取得に失敗しました",
       "Rename without exist path": "存在するパス以外を名前変更する",
       "Current page name": "現在のページ名",
       "Recursively": "再帰的に移動/名前変更",
@@ -450,7 +450,7 @@
     "label": {
       "Duplicate page": "ページを複製する",
       "New page name": "複製後のページ名",
-      "Fail to get subordinated pages": "配下ページの取得に失敗しました",
+      "Failed to get subordinated pages": "配下ページの取得に失敗しました",
       "Current page name": "現在のページ名",
       "Recursively": "再帰的に複製",
       "Duplicate without exist path": "存在するパス以外を複製する",

+ 3 - 3
packages/app/resource/locales/zh_CN/translation.json

@@ -394,8 +394,8 @@
 		"label": {
 			"Move/Rename page": "页面 移动/重命名",
       "New page name": "新建页面名称",
-      "Fail to get subordinated pages": "Fail to get subordinated pages",
-      "Fail to get exist path": "Fail to get exist path",
+      "Failed to get subordinated pages": "Failed to get subordinated pages",
+      "Failed to get exist path": "Failed to get exist path",
       "Rename without exist path": "Rename without exist path",
 			"Current page name": "当前页面名称",
 			"Recursively": "递归地",
@@ -427,7 +427,7 @@
 		"label": {
 			"Duplicate page": "Duplicate page",
       "New page name": "New page name",
-      "Fail to get subordinated pages": "Fail to get subordinated pages",
+      "Failed to get subordinated pages": "Failed to get subordinated pages",
 			"Current page name": "Current page name",
       "Recursively": "Recursively",
       "Duplicate without exist path": "Duplicate without exist path",

+ 2 - 1
packages/app/src/components/Admin/ElasticsearchManagement/StatusTable.jsx

@@ -27,7 +27,8 @@ class StatusTable extends React.PureComponent {
     }
     else {
       connectionStatusLabel = isConnected
-        ? <span className="badge badge-pill badge-success">{ t('full_text_search_management.connection_status_label_connected') }</span>
+        // eslint-disable-next-line max-len
+        ? <span data-testid="connection-status-badge-connected" className="badge badge-pill badge-success">{ t('full_text_search_management.connection_status_label_connected') }</span>
         : <span className="badge badge-pill badge-danger">{ t('full_text_search_management.connection_status_label_disconnected') }</span>;
     }
 

+ 1 - 1
packages/app/src/components/Admin/Notification/NotificationSetting.jsx

@@ -54,7 +54,7 @@ const SlackIntegrationListItem = ({ isEnabled, currentBotType }) => {
   const isCautionVisible = currentBotType === SlackbotType.OFFICIAL || currentBotType === SlackbotType.CUSTOM_WITH_PROXY;
 
   return (
-    <li className="list-group-item">
+    <li data-testid="slack-integration-list-item" className="list-group-item">
       <h4>
         <Badge isEnabled={isEnabled} />
         <a href="/admin/slack-integration" className="ml-2">{t('slack_integration')}</a>

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

@@ -88,7 +88,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
       return;
     }
     await onClickRevertMenuItem(pageId);
-  }, [onClickRevertMenuItem]);
+  }, [onClickRevertMenuItem, pageId]);
 
 
   // eslint-disable-next-line react-hooks/rules-of-hooks
@@ -212,7 +212,8 @@ export const PageItemControlSubstance = (props: PageItemControlSubstanceProps):
   const shouldFetch = fetchOnInit === true || (!isIPageInfoForOperation(presetPageInfo) && isOpen);
   const shouldMutate = fetchOnInit === true || !isIPageInfoForOperation(presetPageInfo);
 
-  const { data: fetchedPageInfo, mutate: mutatePageInfo } = useSWRxPageInfo(shouldFetch ? pageId : null);
+  const { data: fetchedPageInfo } = useSWRxPageInfo(shouldFetch ? pageId : null);
+  const { mutate: mutatePageInfo } = useSWRxPageInfo(shouldMutate ? pageId : null);
 
   // mutate after handle event
   const bookmarkMenuItemClickHandler = useCallback(async(_pageId: string, _newValue: boolean) => {
@@ -249,8 +250,7 @@ export const PageItemControlSubstance = (props: PageItemControlSubstanceProps):
   }, [onClickDeleteMenuItem, pageId, fetchedPageInfo, presetPageInfo]);
 
   return (
-    <Dropdown isOpen={isOpen} toggle={() => setIsOpen(!isOpen)}>
-
+    <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">
           <i className="icon-options text-muted"></i>

+ 5 - 2
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -67,12 +67,15 @@ const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
   const { open: openPresentationModal } = usePagePresentationModal();
   const { open: openAccessoriesModal } = usePageAccessoriesModal();
 
-  const hrefForPresentationModal = '?presentation=1';
+  const hrefForPresentationModal = `${pageId}/?presentation=1`;
 
   return (
     <>
       {/* Presentation */}
-      <DropdownItem onClick={() => openPresentationModal(hrefForPresentationModal)}>
+      <DropdownItem
+        onClick={() => openPresentationModal(hrefForPresentationModal)}
+        data-testid="open-presentation-modal-btn"
+      >
         <i className="icon-fw"><PresentationIcon /></i>
         { t('Presentation Mode') }
       </DropdownItem>

+ 2 - 3
packages/app/src/components/PageDuplicateModal.jsx

@@ -89,12 +89,11 @@ const PageDuplicateModal = (props) => {
   const getSubordinatedList = useCallback(async() => {
     try {
       const res = await appContainer.apiv3Get('/pages/subordinated-list', { path, limit: LIMIT_FOR_LIST });
-      const { subordinatedPaths } = res.data;
-      setSubordinatedPages(subordinatedPaths);
+      setSubordinatedPages(res.data.subordinatedPages);
     }
     catch (err) {
       setErrs(err);
-      toastError(t('modal_duplicate.label.Fail to get subordinated pages'));
+      toastError(t('modal_duplicate.label.Failed to get subordinated pages'));
     }
   }, [appContainer, path, t]);
 

+ 11 - 2
packages/app/src/components/PageList/PageListItemL.tsx

@@ -11,6 +11,10 @@ import urljoin from 'url-join';
 
 import { UserPicture, PageListMeta } from '@growi/ui';
 import { DevidedPagePath } from '@growi/core';
+
+
+import { ISelectable } from '~/client/interfaces/selectable-all';
+import { bookmark, unbookmark } from '~/client/services/page-operation';
 import { useIsDeviceSmallerThanLg } from '~/stores/ui';
 import {
   usePageRenameModal, usePageDuplicateModal, usePageDeleteModal, usePutBackPageModal,
@@ -20,11 +24,10 @@ import {
 } from '~/interfaces/page';
 import { IPageSearchMeta, isIPageSearchMeta } from '~/interfaces/search';
 import { OnDeletedFunction } from '~/interfaces/ui';
+import LinkedPagePath from '~/models/linked-page-path';
 
 import { ForceHideMenuItems, PageItemControl } from '../Common/Dropdown/PageItemControl';
-import LinkedPagePath from '~/models/linked-page-path';
 import PagePathHierarchicalLink from '../PagePathHierarchicalLink';
-import { ISelectable } from '~/client/interfaces/selectable-all';
 
 type Props = {
   page: IPageWithMeta | IPageWithMeta<IPageInfoAll & IPageSearchMeta>,
@@ -92,6 +95,11 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
     }
   }, [isDeviceSmallerThanLg, onClickItem, pageData._id]);
 
+  const bookmarkMenuItemClickHandler = async(_pageId: string, _newValue: boolean): Promise<void> => {
+    const bookmarkOperation = _newValue ? bookmark : unbookmark;
+    await bookmarkOperation(_pageId);
+  };
+
   const duplicateMenuItemClickHandler = useCallback(() => {
     const page = {
       pageId: pageData._id,
@@ -206,6 +214,7 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
                   pageInfo={pageMeta}
                   isEnableActions={isEnableActions}
                   forceHideMenuItems={forceHideMenuItems}
+                  onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
                   onClickRenameMenuItem={renameMenuItemClickHandler}
                   onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
                   onClickDeleteMenuItem={deleteMenuItemClickHandler}

+ 1 - 1
packages/app/src/components/PageRenameModal.jsx

@@ -70,7 +70,7 @@ const PageRenameModal = (props) => {
     }
     catch (err) {
       setErrs(err);
-      toastError(t('modal_rename.label.Fail to get subordinated pages'));
+      toastError(t('modal_rename.label.Failed to get subordinated pages'));
     }
   }, [path, t]);
 

+ 1 - 1
packages/app/src/components/Sidebar.tsx

@@ -72,7 +72,7 @@ const SidebarContentsWrapper = () => {
       />
 
       <div id="grw-sidebar-contents-scroll-target" style={{ minHeight: '100%' }}>
-        <div id="grw-sidebar-content-container" onLoad={() => setResetKey(Math.random())}>
+        <div id="grw-sidebar-content-container" className="grw-sidebar-content-container" onLoad={() => setResetKey(Math.random())}>
           <SidebarContents />
         </div>
       </div>

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

@@ -355,7 +355,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
     <div id={`pagetree-item-${page._id}`} className={`grw-pagetree-item-container ${isOver ? 'grw-pagetree-is-over' : ''} ${shouldHide ? 'd-none' : ''}`}>
       <li
         ref={(c) => { drag(c); drop(c) }}
-        className={`list-group-item list-group-item-action border-0 py-1 d-flex align-items-center ${page.isTarget ? 'grw-pagetree-is-target' : ''}`}
+        className={`list-group-item list-group-item-action border-0 py-0 d-flex align-items-center ${page.isTarget ? 'grw-pagetree-is-target' : ''}`}
         id={page.isTarget ? 'grw-pagetree-is-target' : `grw-pagetree-list-${page._id}`}
       >
         <div className="grw-triangle-container d-flex justify-content-center">
@@ -401,13 +401,14 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
             onClickRenameMenuItem={renameMenuItemClickHandler}
             onClickDeleteMenuItem={deleteMenuItemClickHandler}
           >
+            {/* pass the color property to reactstrap dropdownToggle props. https://6-4-0--reactstrap.netlify.app/components/dropdowns/  */}
             <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover">
               <i className="icon-options fa fa-rotate-90 text-muted p-1"></i>
             </DropdownToggle>
           </PageItemControl>
           <button
             type="button"
-            className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover"
+            className="border-0 rounded btn btn-page-item-control p-0 grw-visible-on-hover"
             onClick={onClickPlusButton}
           >
             <i className="icon-plus text-muted d-block p-1" />

+ 4 - 1
packages/app/src/components/Sidebar/SidebarNav.tsx

@@ -15,7 +15,7 @@ type PrimaryItemProps = {
 
 const PrimaryItem: FC<PrimaryItemProps> = (props: PrimaryItemProps) => {
   const {
-    contents, iconName, onItemSelected,
+    contents, label, iconName, onItemSelected,
   } = props;
 
   const { data: currentContents, mutate } = useCurrentSidebarContents();
@@ -31,9 +31,12 @@ const PrimaryItem: FC<PrimaryItemProps> = (props: PrimaryItemProps) => {
     scheduleToPutUserUISettings({ currentSidebarContents: contents });
   }, [contents, mutate, onItemSelected]);
 
+  const labelForTestId = label.toLowerCase().replace(' ', '-');
+
   return (
     <button
       type="button"
+      data-testid={`grw-sidebar-nav-primary-${labelForTestId}`}
       className={`d-block btn btn-primary ${isSelected ? 'active' : ''}`}
       onClick={itemSelectedHandler}
     >

+ 2 - 2
packages/app/src/components/Sidebar/Tag.tsx

@@ -11,7 +11,7 @@ const Tag: FC = () => {
   }, [isOnReload]);
 
   return (
-    <>
+    <div data-testid="grw-sidebar-content-tags">
       <div className="grw-sidebar-content-header p-3 d-flex">
         <h3 className="mb-0">{t('Tags')}</h3>
         <button
@@ -36,7 +36,7 @@ const Tag: FC = () => {
       <div className="grw-container-convertible mb-5 pb-5">
         <TagsList isOnReload={isOnReload} />
       </div>
-    </>
+    </div>
   );
 
 };

+ 26 - 13
packages/app/src/server/models/page.ts

@@ -43,7 +43,7 @@ type TargetAndAncestorsResult = {
 export type CreateMethod = (path: string, body: string, user, options) => Promise<PageDocument & { _id: any }>
 export interface PageModel extends Model<PageDocument> {
   [x: string]: any; // for obsolete methods
-  createEmptyPagesByPaths(paths: string[], publicOnly?: boolean): Promise<void>
+  createEmptyPagesByPaths(paths: string[], onlyMigratedAsExistingPages?: boolean, publicOnly?: boolean): Promise<void>
   getParentAndFillAncestors(path: string): Promise<PageDocument & { _id: any }>
   findByIdsAndViewer(pageIds: string[], user, userGroups?, includeEmpty?: boolean): Promise<PageDocument[]>
   findByPathAndViewer(path: string | null, user, userGroups?, useFindOne?: boolean, includeEmpty?: boolean): Promise<PageDocument[]>
@@ -121,9 +121,12 @@ const generateChildrenRegExp = (path: string): RegExp => {
 /*
  * Create empty pages if the page in paths didn't exist
  */
-schema.statics.createEmptyPagesByPaths = async function(paths: string[], publicOnly = false): Promise<void> {
+schema.statics.createEmptyPagesByPaths = async function(paths: string[], onlyMigratedAsExistingPages = true, publicOnly = false): Promise<void> {
   // find existing parents
   const builder = new PageQueryBuilder(this.find(publicOnly ? { grant: GRANT_PUBLIC } : {}, { _id: 0, path: 1 }), true);
+  if (onlyMigratedAsExistingPages) {
+    builder.addConditionAsMigrated();
+  }
   const existingPages = await builder
     .addConditionToListByPathsArray(paths)
     .query
@@ -219,9 +222,15 @@ schema.statics.replaceTargetWithPage = async function(exPage, pageToReplaceWith?
  */
 schema.statics.getParentAndFillAncestors = async function(path: string): Promise<PageDocument> {
   const parentPath = nodePath.dirname(path);
-  const parent = await this.findOne({ path: parentPath }); // find the oldest parent which must always be the true parent
-  if (parent != null) {
-    return parent;
+
+  const builder1 = new PageQueryBuilder(this.find({ path: parentPath }), true);
+  const pagesCanBeParent = await builder1
+    .addConditionAsMigrated()
+    .query
+    .exec();
+
+  if (pagesCanBeParent.length >= 1) {
+    return pagesCanBeParent[0]; // the earliest page will be the result
   }
 
   /*
@@ -233,8 +242,8 @@ schema.statics.getParentAndFillAncestors = async function(path: string): Promise
   await this.createEmptyPagesByPaths(ancestorPaths);
 
   // find ancestors
-  const builder = new PageQueryBuilder(this.find(), true);
-  const ancestors = await builder
+  const builder2 = new PageQueryBuilder(this.find(), true);
+  const ancestors = await builder2
     .addConditionToListByPathsArray(ancestorPaths)
     .addConditionToSortPagesByDescPath()
     .query
@@ -246,15 +255,14 @@ schema.statics.getParentAndFillAncestors = async function(path: string): Promise
   // bulkWrite to update ancestors
   const nonRootAncestors = ancestors.filter(page => !isTopPage(page.path));
   const operations = nonRootAncestors.map((page) => {
-    const { path } = page;
-    const parentPath = nodePath.dirname(path);
+    const parentPath = nodePath.dirname(page.path);
     return {
       updateOne: {
         filter: {
-          path,
+          _id: page._id,
         },
         update: {
-          parent: ancestorsMap.get(parentPath),
+          parent: ancestorsMap.get(parentPath)._id,
         },
       },
     };
@@ -560,6 +568,10 @@ schema.statics.normalizeDescendantCountById = async function(pageId) {
   return this.updateOne({ _id: pageId }, { $set: { descendantCount: sumChildrenDescendantCount + sumChildPages } }, { new: true });
 };
 
+schema.statics.takeOffFromTree = async function(pageId: ObjectIdLike) {
+  return this.findByIdAndUpdate(pageId, { $set: { parent: null } });
+};
+
 export type PageCreateOptions = {
   format?: string
   grantUserGroupId?: ObjectIdLike
@@ -672,8 +684,6 @@ export default (crowi: Crowi): any => {
 
     let savedPage = await page.save();
 
-    await crowi.pageService.updateDescendantCountOfAncestors(page._id, 1, false);
-
     /*
      * After save
      */
@@ -694,6 +704,9 @@ export default (crowi: Crowi): any => {
 
     pageEvent.emit('create', savedPage, user);
 
+    // update descendantCount asynchronously
+    await crowi.pageService.updateDescendantCountOfAncestors(savedPage._id, 1, false);
+
     return savedPage;
   };
 

+ 29 - 8
packages/app/src/server/routes/apiv3/bookmarks.js

@@ -258,6 +258,11 @@ module.exports = (crowi) => {
    */
   router.put('/', accessTokenParser, loginRequiredStrictly, csrf, validator.bookmarks, apiV3FormValidator, async(req, res) => {
     const { pageId, bool } = req.body;
+    const userId = req.user?._id;
+
+    if (userId == null) {
+      return res.apiv3Err('A logged in user is required.');
+    }
 
     let bookmark;
     try {
@@ -265,15 +270,29 @@ module.exports = (crowi) => {
       if (page == null) {
         return res.apiv3Err(`Page '${pageId}' is not found or forbidden`);
       }
-      if (bool) {
-        bookmark = await Bookmark.add(page, req.user);
 
-        const pageEvent = crowi.event('page');
-        // in-app notification
-        pageEvent.emit('bookmark', page, req.user);
+      bookmark = await Bookmark.findByPageIdAndUserId(page._id, req.user._id);
+
+      if (bookmark == null) {
+        if (bool) {
+          bookmark = await Bookmark.add(page, req.user);
+
+          const pageEvent = crowi.event('page');
+          // in-app notification
+          pageEvent.emit('bookmark', page, req.user);
+        }
+        else {
+          logger.warn(`Removing the bookmark for ${page._id} by ${req.user._id} failed because the bookmark does not exist.`);
+        }
       }
       else {
-        bookmark = await Bookmark.removeBookmark(page, req.user);
+        // eslint-disable-next-line no-lonely-if
+        if (bool) {
+          logger.warn(`Adding the bookmark for ${page._id} by ${req.user._id} failed because the bookmark has already exist.`);
+        }
+        else {
+          bookmark = await Bookmark.removeBookmark(page, req.user);
+        }
       }
     }
     catch (err) {
@@ -281,8 +300,10 @@ module.exports = (crowi) => {
       return res.apiv3Err(err, 500);
     }
 
-    bookmark.depopulate('page');
-    bookmark.depopulate('user');
+    if (bookmark != null) {
+      bookmark.depopulate('page');
+      bookmark.depopulate('user');
+    }
 
     return res.apiv3({ bookmark });
   });

+ 2 - 2
packages/app/src/server/routes/apiv3/pages.js

@@ -718,10 +718,10 @@ module.exports = (crowi) => {
     const limit = parseInt(req.query.limit) || LIMIT_FOR_LIST;
 
     try {
-      const pageData = await Page.findByPath(path);
+      const pageData = await Page.findByPath(path, true);
       const result = await Page.findManageableListWithDescendants(pageData, req.user, { limit });
 
-      return res.apiv3({ subordinatedPaths: result });
+      return res.apiv3({ subordinatedPages: result });
     }
     catch (err) {
       return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);

+ 21 - 9
packages/app/src/server/service/page.ts

@@ -266,9 +266,9 @@ class PageService {
       };
     }
 
-    const isBookmarked = await Bookmark.findByPageIdAndUserId(pageId, user._id);
-    const isLiked = page.isLiked(user);
-    const isAbleToDeleteCompletely = this.canDeleteCompletely((page.creator as IUserHasId)?._id, user);
+    const isBookmarked: boolean = (await Bookmark.findByPageIdAndUserId(pageId, user._id)) != null;
+    const isLiked: boolean = page.isLiked(user);
+    const isAbleToDeleteCompletely: boolean = this.canDeleteCompletely((page.creator as IUserHasId)?._id, user);
 
     const subscription = await Subscription.findByUserIdAndTargetId(user._id, pageId);
 
@@ -434,11 +434,15 @@ class PageService {
       }
     }
 
-    // Rename target (update parent attr)
+    // 1. Take target off from tree
+    await Page.takeOffFromTree(page._id);
+
+    // 2. Find new parent
     const update: Partial<IPage> = {};
     // find or create parent
     const newParent = await Page.getParentAndFillAncestors(newPagePath);
-    // update Page
+
+    // 3. Put back target page to tree (also update the other attrs)
     update.path = newPagePath;
     update.parent = newParent._id;
     if (updateMetadata) {
@@ -447,9 +451,6 @@ class PageService {
     }
     const renamedPage = await Page.findByIdAndUpdate(page._id, { $set: update }, { new: true });
 
-    // remove empty pages at leaf position
-    await Page.removeLeafEmptyPagesRecursively(page.parent);
-
     // create page redirect
     if (options.createRedirectPage) {
       const PageRedirect = mongoose.model('PageRedirect') as unknown as PageRedirectModel;
@@ -472,6 +473,8 @@ class PageService {
   }
 
   async renameSubOperation(page, newPagePath: string, user, options, renamedPage, pageOpId: ObjectIdLike): Promise<void> {
+    const Page = mongoose.model('Page') as unknown as PageModel;
+
     const exParentId = page.parent;
 
     // update descendants first
@@ -485,6 +488,15 @@ class PageService {
     const nToIncrease = (renamedPage.isEmpty ? 0 : 1) + page.descendantCount;
     await this.updateDescendantCountOfAncestors(renamedPage._id, nToIncrease, false);
 
+    // Remove leaf empty pages if not moving to under the ex-target position
+    const pathToTest = escapeStringRegexp(addTrailingSlash(page.path));
+    const pathToBeTested = newPagePath;
+    const isRenamingToUnderExTarget = (new RegExp(`^${pathToTest}`)).test(pathToBeTested);
+    if (!isRenamingToUnderExTarget) {
+      // remove empty pages at leaf position
+      await Page.removeLeafEmptyPagesRecursively(page.parent);
+    }
+
     await PageOperation.findByIdAndDelete(pageOpId);
   }
 
@@ -2457,7 +2469,7 @@ class PageService {
         const parentPaths = Array.from(parentPathsSet);
 
         // fill parents with empty pages
-        await Page.createEmptyPagesByPaths(parentPaths, publicOnly);
+        await Page.createEmptyPagesByPaths(parentPaths, false, publicOnly);
 
         // find parents again
         const builder = new PageQueryBuilder(Page.find({}, { _id: 1, path: 1 }), true);

+ 4 - 1
packages/app/src/stores/page.tsx

@@ -82,8 +82,11 @@ export const useSWRxPageInfo = (
     shareLinkId?: string | null,
 ): SWRResponse<IPageInfo | IPageInfoForOperation, Error> => {
 
+  // assign null if shareLinkId is undefined in order to identify SWR key only by pageId
+  const fixedShareLinkId = shareLinkId ?? null;
+
   return useSWRImmutable(
-    pageId != null ? ['/page/info', pageId, shareLinkId] : null,
+    pageId != null ? ['/page/info', pageId, fixedShareLinkId] : null,
     (endpoint, pageId, shareLinkId) => apiv3Get(endpoint, { pageId, shareLinkId }).then(response => response.data),
   );
 };

+ 68 - 79
packages/app/src/styles/_sidebar.scss

@@ -25,66 +25,6 @@
   // set the max value that should be taken when sticky
   height: calc(100vh - $grw-navbar-border-width);
 
-  .grw-navigation-resize-button {
-    position: fixed;
-
-    $width: 27.691px;
-    $height: 23.999px;
-
-    @mixin hitarea($size-hitarea) {
-      top: ($width - $size-hitarea) / 2;
-      left: ($height - $size-hitarea) / 2;
-      width: $size-hitarea;
-      height: $size-hitarea;
-    }
-
-    // locate to the center of screen
-    top: calc(50vh - $height/2);
-
-    padding: 0px;
-    background-color: transparent;
-    border: 0;
-    opacity: 0;
-    transition: opacity 300ms cubic-bezier(0.2, 0, 0, 1) 0s;
-    transform: translateX(-50%);
-
-    .hexagon-container {
-      // set transform
-      svg * {
-        transition: fill 100ms linear;
-      }
-      svg {
-        width: $width + 2px; // add 1px for drop-shadow
-        height: $height + 2px; // add 1px for drop-shadow
-        .background {
-          filter: drop-shadow(0px 1px 0px rgba(#999, 60%));
-        }
-      }
-    }
-    .hitarea {
-      @extend .rounded-pill;
-
-      position: absolute;
-      @include hitarea(30px);
-    }
-
-    // reverse and center icon at the time of collapsed
-    &.collapsed {
-      opacity: 1;
-      .hexagon-container svg {
-        transform: rotate(180deg);
-      }
-      .hitarea {
-        @include hitarea(80px);
-      }
-    }
-  }
-  &:hover {
-    .grw-navigation-resize-button {
-      opacity: 1;
-    }
-  }
-
   // override @atlaskit/navigation-next styles
   $navbar-total-height: $grw-navbar-height + $grw-navbar-border-width;
   .data-layout-container {
@@ -104,6 +44,7 @@
       flex-direction: row;
       height: 100%;
       overflow: hidden;
+
       .grw-contextual-navigation {
         position: relative;
         width: 240px;
@@ -137,6 +78,11 @@
           }
         }
       }
+
+      .grw-sidebar-content-container {
+        position: relative;
+        z-index: 110; // greater than the value of .grw-navigation-draggable to fix https://redmine.weseek.co.jp/issues/86678
+      }
     }
     .grw-navigation-draggable {
       position: absolute;
@@ -145,20 +91,6 @@
       left: 100%;
       z-index: 100; // greater than the value of slimScrollBar
       width: 0;
-      transform: unset; // unset for 'position: fixed' of .ak-navigation-resize-button
-      .grw-navigation-draggable-first-child {
-        position: absolute;
-        top: 0px;
-        bottom: 0px;
-        left: -3px;
-        width: 3px;
-        pointer-events: none;
-        background: linear-gradient(to left, rgba(0, 0, 0, 0.1) 0px, rgba(0, 0, 0, 0.1) 1px, rgba(0, 0, 0, 0.1) 1px, rgba(0, 0, 0, 0) 100%);
-        opacity: 0.5;
-        transition-timing-function: cubic-bezier(0.2, 0, 0, 1);
-        transition-duration: 0.22s;
-        transition-property: left, opacity, width;
-      }
       .grw-navigation-draggable-hitarea {
         position: relative;
         left: -4px;
@@ -168,14 +100,71 @@
         .grw-navigation-draggable-hitarea-child {
           position: absolute;
           left: 3px;
+          display: none;
           width: 2px;
           height: 100%;
           background-color: rgb(76, 154, 255);
-          opacity: 0;
-          transition: opacity 200ms ease 0s;
         }
-        &:hover .grw-navigation-draggable-hitarea-child {
-          opacity: 1;
+      }
+      .grw-navigation-resize-button {
+        position: fixed;
+
+        $width: 27.691px;
+        $height: 23.999px;
+
+        @mixin hitarea($size-hitarea) {
+          top: ($width - $size-hitarea) / 2;
+          left: ($height - $size-hitarea) / 2;
+          width: $size-hitarea;
+          height: $size-hitarea;
+        }
+
+        // locate to the center of screen
+        top: calc(50vh - $height/2);
+
+        display: none;
+        padding: 0px;
+        background-color: transparent;
+        border: 0;
+        transform: translateX(-50%);
+
+        .hexagon-container {
+          // set transform
+          svg * {
+            transition: fill 100ms linear;
+          }
+          svg {
+            width: $width + 2px; // add 1px for drop-shadow
+            height: $height + 2px; // add 1px for drop-shadow
+            .background {
+              filter: drop-shadow(0px 1px 0px rgba(#999, 60%));
+            }
+          }
+        }
+        .hitarea {
+          @extend .rounded-pill;
+
+          position: absolute;
+          @include hitarea(30px);
+        }
+
+        // reverse and center icon at the time of collapsed
+        &.collapsed {
+          display: block;
+          .hexagon-container svg {
+            transform: rotate(180deg);
+          }
+          .hitarea {
+            @include hitarea(80px);
+          }
+        }
+      }
+      &:hover {
+        .grw-navigation-draggable-hitarea-child {
+          display: block;
+        }
+        .grw-navigation-resize-button {
+          display: block;
         }
       }
     }
@@ -290,7 +279,7 @@
   }
 
   .grw-navigation-resize-button {
-    display: none;
+    display: none !important;
   }
 
   .grw-drawer-toggler {

+ 4 - 0
packages/app/test/cypress/integration/2-basic-features/access-to-admin-page.spec.ts

@@ -79,6 +79,8 @@ context('Access to Admin page', () => {
   it('/admin/notification is successfully loaded', () => {
     cy.visit('/admin/notification');
     cy.getByTestid('admin-notification').should('be.visible');
+    // wait for retrieving slack integration status
+    cy.getByTestid('slack-integration-list-item').should('be.visible');
     cy.screenshot(`${ssPrefix}-admin-notification`, { capture: 'viewport' });
   });
 
@@ -109,6 +111,8 @@ context('Access to Admin page', () => {
   it('/admin/search is successfully loaded', () => {
     cy.visit('/admin/search');
     cy.getByTestid('admin-full-text-search').should('be.visible');
+    // wait for connected
+    cy.getByTestid('connection-status-badge-connected').should('be.visible');
     cy.screenshot(`${ssPrefix}-admin-search`, { capture: 'viewport' });
   });
 

+ 2 - 0
packages/app/test/cypress/integration/2-basic-features/access-to-me-page.spec.ts

@@ -11,6 +11,8 @@ context('Access to /me page', () => {
     cy.getCookie('connect.sid').then(cookie => {
       connectSid = cookie?.value;
     });
+    // collapse sidebar
+    cy.collapseSidebar(true);
   });
 
   beforeEach(() => {

+ 2 - 6
packages/app/test/cypress/integration/2-basic-features/access-to-page.spec.ts

@@ -12,6 +12,8 @@ context('Access to page', () => {
     cy.getCookie('connect.sid').then(cookie => {
       connectSid = cookie?.value;
     });
+    // collapse sidebar
+    cy.collapseSidebar(true);
   });
 
   beforeEach(() => {
@@ -22,12 +24,6 @@ context('Access to page', () => {
 
   it('/Sandbox is successfully loaded', () => {
     cy.visit('/Sandbox', {  });
-
-    // collapse sidebar and wait saving
-    cy.collapseSidebar(true);
-    // eslint-disable-next-line cypress/no-unnecessary-waiting
-    cy.wait(1500);
-
     cy.screenshot(`${ssPrefix}-sandbox`, { capture: 'viewport' });
   });
 

+ 9 - 0
packages/app/test/cypress/integration/2-basic-features/access-to-special-page.spec.ts

@@ -12,6 +12,8 @@ context('Access to special pages', () => {
     cy.getCookie('connect.sid').then(cookie => {
       connectSid = cookie?.value;
     });
+    // collapse sidebar
+    cy.collapseSidebar(true);
   });
 
   beforeEach(() => {
@@ -28,6 +30,13 @@ context('Access to special pages', () => {
 
   it('/tags is successfully loaded', () => {
     cy.visit('/tags');
+
+    // open sidebar
+    cy.collapseSidebar(false);
+    // select tags
+    cy.getByTestid('grw-sidebar-nav-primary-tags').click();
+    cy.getByTestid('grw-sidebar-content-tags').should('be.visible');
+
     cy.getByTestid('tags-page').should('be.visible');
     cy.screenshot(`${ssPrefix}-tags`, { capture: 'viewport' });
   });

+ 2 - 0
packages/app/test/cypress/integration/2-basic-features/open-page-create-modal.spec.ts

@@ -12,6 +12,8 @@ context('Open PageCreateModal', () => {
     cy.getCookie('connect.sid').then(cookie => {
       connectSid = cookie?.value;
     });
+    // collapse sidebar
+    cy.collapseSidebar(true);
   });
 
   beforeEach(() => {

+ 61 - 0
packages/app/test/cypress/integration/2-basic-features/open-presentation-modal.spec.ts

@@ -0,0 +1,61 @@
+
+context('Open presentation modal', () => {
+  const ssPrefix = 'access-to-presentation-modal-';
+
+  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('PageCreateModal for "/" is shown successfully', () => {
+    cy.visit('/');
+
+    cy.get('#grw-subnav-container').within(() => {
+      cy.getByTestid('open-page-item-control-btn').click({force: true})
+      cy.getByTestid('open-presentation-modal-btn').click({force: true})
+    });
+
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(1500);
+    cy.screenshot(`${ssPrefix}-opne-top`, { capture: 'viewport' });
+  });
+
+  it('PageCreateModal for "/Sandbox/Bootstrap4" is shown successfully', () => {
+    cy.visit('/Sandbox/Bootstrap4');
+
+    cy.get('#grw-subnav-container').within(() => {
+      cy.getByTestid('open-page-item-control-btn').click({force: true})
+      cy.getByTestid('open-presentation-modal-btn').click({force: true})
+    });
+
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(1500);
+    cy.screenshot(`${ssPrefix}-open-bootstrap4`, { capture: 'viewport' });
+  });
+
+  it('PageCreateModal for /Sandbox/Bootstrap4#Cards" is shown successfully', () => {
+    cy.visit('/Sandbox/Bootstrap4#Cards');
+
+    cy.get('#grw-subnav-container').within(() => {
+      cy.getByTestid('open-page-item-control-btn').click({force: true})
+      cy.getByTestid('open-presentation-modal-btn').click({force: true})
+    });
+
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(1500);
+    cy.screenshot(`${ssPrefix}-open-bootstrap4-with-ancker-link`, { capture: 'viewport' });
+  });
+});

+ 2 - 0
packages/app/test/cypress/integration/3-search/access-to-private-legacy-pages-directly.spec.ts

@@ -11,6 +11,8 @@ context('Access to legacy private pages directly', () => {
     cy.getCookie('connect.sid').then(cookie => {
       connectSid = cookie?.value;
     });
+    // collapse sidebar
+    cy.collapseSidebar(true);
   });
 
   beforeEach(() => {

+ 4 - 2
packages/app/test/cypress/integration/3-search/access-to-result-page-directly.spec.ts

@@ -11,6 +11,8 @@ context('Access to search result page directly', () => {
     cy.getCookie('connect.sid').then(cookie => {
       connectSid = cookie?.value;
     });
+    // collapse sidebar
+    cy.collapseSidebar(true);
   });
 
   beforeEach(() => {
@@ -20,7 +22,7 @@ context('Access to search result page directly', () => {
   });
 
   it('/_search with "q" param is successfully loaded', () => {
-    cy.visit('/_search', { qs: { q: 'bootstrap4 labels alerts' } });
+    cy.visit('/_search', { qs: { q: 'block labels alerts cards' } });
 
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
@@ -29,7 +31,7 @@ context('Access to search result page directly', () => {
   });
 
   it('checkboxes behaviors', () => {
-    cy.visit('/_search', { qs: { q: 'bootstrap4 labels alerts' } });
+    cy.visit('/_search', { qs: { q: 'block labels alerts cards' } });
 
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');

+ 11 - 1
packages/app/test/cypress/support/commands.ts

@@ -39,11 +39,21 @@ Cypress.Commands.add('login', (username, password) => {
 });
 
 Cypress.Commands.add('collapseSidebar', (isCollapsed) => {
+  const isGrowiPage = Cypress.$('body.growi').length > 0;
+
+  if (!isGrowiPage) {
+    cy.visit('/');
+  }
+
   cy.getByTestid('grw-contextual-navigation-sub').then(($contents) => {
     const isCurrentCollapsed = $contents.hasClass('d-none');
     // toggle when the current state and isCoolapsed is not match
     if (isCurrentCollapsed !== isCollapsed) {
-      cy.getByTestid("grw-navigation-resize-button").click();
+      cy.getByTestid("grw-navigation-resize-button").click({force: true});
+
+      // wait until saving UserUISettings
+      // eslint-disable-next-line cypress/no-unnecessary-waiting
+      cy.wait(1500);
     }
   });
 });

+ 4 - 2
packages/app/test/integration/global-setup.js

@@ -30,8 +30,10 @@ module.exports = async() => {
 
   // create global user & rootPage
   const globalUser = (await userCollection.insertMany([{ name: 'globalUser', username: 'globalUser', email: 'globalUser@example.com' }]))[0];
-  await userCollection.insertMany([{ name: 'v5DummyUser1', username: 'v5DummyUser1', email: 'v5DummyUser1@example.com' }])[0];
-  await userCollection.insertMany([{ name: 'v5DummyUser2', username: 'v5DummyUser2', email: 'v5DummyUser2@example.com' }])[0];
+  await userCollection.insertMany([
+    { name: 'v5DummyUser1', username: 'v5DummyUser1', email: 'v5DummyUser1@example.com' },
+    { name: 'v5DummyUser2', username: 'v5DummyUser2', email: 'v5DummyUser2@example.com' },
+  ]);
   await pageCollection.insertMany([{
     path: '/',
     grant: 1,

+ 1 - 1
packages/codemirror-textlint/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/codemirror-textlint",
-  "version": "5.0.0-RC.7",
+  "version": "5.0.0-RC.8",
   "license": "MIT",
   "main": "dist/index.js",
   "scripts": {

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/core",
-  "version": "5.0.0-RC.7",
+  "version": "5.0.0-RC.8",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-attachment-refs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-attachment-refs",
-  "version": "5.0.0-RC.7",
+  "version": "5.0.0-RC.8",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-lsx/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-lsx",
-  "version": "5.0.0-RC.7",
+  "version": "5.0.0-RC.8",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-pukiwiki-like-linker/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-pukiwiki-like-linker",
-  "version": "5.0.0-RC.7",
+  "version": "5.0.0-RC.8",
   "description": "GROWI plugin to add PukiwikiLikeLinker",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slack",
-  "version": "5.0.0-RC.7",
+  "version": "5.0.0-RC.8",
   "license": "MIT",
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",

+ 1 - 1
packages/slackbot-proxy/package.json

@@ -25,7 +25,7 @@
   },
   "dependencies": {
     "@godaddy/terminus": "^4.9.0",
-    "@growi/slack": "^5.0.0-RC.7",
+    "@growi/slack": "^5.0.0-RC.8",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",

+ 1 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/ui",
-  "version": "5.0.0-RC.7",
+  "version": "5.0.0-RC.8",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": [