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

Merge branch 'master' of https://github.com/weseek/growi into fix/105954-global-search-is-not-displayed

Shun Miyazawa 3 лет назад
Родитель
Сommit
a5ebbb7fc8
38 измененных файлов с 531 добавлено и 213 удалено
  1. 17 21
      .github/workflows/ci-app-prod.yml
  2. 16 1
      CHANGELOG.md
  3. 2 2
      packages/app/docker/README.md
  4. 5 1
      packages/app/public/static/locales/en_US/admin.json
  5. 1 0
      packages/app/public/static/locales/en_US/translation.json
  6. 7 3
      packages/app/public/static/locales/ja_JP/admin.json
  7. 1 0
      packages/app/public/static/locales/ja_JP/translation.json
  8. 5 1
      packages/app/public/static/locales/zh_CN/admin.json
  9. 1 0
      packages/app/public/static/locales/zh_CN/translation.json
  10. 4 5
      packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx
  11. 6 1
      packages/app/src/components/Common/Dropdown/PageItemControl.tsx
  12. 54 13
      packages/app/src/components/Common/ImageCropModal.tsx
  13. 3 4
      packages/app/src/components/Me/ProfileImageSettings.tsx
  14. 15 13
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  15. 1 1
      packages/app/src/components/Page/TagLabels.tsx
  16. 94 0
      packages/app/src/components/PageEditor/DrawioCommunicationHelper.ts
  17. 72 18
      packages/app/src/components/PageEditor/DrawioModal.tsx
  18. 1 0
      packages/app/src/components/PageList/PageListItemL.tsx
  19. 1 1
      packages/app/src/components/PrivateLegacyPages.tsx
  20. 1 1
      packages/app/src/components/SavePageControls.tsx
  21. 1 1
      packages/app/src/components/Sidebar/RecentChanges.tsx
  22. 1 1
      packages/app/src/interfaces/activity.ts
  23. 1 1
      packages/app/src/server/routes/index.js
  24. 15 1
      packages/app/src/server/service/installer.ts
  25. 2 2
      packages/app/src/stores/context.tsx
  26. 0 60
      packages/app/src/stores/modal.tsx
  27. 0 15
      packages/app/src/utils/drawio-config.ts
  28. 9 0
      packages/app/test/cypress/integration/20-basic-features/access-to-page.spec.ts
  29. 11 4
      packages/app/test/cypress/integration/20-basic-features/access-to-pagelist.spec.ts
  30. 82 10
      packages/app/test/cypress/integration/20-basic-features/use-tools.spec.ts
  31. 14 10
      packages/app/test/cypress/integration/21-basic-features-for-guest/access-to-page.spec.ts
  32. 53 12
      packages/app/test/cypress/integration/30-search/search.spec.ts
  33. 2 0
      packages/app/test/cypress/integration/40-admin/access-to-admin-page.spec.ts
  34. 21 3
      packages/app/test/cypress/integration/50-sidebar/access-to-side-bar.spec.ts
  35. 8 3
      packages/app/test/cypress/integration/50-sidebar/switching-sidebar-mode.spec.ts
  36. 2 2
      packages/app/test/cypress/support/commands.ts
  37. 1 1
      packages/app/test/cypress/support/index.ts
  38. 1 1
      packages/core/src/utils/template-checker.ts

+ 17 - 21
.github/workflows/ci-app-prod.yml

@@ -3,8 +3,7 @@ name: Node CI for app production
 on:
   push:
     branches:
-      # - master
-      - support/apply-nextjs-2
+      - master
     paths:
       - .github/workflows/ci-app-prod.yml
       - .github/workflows/reusable-app-prod.yml
@@ -21,8 +20,7 @@ on:
       - packages/plugin-**
   pull_request:
     branches:
-      # - master
-      - support/apply-nextjs-2
+      - master
     types: [opened, reopened, synchronize]
     paths:
       - .github/workflows/ci-app-prod.yml
@@ -42,8 +40,7 @@ on:
 jobs:
 
   test-prod-node14:
-    # uses: weseek/growi/.github/workflows/reusable-app-prod.yml@support/master
-    uses: weseek/growi/.github/workflows/reusable-app-prod.yml@support/apply-nextjs-2
+    uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
     with:
       node-version: 14.x
       skip-cypress: true
@@ -52,8 +49,7 @@ jobs:
 
 
   test-prod-node16:
-    # uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
-    uses: weseek/growi/.github/workflows/reusable-app-prod.yml@support/apply-nextjs-2
+    uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
     with:
       node-version: 16.x
       # skip-cypress: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) && contains( github.event.pull_request.labels.*.name, 'github_actions' ) }}
@@ -63,19 +59,19 @@ jobs:
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
-  # run-reg-suit-node16:
-  #   needs: [test-prod-node16]
+  run-reg-suit-node16:
+    needs: [test-prod-node16]
 
-  #   uses: weseek/growi/.github/workflows/reusable-app-reg-suit.yml@master
+    uses: weseek/growi/.github/workflows/reusable-app-reg-suit.yml@master
 
-  #   if: always()
+    if: always()
 
-  #   with:
-  #     node-version: 16.x
-  #     skip-reg-suit: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) && contains( github.event.pull_request.labels.*.name, 'github_actions' ) }}
-  #     cypress-report-artifact-name: Cypress report
-  #   secrets:
-  #     REG_NOTIFY_GITHUB_PLUGIN_CLIENTID: ${{ secrets.REG_NOTIFY_GITHUB_PLUGIN_CLIENTID }}
-  #     AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
-  #     AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
-  #     SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
+    with:
+      node-version: 16.x
+      skip-reg-suit: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) && contains( github.event.pull_request.labels.*.name, 'github_actions' ) }}
+      cypress-report-artifact-name: Cypress report
+    secrets:
+      REG_NOTIFY_GITHUB_PLUGIN_CLIENTID: ${{ secrets.REG_NOTIFY_GITHUB_PLUGIN_CLIENTID }}
+      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
+      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

+ 16 - 1
CHANGELOG.md

@@ -1,9 +1,24 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v5.1.4...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v5.1.5...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v5.1.5](https://github.com/weseek/growi/compare/v5.1.4...v5.1.5) - 2022-10-04
+
+### 💎 Features
+
+- feat: Add option to not use crop modal on brand logo upload (#6677) @Yohei-Shiina
+
+### 🚀 Improvement
+
+- imprv: Emoji picker performance for v5.x (#6689) @hakumizuki
+
+### 🐛 Bug Fixes
+
+- fix(auditlog): Attachment download is displayed even if the filter is unchecked (#6688) @miya
+- fix: firstName and lastName japanese translations in SAML  (#6631) @kaoritokashiki
+
 ## [v5.1.4](https://github.com/weseek/growi/compare/v5.1.3...v5.1.4) - 2022-09-12
 
 ### 💎 Features

+ 2 - 2
packages/app/docker/README.md

@@ -11,8 +11,8 @@ Supported tags and respective Dockerfile links
 ------------------------------------------------
 
 * [`6.0.0`, `6.0`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.0.0/packages/app/docker/Dockerfile)
-* [`5.1.4`, `5.1`, `5`](https://github.com/weseek/growi/blob/v5.1.4/packages/app/docker/Dockerfile)
-* [`5.1.4-nocdn`, `5.1-nocdn`, `5-nocdn`](https://github.com/weseek/growi/blob/v5.1.4/packages/app/docker/Dockerfile)
+* [`5.1.5`, `5.1`, `5`](https://github.com/weseek/growi/blob/v5.1.5/packages/app/docker/Dockerfile)
+* [`5.1.5-nocdn`, `5.1-nocdn`, `5-nocdn`](https://github.com/weseek/growi/blob/v5.1.5/packages/app/docker/Dockerfile)
 * [`4.5.23`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)
 * [`4.5.23-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)
 

+ 5 - 1
packages/app/public/static/locales/en_US/admin.json

@@ -907,6 +907,10 @@
     "PAGE_DELETE_COMPLETELY": "Delete completely page",
     "PAGE_REVERT": "Revert page",
     "PAGE_EMPTY_TRASH": "Empty trash",
+    "PAGE_RECURSIVELY_RENAME": "Recursive page rename",
+    "PAGE_RECURSIVELY_DELETE": "Recursive page delete",
+    "PAGE_RECURSIVELY_DELETE_COMPLETELY": "Recursive page delete completely",
+    "PAGE_RECURSIVELY_REVERT": "Recursive page revert",
     "PAGE_SUBSCRIBE": "Subscribe page",
     "PAGE_UNSUBSCRIBE": "Unsubscribe page",
     "PAGE_EXPORT": "Export page",
@@ -924,7 +928,7 @@
     "SHARE_LINK_NOT_FOUND": "Page view (Not found share link)",
     "ATTACHMENT_ADD": "Add Attachment",
     "ATTACHMENT_REMOVE": "Delete Attachment",
-    "ACTION_ATTACHMENT_DOWNLOAD": "Download Attachment",
+    "ATTACHMENT_DOWNLOAD": "Download Attachment",
     "SEARCH_PAGE": "Page Search",
     "SEARCH_PAGE_VIEW": "Page view(Search results page)",
     "ADMIN_APP_SETTING_UPDATE": "Update App Settings",

+ 1 - 0
packages/app/public/static/locales/en_US/translation.json

@@ -815,6 +815,7 @@
   "crop_image_modal": {
     "image_crop": "Image Crop",
     "crop": "Crop",
+    "save": "Save",
     "reset": "Reset",
     "cancel": "Cancel"
   },

+ 7 - 3
packages/app/public/static/locales/ja_JP/admin.json

@@ -225,8 +225,8 @@
       "attrMapId": "ID",
       "attrMapUsername": "ユーザー名",
       "attrMapMail": "メールアドレス",
-      "attrMapFirstName": "",
-      "attrMapLastName": "",
+      "attrMapFirstName": "",
+      "attrMapLastName": "",
       "ABLCRule": "ルール"
     }
   },
@@ -919,6 +919,10 @@
     "PAGE_DELETE_COMPLETELY": "ページの完全削除",
     "PAGE_REVERT": "ページを元に戻す",
     "PAGE_EMPTY_TRASH": "ゴミ箱を空にする",
+    "PAGE_RECURSIVELY_RENAME": "再帰的なページのリネーム",
+    "PAGE_RECURSIVELY_DELETE": "再帰的なページの削除",
+    "PAGE_RECURSIVELY_DELETE_COMPLETELY": "再起的なページの完全削除",
+    "PAGE_RECURSIVELY_REVERT": "再起的なページの復元",
     "PAGE_SUBSCRIBE": "ページをサブスクライブ",
     "PAGE_UNSUBSCRIBE": "ページをアンサブスクライブ",
     "PAGE_EXPORT": "マークダウン形式でページをエクスポート",
@@ -936,7 +940,7 @@
     "SHARE_LINK_NOT_FOUND": "ページ閲覧(存在しない共有リンク)",
     "ATTACHMENT_ADD": "添付データの追加",
     "ATTACHMENT_REMOVE": "添付データの削除",
-    "ACTION_ATTACHMENT_DOWNLOAD": "添付データのダウンロード",
+    "ATTACHMENT_DOWNLOAD": "添付データのダウンロード",
     "SEARCH_PAGE": "ページの検索",
     "SEARCH_PAGE_VIEW": "ページ閲覧(検索結果ページ)",
     "ADMIN_APP_SETTING_UPDATE": "アプリ設定の更新",

+ 1 - 0
packages/app/public/static/locales/ja_JP/translation.json

@@ -806,6 +806,7 @@
   "crop_image_modal": {
     "image_crop": "画像の切り抜き",
     "crop": "トリミング",
+    "save": "保存",
     "reset": "リセット",
     "cancel": "キャンセル"
   },

+ 5 - 1
packages/app/public/static/locales/zh_CN/admin.json

@@ -885,6 +885,10 @@
     "PAGE_DELETE_COMPLETELY": "彻底删除页面",
     "PAGE_REVERT": "还原页面",
     "PAGE_EMPTY_TRASH": "清空垃圾箱",
+    "PAGE_RECURSIVELY_RENAME": "递归页面重命名",
+    "PAGE_RECURSIVELY_DELETE": "递归页面删除",
+    "PAGE_RECURSIVELY_DELETE_COMPLETELY": "递归页面完全删除",
+    "PAGE_RECURSIVELY_REVERT": "递归页面还原",
     "PAGE_SUBSCRIBE": "订阅页面",
     "PAGE_UNSUBSCRIBE": "退订页面",
     "PAGE_EXPORT": "导出页面",
@@ -902,7 +906,7 @@
     "SHARE_LINK_NOT_FOUND": "页面浏览量(未找到分享链接)",
     "ATTACHMENT_ADD": "添加附件",
     "ATTACHMENT_REMOVE": "删除附件",
-    "ACTION_ATTACHMENT_DOWNLOAD": "下载附件",
+    "ATTACHMENT_DOWNLOAD": "下载附件",
     "SEARCH_PAGE": "页面搜索",
     "SEARCH_PAGE_VIEW": "页面浏览量(搜索结果页面)",
     "ADMIN_APP_SETTING_UPDATE": "更新应用设置",

+ 1 - 0
packages/app/public/static/locales/zh_CN/translation.json

@@ -862,6 +862,7 @@
   "crop_image_modal": {
     "image_crop": "图像裁剪",
     "crop": "修剪",
+    "save": "节省",
     "reset": "重启",
     "cancel": "取消"
   },

+ 4 - 5
packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx

@@ -66,7 +66,6 @@ const CustomizeLogoSetting = (): JSX.Element => {
     }
   }, [t, isDefaultLogo, customizedLogoSrc]);
 
-
   const onClickDeleteBtn = useCallback(async() => {
     try {
       await apiv3Delete('/customize-setting/delete-brand-logo');
@@ -80,7 +79,8 @@ const CustomizeLogoSetting = (): JSX.Element => {
     }
   }, [t]);
 
-  const onCropCompleted = useCallback(async(croppedImage) => {
+
+  const processImageCompletedHandler = useCallback(async(croppedImage) => {
     try {
       const formData = new FormData();
       formData.append('file', croppedImage);
@@ -93,10 +93,8 @@ const CustomizeLogoSetting = (): JSX.Element => {
       setRetrieveError(err);
       throw new Error('Failed to upload brand logo');
     }
-    setIsImageCropModalShow(false);
   }, [t]);
 
-
   return (
     <React.Fragment>
       <div className="row">
@@ -172,8 +170,9 @@ const CustomizeLogoSetting = (): JSX.Element => {
         isShow={isImageCropModalShow}
         src={uploadLogoSrc}
         onModalClose={() => setIsImageCropModalShow(false)}
-        onCropCompleted={onCropCompleted}
+        onImageProcessCompleted={processImageCompletedHandler}
         isCircular={false}
+        showCropOption={false}
       />
     </React.Fragment>
   );

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

@@ -245,7 +245,12 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
   }
 
   return (
-    <DropdownMenu positionFixed modifiers={{ preventOverflow: { boundariesElement: undefined } }} right={alignRight}>
+    <DropdownMenu
+      data-testid="page-item-control-menu"
+      positionFixed
+      modifiers={{ preventOverflow: { boundariesElement: undefined } }}
+      right={alignRight}
+    >
       {contents}
     </DropdownMenu>
   );

+ 54 - 13
packages/app/src/components/Common/ImageCropModal.tsx

@@ -33,17 +33,19 @@ type Props = {
   isShow: boolean,
   src: string | ArrayBuffer | null,
   onModalClose: () => void,
-  onCropCompleted: (res: any) => void,
+  onImageProcessCompleted: (res: any) => void,
   isCircular: boolean,
+  showCropOption: boolean
 }
 const ImageCropModal: FC<Props> = (props: Props) => {
 
   const {
-    isShow, src, onModalClose, onCropCompleted, isCircular,
+    isShow, src, onModalClose, onImageProcessCompleted, isCircular, showCropOption,
   } = props;
 
-  const [imageRef, setImageRef] = useState<HTMLImageElement>();
+  const [imageRef, setImageRef] = useState<HTMLImageElement | null>(null);
   const [cropOptions, setCropOtions] = useState<CropOptions>(null);
+  const [isCropImage, setIsCropImage] = useState<boolean>(true);
   const { t } = useTranslation();
   const reset = useCallback(() => {
     if (imageRef) {
@@ -93,31 +95,70 @@ const ImageCropModal: FC<Props> = (props: Props) => {
     }
   };
 
-  const crop = async() => {
-    // crop immages
+  // Convert base64 Image to blob
+  const convertBase64ToBlob = async(base64Image: string) => {
+    const base64Response = await fetch(base64Image);
+    return base64Response.blob();
+  };
+
+
+  // Clear image and set isImageCrop true on modal close
+  const onModalCloseHandler = async() => {
+    setImageRef(null);
+    setIsCropImage(true);
+    onModalClose();
+  };
+
+  // Process and save image
+  // Cropping image is optional
+  // If crop is active , the saved image is cropped image (png). Otherwise, the original image will be saved (Original size and file type)
+  const processAndSaveImage = async() => {
     if (imageRef && cropOptions?.width && cropOptions.height) {
-      const result = await getCroppedImg(imageRef, cropOptions);
-      onCropCompleted(result);
+      const processedImage = isCropImage ? await getCroppedImg(imageRef, cropOptions) : await convertBase64ToBlob(imageRef.src);
+      // Save image to database
+      onImageProcessCompleted(processedImage);
     }
+    onModalCloseHandler();
   };
 
   return (
-    <Modal isOpen={isShow} toggle={onModalClose}>
-      <ModalHeader tag="h4" toggle={onModalClose} className="bg-info text-light">
+    <Modal isOpen={isShow} toggle={onModalCloseHandler}>
+      <ModalHeader tag="h4" toggle={onModalCloseHandler} className="bg-info text-light">
         {t('crop_image_modal.image_crop')}
       </ModalHeader>
       <ModalBody className="my-4">
-        <ReactCrop src={src} crop={cropOptions} onImageLoaded={onImageLoaded} onChange={onCropChange} circularCrop={isCircular} />
+        {
+          isCropImage
+            ? (<ReactCrop src={src} crop={cropOptions} onImageLoaded={onImageLoaded} onChange={onCropChange} circularCrop={isCircular} />)
+            : (<img style={{ maxWidth: imageRef?.width }} src={imageRef?.src} />)
+        }
       </ModalBody>
       <ModalFooter>
         <button type="button" className="btn btn-outline-danger rounded-pill mr-auto" onClick={reset}>
           {t('crop_image_modal.reset')}
         </button>
-        <button type="button" className="btn btn-outline-secondary rounded-pill mr-2" onClick={onModalClose}>
+        { !showCropOption && (
+          <div className="mr-auto">
+            <div className="custom-control custom-switch ">
+              <input
+                id="cropImageOption"
+                className="custom-control-input mr-auto"
+                type="checkbox"
+                checked={isCropImage}
+                onChange={() => { setIsCropImage(!isCropImage) }}
+              />
+              <label className="custom-control-label" htmlFor="cropImageOption">
+                { t('crop_image_modal.image_crop') }
+              </label>
+            </div>
+          </div>
+        )
+        }
+        <button type="button" className="btn btn-outline-secondary rounded-pill mr-2" onClick={onModalCloseHandler}>
           {t('crop_image_modal.cancel')}
         </button>
-        <button type="button" className="btn btn-outline-primary rounded-pill" onClick={crop}>
-          {t('crop_image_modal.crop')}
+        <button type="button" className="btn btn-outline-primary rounded-pill" onClick={processAndSaveImage}>
+          { isCropImage ? t('crop_image_modal.crop') : t('crop_image_modal.save') }
         </button>
       </ModalFooter>
     </Modal>

+ 3 - 4
packages/app/src/components/Me/ProfileImageSettings.tsx

@@ -42,7 +42,7 @@ const ProfileImageSettings = (): JSX.Element => {
     setShowImageCropModal(true);
   }, []);
 
-  const cropCompletedHandler = useCallback(async(croppedImage) => {
+  const processImageCompletedHandler = useCallback(async(croppedImage) => {
     try {
       const formData = new FormData();
       formData.append('file', croppedImage);
@@ -53,8 +53,6 @@ const ProfileImageSettings = (): JSX.Element => {
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       setUploadedPictureSrc((response as any).attachment.filePathProxied);
 
-      // close modal
-      setShowImageCropModal(false);
     }
     catch (err) {
       toastError(err);
@@ -158,8 +156,9 @@ const ProfileImageSettings = (): JSX.Element => {
         isShow={showImageCropModal}
         src={imageCropSrc}
         onModalClose={() => setShowImageCropModal(false)}
-        onCropCompleted={cropCompletedHandler}
+        onImageProcessCompleted={processImageCompletedHandler}
         isCircular
+        showCropOption
       />
 
       <div className="row my-3">

+ 15 - 13
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -385,19 +385,21 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
     : currentPage?.path;
 
   return (
-    <GrowiSubNavigation
-      pagePath={pagePath}
-      pageId={currentPage?._id}
-      showDrawerToggler={isDrawerMode}
-      showTagLabel={isAbleToShowTagLabel}
-      isGuestUser={isGuestUser}
-      isDrawerMode={isDrawerMode}
-      isCompactMode={isCompactMode}
-      tags={isViewMode ? tagsInfoData?.tags : tagsForEditors}
-      tagsUpdatedHandler={isViewMode ? tagsUpdatedHandlerForViewMode : tagsUpdatedHandlerForEditMode}
-      rightComponent={RightComponent}
-      additionalClasses={['container-fluid']}
-    />
+    <div data-testid="grw-contextual-sub-nav">
+      <GrowiSubNavigation
+        pagePath={pagePath}
+        pageId={currentPage?._id}
+        showDrawerToggler={isDrawerMode}
+        showTagLabel={isAbleToShowTagLabel}
+        isGuestUser={isGuestUser}
+        isDrawerMode={isDrawerMode}
+        isCompactMode={isCompactMode}
+        tags={isViewMode ? tagsInfoData?.tags : tagsForEditors}
+        tagsUpdatedHandler={isViewMode ? tagsUpdatedHandlerForViewMode : tagsUpdatedHandlerForEditMode}
+        rightComponent={RightComponent}
+        additionalClasses={['container-fluid']}
+      />
+    </div>
   );
 };
 

+ 1 - 1
packages/app/src/components/Page/TagLabels.tsx

@@ -36,7 +36,7 @@ export const TagLabels:FC<Props> = (props: Props) => {
 
   return (
     <>
-      <div className={`${styles['grw-tag-labels']} grw-tag-labels d-flex align-items-center`}>
+      <div className={`${styles['grw-tag-labels']} grw-tag-labels d-flex align-items-center`} data-testid="grw-tag-labels">
         <i className="tag-icon icon-tag mr-2"></i>
         <RenderTagLabels
           tags={tags}

+ 94 - 0
packages/app/src/components/PageEditor/DrawioCommunicationHelper.ts

@@ -0,0 +1,94 @@
+import loggerFactory from '~/utils/logger';
+
+
+const logger = loggerFactory('growi:cli:DrawioCommunicationHelper');
+
+export type DrawioConfig = {
+  css: string,
+  customFonts: string[],
+}
+
+export type DrawioCommunicationCallbackOptions = {
+  onClose?: () => void;
+  onSave?: (drawioData: string) => void;
+}
+
+export class DrawioCommunicationHelper {
+
+  drawioUri: string;
+
+  drawioConfig: DrawioConfig;
+
+  callbackOpts?: DrawioCommunicationCallbackOptions;
+
+
+  constructor(drawioUri: string, drawioConfig: DrawioConfig, callbackOpts?: DrawioCommunicationCallbackOptions) {
+    this.drawioUri = drawioUri;
+    this.drawioConfig = drawioConfig;
+    this.callbackOpts = callbackOpts;
+  }
+
+  onReceiveMessage(event: MessageEvent, drawioMxFile: string): void {
+
+    // check origin
+    if (event.origin != null && this.drawioUri != null) {
+      const originUrl = new URL(event.origin);
+      const drawioUrl = new URL(this.drawioUri);
+
+      if (originUrl.origin !== drawioUrl.origin) {
+        logger.debug(`Skipping the event because the origins are mismatched. expected: '${drawioUrl.origin}', actual: '${originUrl.origin}'`);
+        return;
+      }
+    }
+
+    if (event.data === 'ready') {
+      event.source?.postMessage(drawioMxFile, { targetOrigin: '*' });
+      return;
+    }
+
+    if (event.data === '{"event":"configure"}') {
+      if (event.source == null) {
+        return;
+      }
+
+      // refs:
+      //  * https://desk.draw.io/support/solutions/articles/16000103852-how-to-customise-the-draw-io-interface
+      //  * https://desk.draw.io/support/solutions/articles/16000042544-how-does-embed-mode-work-
+      //  * https://desk.draw.io/support/solutions/articles/16000058316-how-to-configure-draw-io-
+      event.source.postMessage(JSON.stringify({
+        action: 'configure',
+        config: this.drawioConfig,
+      }), { targetOrigin: '*' });
+
+      return;
+    }
+
+    if (typeof event.data === 'string' && event.data.match(/mxfile/)) {
+      if (event.data.length > 0) {
+        const parser = new DOMParser();
+        const dom = parser.parseFromString(event.data, 'text/xml');
+        const drawioData = dom.getElementsByTagName('diagram')[0].innerHTML;
+
+        /*
+        * Saving Drawio will be implemented by the following tasks
+        * https://redmine.weseek.co.jp/issues/100845
+        * https://redmine.weseek.co.jp/issues/104507
+        */
+
+        this.callbackOpts?.onSave?.(drawioData);
+      }
+
+      this.callbackOpts?.onClose?.();
+
+      return;
+    }
+
+    if (typeof event.data === 'string' && event.data.length === 0) {
+      this.callbackOpts?.onClose?.();
+      return;
+    }
+
+    // NOTHING DONE. (Receive unknown iframe message.)
+  }
+
+}

+ 72 - 18
packages/app/src/components/PageEditor/DrawioModal.tsx

@@ -1,4 +1,6 @@
 import React, {
+  useCallback,
+  useEffect,
   useMemo,
 } from 'react';
 
@@ -13,25 +15,42 @@ import { useDrawioUri } from '~/stores/context';
 import { useDrawioModal } from '~/stores/modal';
 import { usePersonalSettings } from '~/stores/personal-settings';
 
+import { DrawioCommunicationHelper } from './DrawioCommunicationHelper';
+
+
+const headerColor = '#334455';
+const fontFamily = "Lato, -apple-system, BlinkMacSystemFont, 'Hiragino Kaku Gothic ProN', Meiryo, sans-serif";
+
+const drawioConfig = {
+  css: `
+  .geMenubarContainer { background-color: ${headerColor} !important; }
+  .geMenubar { background-color: ${headerColor} !important; }
+  .geEditor { font-family: ${fontFamily} !important; }
+  html td.mxPopupMenuItem {
+    font-family: ${fontFamily} !important;
+    font-size: 8pt !important;
+  }
+  `,
+  customFonts: ['Lato', 'Charter'],
+};
+
 
 type Props = {
   // onSave: (drawioData) => void,
 };
 
 export const DrawioModal = (props: Props): JSX.Element => {
-  const { data: growiDrawioUri } = useDrawioUri();
+  const { data: drawioUri } = useDrawioUri();
   const { data: personalSettingsInfo } = usePersonalSettings();
 
-
   const { data: drawioModalData, close: closeDrawioModal } = useDrawioModal();
   const isOpened = drawioModalData?.isOpened ?? false;
 
-  const cancel = () => {
-    closeDrawioModal();
-  };
+  const drawioUriWithParams = useMemo(() => {
+    if (drawioUri == null) {
+      return undefined;
+    }
 
-  const drawioUrl = useMemo(() => {
-    const drawioUri = growiDrawioUri || 'https://embed.diagrams.net/';
     const url = new URL(drawioUri);
 
     // refs: https://desk.draw.io/support/solutions/articles/16000042546-what-url-parameters-are-supported-
@@ -42,13 +61,46 @@ export const DrawioModal = (props: Props): JSX.Element => {
     url.searchParams.append('configure', '1');
 
     return url;
-  }, [growiDrawioUri, personalSettingsInfo?.lang]);
+  }, [drawioUri, personalSettingsInfo?.lang]);
+
+  const drawioCommunicationHelper = useMemo(() => {
+    if (drawioUri == null) {
+      return undefined;
+    }
 
+    return new DrawioCommunicationHelper(
+      drawioUri,
+      drawioConfig,
+      { onClose: closeDrawioModal },
+    );
+  }, [closeDrawioModal, drawioUri]);
+
+  const receiveMessageHandler = useCallback((event: MessageEvent) => {
+    if (drawioModalData == null) {
+      return;
+    }
+
+    drawioCommunicationHelper?.onReceiveMessage(event, drawioModalData.drawioMxFile);
+  }, [drawioCommunicationHelper, drawioModalData]);
+
+  useEffect(() => {
+    if (isOpened) {
+      window.addEventListener('message', receiveMessageHandler);
+    }
+    else {
+      window.removeEventListener('message', receiveMessageHandler);
+    }
+
+    // clean up
+    return function() {
+      window.removeEventListener('message', receiveMessageHandler);
+    };
+  }, [isOpened, receiveMessageHandler]);
 
   return (
     <Modal
       isOpen={isOpened}
-      toggle={cancel}
+      toggle={() => closeDrawioModal()}
       backdrop="static"
       className="drawio-modal grw-body-only-modal-expanded"
       size="xl"
@@ -62,15 +114,17 @@ export const DrawioModal = (props: Props): JSX.Element => {
           </div>
         </div>
         {/* iframe */}
-        <div className="w-100 h-100 position-absolute d-flex">
-          { isOpened && (
-            <iframe
-              src={drawioUrl.href}
-              className="border-0 flex-grow-1"
-            >
-            </iframe>
-          ) }
-        </div>
+        { drawioUriWithParams != null && (
+          <div className="w-100 h-100 position-absolute d-flex">
+            { isOpened && (
+              <iframe
+                src={drawioUriWithParams.href}
+                className="border-0 flex-grow-1"
+              >
+              </iframe>
+            ) }
+          </div>
+        ) }
       </ModalBody>
     </Modal>
   );

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

@@ -164,6 +164,7 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
     <li
       key={pageData._id}
       className={`list-group-item d-flex align-items-center px-3 px-md-1 ${styleListGroupItem} ${styleActive}`}
+      data-testid="page-list-item-L"
       onClick={clickHandler}
     >
       <div className="text-break w-100">

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

@@ -74,7 +74,7 @@ const SearchResultListHead = React.memo((props: SearchResultListHeadProps): JSX.
 
   if (isSuccess) {
     return (
-      <div className="card border-success mt-3">
+      <div className="card border-success mt-3" data-testid="search-result-private-legacy-pages">
         <div className="card-body">
           <h2 className="card-title text-success">{t('private_legacy_pages.nopages_title')}</h2>
           <p className="card-text">

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

@@ -85,7 +85,7 @@ export const SavePageControls = (props: Props): JSX.Element | null => {
       }
 
       <UncontrolledButtonDropdown direction="up">
-        <Button id="caret" color="primary" className="btn-submit" onClick={save}>
+        <Button data-testid="save-page-btn" id="caret" color="primary" className="btn-submit" onClick={save}>
           {labelSubmitButton}
         </Button>
         <DropdownToggle caret color="primary" />

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

@@ -36,7 +36,7 @@ const PageItemLower = memo(({ page }: PageItemProps): JSX.Element => {
         <div className="icon-bubble mr-1 d-inline-block"></div>
         <div className="mr-2 grw-list-counts d-inline-block">{page.commentCount}</div>
       </div>
-      <div className="grw-formatted-distance-date small mt-auto">
+      <div className="grw-formatted-distance-date small mt-auto" data-hide-in-vrt>
         <FormattedDistanceDate id={page._id} date={page.updatedAt} />
       </div>
     </div>

+ 1 - 1
packages/app/src/interfaces/activity.ts

@@ -67,7 +67,7 @@ const ACTION_SHARE_LINK_EXPIRED_PAGE_VIEW = 'SHARE_LINK_EXPIRED_PAGE_VIEW';
 const ACTION_SHARE_LINK_NOT_FOUND = 'SHARE_LINK_NOT_FOUND';
 const ACTION_ATTACHMENT_ADD = 'ATTACHMENT_ADD';
 const ACTION_ATTACHMENT_REMOVE = 'ATTACHMENT_REMOVE';
-const ACTION_ATTACHMENT_DOWNLOAD = 'ACTION_ATTACHMENT_DOWNLOAD';
+const ACTION_ATTACHMENT_DOWNLOAD = 'ATTACHMENT_DOWNLOAD';
 const ACTION_SEARCH_PAGE = 'SEARCH_PAGE';
 const ACTION_SEARCH_PAGE_VIEW = 'SEARCH_PAGE_VIEW';
 const ACTION_ADMIN_APP_SETTINGS_UPDATE = 'ADMIN_APP_SETTING_UPDATE';

+ 1 - 1
packages/app/src/server/routes/index.js

@@ -207,7 +207,7 @@ module.exports = function(crowi, app) {
   app.get('/me/*'                                 , loginRequiredStrictly, injectUserUISettings, next.delegateToNext);
   // external-accounts
   // my in-app-notifications
-  // app.get('/me/all-in-app-notifications'   , loginRequiredStrictly, allInAppNotifications.list);
+  // app.get('/me/all-in-app-notifications'   , loginRequiredStrictly, injectUserUISettings, allInAppNotifications.list);
   // app.get('/me/external-accounts'               , loginRequiredStrictly, injectUserUISettings, me.externalAccounts.list);
   // // my drafts
   // app.get('/me/drafts'                          , loginRequiredStrictly, injectUserUISettings, me.drafts.list);

+ 15 - 1
packages/app/src/server/service/installer.ts

@@ -1,6 +1,7 @@
 import path from 'path';
 
 import { Lang } from '@growi/core';
+import { addSeconds } from 'date-fns';
 import ExtensibleCustomError from 'extensible-custom-error';
 import fs from 'graceful-fs';
 import mongoose from 'mongoose';
@@ -78,7 +79,20 @@ export class InstallerService {
         // TODO typescriptize models/user.js and remove eslint-disable-next-line
         // eslint-disable-next-line @typescript-eslint/no-explicit-any
         const Page = mongoose.model('Page') as any;
-        await Page.updateMany({}, { createdAt: initialPagesCreatedAt, updatedAt: initialPagesCreatedAt });
+
+        // Increment timestamp to avoid difference for order in VRT
+        const pagePaths = ['/Sandbox', '/Sandbox/Bootstrap4', '/Sandbox/Diagrams', '/Sandbox/Math'];
+        const promises = pagePaths.map(async(path: string, idx: number) => {
+          const date = addSeconds(initialPagesCreatedAt, idx);
+          return Page.update(
+            { path },
+            {
+              createdAt: date,
+              updatedAt: date,
+            },
+          );
+        });
+        await Promise.all(promises);
       }
       catch (err) {
         logger.error('Failed to update createdAt', err);

+ 2 - 2
packages/app/src/stores/context.tsx

@@ -130,8 +130,8 @@ export const useRevisionIdHackmdSynced = (initialData?: Nullable<any>): SWRRespo
   return useStaticSWR<Nullable<any>, Error>('revisionIdHackmdSynced', initialData);
 };
 
-export const useDrawioUri = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
-  return useStaticSWR<Nullable<string>, Error>('drawioUri', initialData);
+export const useDrawioUri = (initialData?: string): SWRResponse<string, Error> => {
+  return useStaticSWR('drawioUri', initialData, { fallbackData: 'https://embed.diagrams.net/' });
 };
 
 export const useHackmdUri = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {

+ 0 - 60
packages/app/src/stores/modal.tsx

@@ -5,7 +5,6 @@ import {
   OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction, OnPutBackedFunction,
 } from '~/interfaces/ui';
 import { IUserGroupHasId } from '~/interfaces/user';
-import { dwawioConfig } from '~/utils/drawio-config';
 
 import { useStaticSWR } from './use-static-swr';
 
@@ -453,7 +452,6 @@ type DrawioModalStatus = {
 type DrawioModalStatusUtils = {
   open(drawioMxFile: string): void,
   close(): void,
-  receiveFromDrawio(event, drawioMxFile: string): void,
 }
 
 export const useDrawioModal = (status?: DrawioModalStatus): SWRResponse<DrawioModalStatus, Error> & DrawioModalStatusUtils => {
@@ -467,64 +465,7 @@ export const useDrawioModal = (status?: DrawioModalStatus): SWRResponse<DrawioMo
     swrResponse.mutate({ isOpened: false, drawioMxFile: '' });
   };
 
-  const receiveFromDrawio = (event, drawioMxFile: string) => {
-
-    if (event.data === 'ready') {
-      event.source.postMessage(drawioMxFile, '*');
-      return;
-    }
-
-    if (event.data === '{"event":"configure"}') {
-      if (event.source == null) {
-        return;
-      }
-
-      // refs:
-      //  * https://desk.draw.io/support/solutions/articles/16000103852-how-to-customise-the-draw-io-interface
-      //  * https://desk.draw.io/support/solutions/articles/16000042544-how-does-embed-mode-work-
-      //  * https://desk.draw.io/support/solutions/articles/16000058316-how-to-configure-draw-io-
-      event.source.postMessage(JSON.stringify({
-        action: 'configure',
-        config: dwawioConfig,
-      }), '*');
-
-      return;
-    }
-
-    if (typeof event.data === 'string' && event.data.match(/mxfile/)) {
-      if (event.data.length > 0) {
-        const parser = new DOMParser();
-        const dom = parser.parseFromString(event.data, 'text/xml');
-        const drawioData = dom.getElementsByTagName('diagram')[0].innerHTML;
-
-        /*
-        * Saving Drawio will be implemented by the following tasks
-        * https://redmine.weseek.co.jp/issues/100845
-        * https://redmine.weseek.co.jp/issues/104507
-        */
-
-        // if (props.onSave != null) {
-        //   props.onSave(drawioData);
-        // }
-      }
-
-      window.removeEventListener('message', () => receiveFromDrawio);
-      close();
-
-      return;
-    }
-
-    if (typeof event.data === 'string' && event.data.length === 0) {
-      close();
-
-      return;
-    }
-
-    // NOTHING DONE. (Receive unknown iframe message.)
-  };
-
   const open = (drawioMxFile: string): void => {
-    window.addEventListener('message', e => receiveFromDrawio(e, drawioMxFile));
     swrResponse.mutate({ isOpened: true, drawioMxFile });
   };
 
@@ -532,6 +473,5 @@ export const useDrawioModal = (status?: DrawioModalStatus): SWRResponse<DrawioMo
     ...swrResponse,
     open,
     close,
-    receiveFromDrawio,
   };
 };

+ 0 - 15
packages/app/src/utils/drawio-config.ts

@@ -1,15 +0,0 @@
-const headerColor = '#334455';
-const fontFamily = "Lato, -apple-system, BlinkMacSystemFont, 'Hiragino Kaku Gothic ProN', Meiryo, sans-serif";
-
-export const dwawioConfig = {
-  css: `
-  .geMenubarContainer { background-color: ${headerColor} !important; }
-  .geMenubar { background-color: ${headerColor} !important; }
-  .geEditor { font-family: ${fontFamily} !important; }
-  html td.mxPopupMenuItem {
-    font-family: ${fontFamily} !important;
-    font-size: 8pt !important;
-  }
-  `,
-  customFonts: ['Lato', 'Charter'],
-};

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

@@ -21,6 +21,11 @@ context('Access to page', () => {
     // hide fab
     cy.getByTestid('grw-fab-container').invoke('attr', 'style', 'display: none');
 
+    // remove animation for screenshot
+    // remove 'blink' class because ::after element cannot be operated
+    // https://stackoverflow.com/questions/5041494/selecting-and-manipulating-css-pseudo-elements-such-as-before-and-after-usin/21709814#21709814
+    cy.get('#Headers').invoke('removeClass', 'blink');
+
     cy.screenshot(`${ssPrefix}-sandbox-headers`);
   });
 
@@ -28,6 +33,8 @@ context('Access to page', () => {
     cy.visit('/Sandbox/Math');
 
     cy.get('mjx-container').should('be.visible');
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(2000); // wait for 2 seconds for MathJax.typesetPromise();
 
     cy.screenshot(`${ssPrefix}-sandbox-math`);
   });
@@ -59,6 +66,8 @@ context('Access to /me page', () => {
 
   it('/me is successfully loaded', () => {
     cy.visit('/me', {  });
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(500); // wait loading image
     cy.screenshot(`${ssPrefix}-me`);
   });
 

+ 11 - 4
packages/app/test/cypress/integration/20-basic-features/access-to-pagelist.spec.ts

@@ -12,8 +12,7 @@ context('Access to pagelist', () => {
   it('Page list modal is successfully opened ', () => {
     cy.visit('/');
     cy.getByTestid('pageListButton').click({force: true});
-    cy.getByTestid('page-accessories-modal').parent().should('have.class','show');
-    cy.screenshot(`${ssPrefix}1-open-pagelist-modal`);
+    cy.getByTestid('page-accessories-modal').should('be.visible').screenshot(`${ssPrefix}1-open-pagelist-modal`);
   });
 
   it('Successfully duplicate a page from page list', () => {
@@ -21,8 +20,10 @@ context('Access to pagelist', () => {
     cy.getByTestid('pageListButton').click({force: true});
     cy.getByTestid('page-accessories-modal').parent().should('have.class','show').within(() => {
       cy.getByTestid('open-page-item-control-btn').first().click();
-      cy.screenshot(`${ssPrefix}2-click-on-three-dots-menu`);
-      cy.get('.dropdown-menu').should('have.class', 'show').first().within(() => {
+      cy.getByTestid('page-item-control-menu').should('have.class', 'show').first().within(() => {
+        // eslint-disable-next-line cypress/no-unnecessary-waiting
+        cy.wait(300);
+        cy.screenshot(`${ssPrefix}2-open-page-item-control-menu`);
         cy.getByTestid('open-page-duplicate-modal-btn').click();
       });
     });
@@ -50,6 +51,7 @@ context('Access to pagelist', () => {
     cy.getByTestid('page-accessories-modal').parent().should('have.class','show').within(() => {
       cy.get('button.close').eq(0).click();
     });
+
     cy.screenshot(`${ssPrefix}7-page-list-modal-size-fullscreen`, {capture: 'viewport'});
 
     cy.getByTestid('page-accessories-modal').parent().should('have.class','show').within(() => {
@@ -76,6 +78,8 @@ context('Access to timeline', () => {
     cy.getByTestid('page-accessories-modal').parent().should('have.class','show').within(() => {
       cy.get('.nav-title > li').eq(1).find('a').click();
     });
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(500); // wait for loading wiki
     cy.screenshot(`${ssPrefix}1-timeline-list`, {capture: 'viewport'});
   });
 
@@ -86,6 +90,9 @@ context('Access to timeline', () => {
       cy.get('.nav-title > li').eq(1).find('a').click();
       cy.get('button.close').eq(0).click();
     });
+    cy.get('.modal').should('be.visible').scrollTo('top');
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(500); // wait for loading wiki
     cy.screenshot(`${ssPrefix}2-timeline-list-fullscreen`, {capture: 'viewport'});
     cy.getByTestid('page-accessories-modal').parent().should('have.class','show').within(() => {
       cy.get('button.close').eq(1).click();

+ 82 - 10
packages/app/test/cypress/integration/20-basic-features/use-tools.spec.ts

@@ -9,6 +9,7 @@ context('Switch Sidebar content', () => {
   });
 
   it('PageTree is successfully shown', () => {
+    cy.collapseSidebar(false);
     cy.visit('/page');
     cy.getByTestid('grw-sidebar-nav-primary-page-tree').click();
     // eslint-disable-next-line cypress/no-unnecessary-waiting
@@ -28,20 +29,75 @@ context('Modal for page operation', () => {
     cy.fixture("user-admin.json").then(user => {
       cy.login(user.username, user.password);
     });
+    cy.collapseSidebar(true);
   });
+  it("PageCreateModal is shown and closed successfully", () => {
+    cy.visit('/');
+    cy.getByTestid('newPageBtn').click();
 
-  it("PageCreateModal is shown successfully", () => {
-    cy.visit('/me');
+    cy.getByTestid('page-create-modal').should('be.visible').within(() => {
+      cy.screenshot(`${ssPrefix}new-page-modal-opened`);
+      cy.get('button.close').click();
 
+    });
+    cy.screenshot(`${ssPrefix}page-create-modal-closed`, {capture: 'viewport'});
+  });
+  it("Successfully Create Today's page", () => {
+    const pageName = "Today's page";
+    cy.visit('/');
     cy.getByTestid('newPageBtn').click();
 
-    cy.getByTestid('page-create-modal').should('be.visible').screenshot(`${ssPrefix}-open`);
+    cy.getByTestid('page-create-modal').should('be.visible').within(() => {
+      cy.get('.page-today-input2').type(pageName);
+      cy.screenshot(`${ssPrefix}today-add-page-name`);
+      cy.getByTestid('btn-create-memo').click();
+    });
+    cy.getByTestid('page-editor').should('be.visible');
+    cy.getByTestid('save-page-btn').click();
+    cy.get('body').should('not.have.class', 'on-edit');
 
-    cy.getByTestid('row-create-page-under-below').find('input.form-control').clear().type('/new-page');
-    cy.getByTestid('btn-create-page-under-below').click();
+    cy.getByTestid('grw-contextual-sub-nav').should('be.visible');
+    cy.screenshot(`${ssPrefix}create-today-page`);
+  });
+  it('Successfully create page under specific path', () => {
+    const pageName = 'child';
 
+    cy.visit('/SandBox');
+    cy.getByTestid('newPageBtn').click();
+
+    cy.getByTestid('page-create-modal').should('be.visible').within(() => {
+      cy.get('.rbt-input-main').type(pageName);
+      cy.screenshot(`${ssPrefix}under-path-add-page-name`);
+      cy.getByTestid('btn-create-page-under-below').click();
+    });
     cy.getByTestid('page-editor').should('be.visible');
-    cy.screenshot(`${ssPrefix}-create-clicked`, {capture: 'viewport'});
+    cy.getByTestid('save-page-btn').click();
+    cy.get('body').should('not.have.class', 'on-edit');
+
+    cy.getByTestid('grw-contextual-sub-nav').should('be.visible');
+    cy.screenshot(`${ssPrefix}create-page-under-specific-page`);
+  });
+
+  it('Trying to create template page under the root page fail', () => {
+    cy.visit('/');
+    cy.getByTestid('newPageBtn').click();
+
+    cy.getByTestid('page-create-modal').should('be.visible').within(() => {
+      cy.get('#template-type').click();
+      cy.get('#template-type').next().find('button:eq(0)').click({force: true});
+      cy.get('#dd-template-type').next().find('button').click({force: true});
+    });
+    cy.get('.toast-error').should('be.visible').invoke('attr', 'style', 'opacity: 1');
+    cy.screenshot(`${ssPrefix}create-template-for-children-error`, {capture: 'viewport'});
+    cy.get('.toast-error').should('be.visible').click();
+
+    cy.getByTestid('page-create-modal').should('be.visible').within(() => {
+      cy.get('#template-type').click();
+      cy.get('#template-type').next().find('button:eq(1)').click({force: true});
+      cy.get('#dd-template-type').next().find('button').click({force: true});
+    });
+    cy.get('.toast-error').should('be.visible').invoke('attr', 'style', 'opacity: 1');
+    cy.screenshot(`${ssPrefix}create-template-for-descendants-error`, {capture: 'viewport'});
   });
 
   it('PageDeleteModal is shown successfully', () => {
@@ -89,6 +145,7 @@ context('Open presentation modal', () => {
     cy.fixture("user-admin.json").then(user => {
       cy.login(user.username, user.password);
     });
+    cy.collapseSidebar(true);
   });
 
   it('PresentationModal for "/" is shown successfully', () => {
@@ -115,6 +172,7 @@ context('Page Accessories Modal', () => {
     cy.fixture("user-admin.json").then(user => {
       cy.login(user.username, user.password);
     });
+    cy.collapseSidebar(true);
   });
 
   it('Page History is shown successfully', () => {
@@ -160,6 +218,7 @@ context('Tag Oprations', () =>{
     cy.fixture("user-admin.json").then(user => {
       cy.login(user.username, user.password);
     });
+    cy.collapseSidebar(true);
   });
 
   it('Successfully add new tag', () => {
@@ -188,8 +247,10 @@ context('Tag Oprations', () =>{
       cy.get('div.modal-footer > button').click();
     });
 
+    cy.get('.toast').should('be.visible').trigger('mouseover');
     cy.get('.grw-taglabels-container > form > a').contains(tag).should('exist');
-
+    /* eslint-disable cypress/no-unnecessary-waiting */
+    cy.wait(150); // wait for toastr to change its color occured by mouseover
     cy.screenshot(`${ssPrefix}4-click-done`, {capture: 'viewport'});
 
   });
@@ -203,9 +264,14 @@ context('Tag Oprations', () =>{
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
+    // force to add 'active' to pass VRT: https://github.com/weseek/growi/pull/6603
+    cy.getByTestid('page-list-item-L').first().invoke('addClass', 'active');
     cy.screenshot(`${ssPrefix}1-click-tag-name`, {capture: 'viewport'});
 
     cy.getByTestid('open-page-item-control-btn').first().click({force: true});
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(1500); // for wait rendering pagelist info
     cy.screenshot(`${ssPrefix}2-click-three-dots-menu`, {capture: 'viewport'});
 
     cy.getByTestid('open-page-duplicate-modal-btn').first().click({force: true});
@@ -233,24 +299,29 @@ context('Tag Oprations', () =>{
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
     cy.screenshot(`${ssPrefix}1-click-tag-name`, {capture: 'viewport'});
 
     cy.getByTestid('search-result-list').within(() => {
       cy.get('.list-group-item').each(($row) => {
         if($row.find('a').text() === oldPageName){
           cy.wrap($row).within(() => {
-            cy.getByTestid('open-page-item-control-btn').click();
+            cy.getByTestid('open-page-item-control-btn').first().click();
+            cy.getByTestid('page-item-control-menu').should('have.class', 'show').first().within(() => {
+            // eslint-disable-next-line cypress/no-unnecessary-waiting
+            cy.wait(300);
+            cy.screenshot(`${ssPrefix}2-open-page-item-control-menu`);
+            })
           });
         }
       });
     });
-    cy.screenshot(`${ssPrefix}2-click-three-dots-menu`, {capture: 'viewport'});
 
     cy.getByTestid('search-result-list').within(() => {
       cy.get('.list-group-item').each(($row) => {
         if($row.find('a').text() === oldPageName){
           cy.wrap($row).within(() => {
-            cy.getByTestid('open-page-move-rename-modal-btn').click();
+            cy.getByTestid('open-page-move-rename-modal-btn').click({force: true});
           });
         }
       });
@@ -265,6 +336,7 @@ context('Tag Oprations', () =>{
     });
 
     cy.visit(`/${newPageName}`);
+    cy.getByTestid('grw-tag-labels').should('be.visible');
     cy.screenshot(`${ssPrefix}4-new-page-name-applied`, {capture: 'viewport'});
   });
 

+ 14 - 10
packages/app/test/cypress/integration/21-basic-features-for-guest/access-to-page.spec.ts

@@ -1,18 +1,18 @@
 context('Access to page by guest', () => {
   const ssPrefix = 'access-to-page-by-guest-';
 
-  beforeEach(() => {
-    // collapse sidebar
-    cy.collapseSidebar(true);
-  });
-
   it('/Sandbox is successfully loaded', () => {
     cy.visit('/Sandbox', {  });
+    cy.collapseSidebar(true, true);
     cy.screenshot(`${ssPrefix}-sandbox`);
   });
 
   it('/Sandbox with anchor hash is successfully loaded', () => {
     cy.visit('/Sandbox#Headers');
+    cy.collapseSidebar(true, true);
+
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(500);
 
     // hide fab
     cy.getByTestid('grw-fab-container').invoke('attr', 'style', 'display: none');
@@ -22,14 +22,22 @@ context('Access to page by guest', () => {
 
   it('/Sandbox/Math is successfully loaded', () => {
     cy.visit('/Sandbox/Math');
+    cy.collapseSidebar(true, true);
 
     cy.get('mjx-container').should('be.visible');
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(2000); // wait for 2 seconds for MathJax.typesetPromise();
 
     cy.screenshot(`${ssPrefix}-sandbox-math`);
   });
 
   it('/Sandbox with edit is successfully loaded', () => {
     cy.visit('/Sandbox#edit');
+    cy.collapseSidebar(true, true);
+
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(1000);
+
     cy.screenshot(`${ssPrefix}-sandbox-edit-page`);
   })
 
@@ -55,13 +63,9 @@ context('Access to /me page', () => {
 context('Access to special pages by guest', () => {
   const ssPrefix = 'access-to-special-pages-by-guest-';
 
-  beforeEach(() => {
-    // collapse sidebar
-    cy.collapseSidebar(true);
-  });
-
   it('/trash is successfully loaded', () => {
     cy.visit('/trash', {  });
+    cy.collapseSidebar(true, true);
     cy.getByTestid('trash-page-list').should('be.visible');
     cy.screenshot(`${ssPrefix}-trash`);
   });

+ 53 - 12
packages/app/test/cypress/integration/30-search/search.spec.ts

@@ -16,8 +16,10 @@ context('Access to search result page', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
-
-    cy.screenshot(`${ssPrefix}-with-q`);
+    cy.get('#wiki').should('be.visible');
+    // for avoid mismatch by auto scrolling
+    cy.get('.search-result-content-body-container').scrollTo('top');
+    cy.screenshot(`${ssPrefix}with-q`);
   });
 
   it('checkboxes behaviors', () => {
@@ -26,27 +28,31 @@ context('Access to search result page', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
+    // for avoid mismatch by auto scrolling
+    cy.get('.search-result-content-body-container').scrollTo('top');
+
+    // force to add 'active' to pass VRT: https://github.com/weseek/growi/pull/6603
+    cy.getByTestid('page-list-item-L').first().invoke('addClass', 'active');
 
     cy.getByTestid('cb-select').first().click({force: true});
-    cy.screenshot(`${ssPrefix}-the-first-checkbox-on`);
+    cy.screenshot(`${ssPrefix}the-first-checkbox-on`);
     cy.getByTestid('cb-select').first().click({force: true});
-    cy.screenshot(`${ssPrefix}-the-first-checkbox-off`);
+    cy.screenshot(`${ssPrefix}the-first-checkbox-off`);
 
     // click select all checkbox
     cy.getByTestid('cb-select-all').click({force: true});
-    cy.screenshot(`${ssPrefix}-the-select-all-checkbox-1`);
+    cy.screenshot(`${ssPrefix}the-select-all-checkbox-1`);
     cy.getByTestid('cb-select').first().click({force: true});
-    cy.screenshot(`${ssPrefix}-the-select-all-checkbox-2`);
+    cy.screenshot(`${ssPrefix}the-select-all-checkbox-2`);
     cy.getByTestid('cb-select').first().click({force: true});
-    cy.screenshot(`${ssPrefix}-the-select-all-checkbox-3`);
+    cy.screenshot(`${ssPrefix}the-select-all-checkbox-3`);
     cy.getByTestid('cb-select-all').click({force: true});
-    cy.screenshot(`${ssPrefix}-the-select-all-checkbox-4`);
+    cy.screenshot(`${ssPrefix}the-select-all-checkbox-4`);
   });
 
 });
 
-
-
 context('Access to legacy private pages', () => {
   const ssPrefix = 'access-to-legacy-private-pages-directly-';
 
@@ -63,8 +69,9 @@ context('Access to legacy private pages', () => {
     cy.visit('/_private-legacy-pages');
 
     cy.getByTestid('search-result-base').should('be.visible');
+    cy.getByTestid('search-result-private-legacy-pages').should('be.visible');
 
-    cy.screenshot(`${ssPrefix}-shown`);
+    cy.screenshot(`${ssPrefix}shown`);
   });
 
 });
@@ -98,9 +105,20 @@ context('Search all pages', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
+    // force to add 'active' to pass VRT: https://github.com/weseek/growi/pull/6603
+    cy.getByTestid('page-list-item-L').first().invoke('addClass', 'active');
+    // for avoid mismatch by auto scrolling
+    cy.get('.search-result-content-body-container').scrollTo('top');
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(1500);
     cy.screenshot(`${ssPrefix}3-search-page-results`, { capture: 'viewport'});
 
     cy.getByTestid('open-page-item-control-btn').eq(1).click();
+    cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
+    // for avoid mismatch by auto scrolling
+    cy.get('.search-result-content-body-container').scrollTo('top');
     cy.screenshot(`${ssPrefix}4-click-three-dots-menu`, {capture: 'viewport'});
 
     //Add bookmark
@@ -158,12 +176,18 @@ context('Search all pages', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
-
+    cy.get('#wiki').should('be.visible');
+    // force to add 'active' to pass VRT: https://github.com/weseek/growi/pull/6603
+    cy.getByTestid('page-list-item-L').first().invoke('addClass', 'active');
     cy.screenshot(`${ssPrefix}2-search-with-tag-result`, {capture: 'viewport'});
+
     cy.getByTestid('open-page-item-control-btn').first().click();
+    cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
     cy.screenshot(`${ssPrefix}3-click-three-dots-menu-search-with-tag`, {capture: 'viewport'});
 
   });
+
   it('Successfully order page search results by tag', () => {
     const tag = 'help';
 
@@ -172,6 +196,9 @@ context('Search all pages', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
+    // force to add 'active' to pass VRT: https://github.com/weseek/growi/pull/6603
+    cy.getByTestid('page-list-item-L').first().invoke('addClass', 'active');
     cy.screenshot(`${ssPrefix}1-tag-order-click-tag-name`, {capture: 'viewport'});
 
     cy.get('.grw-search-page-nav').within(() => {
@@ -182,6 +209,7 @@ context('Search all pages', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
     cy.screenshot(`${ssPrefix}2-tag-order-by-relevance`);
 
     cy.get('.grw-search-page-nav').within(() => {
@@ -192,6 +220,7 @@ context('Search all pages', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
     cy.screenshot(`${ssPrefix}3-tag-order-by-creation-date`);
 
     cy.get('.grw-search-page-nav').within(() => {
@@ -202,6 +231,7 @@ context('Search all pages', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
     cy.screenshot(`${ssPrefix}4-tag-order-by-last-update-date`);
   });
 
@@ -235,9 +265,20 @@ context('Search current tree with "prefix":', () => {
     cy.getByTestid('search-result-base').should('be.visible');
     cy.getByTestid('search-result-list').should('be.visible');
     cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
+    // force to add 'active' to pass VRT: https://github.com/weseek/growi/pull/6603
+    cy.getByTestid('page-list-item-L').first().invoke('addClass', 'active');
+    // for avoid mismatch by auto scrolling
+    cy.get('.search-result-content-body-container').scrollTo('top');
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(1500);
     cy.screenshot(`${ssPrefix}3-search-page-results`, { capture: 'viewport'});
 
     cy.getByTestid('open-page-item-control-btn').first().click();
+    cy.getByTestid('search-result-content').should('be.visible');
+    cy.get('#wiki').should('be.visible');
+    // for avoid mismatch by auto scrolling
+    cy.get('.search-result-content-body-container').scrollTo('top');
     cy.screenshot(`${ssPrefix}4-click-three-dots-menu`, {capture: 'viewport'});
   });
 

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

@@ -51,6 +51,8 @@ context('Access to Admin page', () => {
   it('/admin/customize is successfully loaded', () => {
     cy.visit('/admin/customize');
     cy.getByTestid('admin-customize').should('be.visible');
+    /* eslint-disable cypress/no-unnecessary-waiting */
+    cy.wait(500); // wait for loading layout image
     cy.screenshot(`${ssPrefix}-admin-customize`);
   });
 

+ 21 - 3
packages/app/test/cypress/integration/50-sidebar/access-to-side-bar.spec.ts

@@ -27,13 +27,20 @@ context('Access to sidebar', () => {
     });
 
     cy.getByTestid('grw-recent-changes').should('be.visible');
+    cy.get('.list-group-item').should('be.visible');
 
-    cy.getByTestid('grw-contextual-navigation-sub').screenshot(`${ssPrefix}recent-changes-1-page-list`);
+    // Avoid blackout misalignment
+    cy.scrollTo('center');
+    cy.screenshot(`${ssPrefix}recent-changes-1-page-list`);
 
     cy.get('#grw-sidebar-contents-wrapper').within(() => {
       cy.get('#recentChangesResize').click({force: true});
-      cy.screenshot(`${ssPrefix}recent-changes-2-switch-sidebar-size`);
+      cy.get('.list-group-item').should('be.visible');
     });
+
+    // Avoid blackout misalignment
+    cy.scrollTo('center');
+    cy.screenshot(`${ssPrefix}recent-changes-2-switch-sidebar-size`);
   });
 
   it('Successfully create a custom sidebar page', () => {
@@ -52,8 +59,17 @@ context('Access to sidebar', () => {
     cy.get('.grw-sidebar-content-header.h5').find('a').click();
     cy.get('.CodeMirror textarea').type(content, {force: true});
     cy.screenshot(`${ssPrefix}custom-sidebar-2-custom-sidebar-editor`);
-    cy.get('.dropup > .btn-submit').click();
+    cy.getByTestid('save-page-btn').click();
     cy.get('body').should('not.have.class', 'on-edit');
+
+    // What to do when UserUISettings is not saved in time
+    cy.getByTestid('grw-sidebar-nav-primary-custom-sidebar').then(($el) => {
+      if (!$el.hasClass('active')) {
+        cy.wrap($el).click();
+      }
+    });
+
+    cy.get('.grw-custom-sidebar-content').should('be.visible');
     cy.getByTestid('grw-contextual-navigation-sub').screenshot(`${ssPrefix}custom-sidebar-3-custom-sidebar-created`);
   });
 
@@ -142,6 +158,7 @@ context('Access to sidebar', () => {
 
   it('Successfully access to My Drafts page', () => {
     cy.visit('/');
+    cy.collapseSidebar(true);
     cy.get('.grw-sidebar-nav-secondary-container').within(() => {
       cy.get('a[href*="/me/drafts"]').click();
     });
@@ -159,6 +176,7 @@ context('Access to sidebar', () => {
 
   it('Successfully access to trash page', () => {
     cy.visit('/');
+    cy.collapseSidebar(true);
     cy.get('.grw-sidebar-nav-secondary-container').within(() => {
       cy.get('a[href*="/trash"]').click();
     });

+ 8 - 3
packages/app/test/cypress/integration/50-sidebar/switching-sidebar-mode.spec.ts

@@ -21,13 +21,18 @@ context('Switch sidebar mode', () => {
 
   it('Switching sidebar mode', () => {
     cy.visit('/');
-    cy.get('.grw-apperance-mode-dropdown').click();
+    cy.collapseSidebar(true, true)
+    cy.get('.grw-apperance-mode-dropdown').first().click();
 
     cy.get('[for="swSidebarMode"]').click({force: true});
-    cy.screenshot(`${ssPrefix}-switch-sidebar-mode`, { capture: 'viewport' });
+    cy.screenshot(`${ssPrefix}-switch-sidebar-mode`, {
+      blackout: ['#revision-toc', '[data-hide-in-vrt=true]'],
+    })
 
     cy.get('[for="swSidebarMode"]').click({force: true});
-    cy.screenshot(`${ssPrefix}-switch-sidebar-mode-back`, { capture: 'viewport' });
+    cy.screenshot(`${ssPrefix}-switch-sidebar-mode-back`, {
+      blackout: ['#revision-toc', '[data-hide-in-vrt=true]'],
+    })
   });
 
 });

+ 2 - 2
packages/app/test/cypress/support/commands.ts

@@ -40,9 +40,9 @@ Cypress.Commands.add('login', (username, password) => {
 
 let isSidebarCollapsed: boolean | undefined;
 
-Cypress.Commands.add('collapseSidebar', (isCollapsed) => {
+Cypress.Commands.add('collapseSidebar', (isCollapsed, force=false) => {
 
-  if (isSidebarCollapsed === isCollapsed) {
+  if (!force && isSidebarCollapsed === isCollapsed) {
     return;
   }
 

+ 1 - 1
packages/app/test/cypress/support/index.ts

@@ -36,7 +36,7 @@ declare global {
     interface Chainable {
        getByTestid(selector: string, options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<JQuery<Element>>,
        login(username: string, password: string): Chainable<void>,
-       collapseSidebar(isCollapsed: boolean): Chainable<void>,
+       collapseSidebar(isCollapsed: boolean, force?: boolean): Chainable<void>,
     }
   }
 }

+ 1 - 1
packages/core/src/utils/template-checker.ts

@@ -3,7 +3,7 @@
  */
 
 export function checkTemplatePath(path: string): boolean {
-  if (path.match(/.*\/_{1,2}template$/)) {
+  if (path.match(/^.*\/_{1,2}template$/)) {
     return true;
   }