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

Merge branch 'master' into feat/sticky-event-features

jam411 3 лет назад
Родитель
Сommit
07c1889cc6
66 измененных файлов с 384 добавлено и 209 удалено
  1. 2 2
      .github/workflows/release.yml
  2. 11 0
      .mergify.yml
  3. 36 2
      CHANGELOG.md
  4. 1 1
      lerna.json
  5. 1 1
      package.json
  6. 1 1
      packages/app/docker/README.md
  7. 10 10
      packages/app/package.json
  8. 7 0
      packages/app/public/static/locales/en_US/translation.json
  9. 8 0
      packages/app/public/static/locales/ja_JP/translation.json
  10. 7 0
      packages/app/public/static/locales/zh_CN/translation.json
  11. 2 3
      packages/app/src/client/services/layout.ts
  12. 1 1
      packages/app/src/components/Admin/AdminHome/SystemInfomationTable.tsx
  13. 31 42
      packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx
  14. 4 1
      packages/app/src/components/InAppNotification/InAppNotificationDropdown.tsx
  15. 1 1
      packages/app/src/components/Layout/MainPane.tsx
  16. 1 1
      packages/app/src/components/Me/ApiSettings.tsx
  17. 2 2
      packages/app/src/components/Me/ProfileImageSettings.tsx
  18. 1 1
      packages/app/src/components/Navbar/AuthorInfo.tsx
  19. 12 5
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  20. 8 8
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  21. 4 1
      packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.jsx
  22. 16 8
      packages/app/src/components/Page.tsx
  23. 2 2
      packages/app/src/components/Page/DisplaySwitcher.tsx
  24. 5 3
      packages/app/src/components/Page/TagEditModal.jsx
  25. 3 1
      packages/app/src/components/Page/TagsInput.tsx
  26. 1 1
      packages/app/src/components/PageAlert/TrashPageAlert.tsx
  27. 6 1
      packages/app/src/components/PageEditor/DrawioModal.tsx
  28. 6 1
      packages/app/src/components/PageEditor/ScrollSyncHelper.js
  29. 1 1
      packages/app/src/components/SearchPage.tsx
  30. 1 1
      packages/app/src/components/Sidebar/RecentChanges.tsx
  31. 1 1
      packages/app/src/components/Sidebar/SidebarNav.tsx
  32. 1 1
      packages/app/src/components/User/UserDate.jsx
  33. 3 3
      packages/app/src/pages/[[...path]].page.tsx
  34. 2 2
      packages/app/src/pages/_app.page.tsx
  35. 4 1
      packages/app/src/pages/admin/customize.page.tsx
  36. 24 4
      packages/app/src/pages/share/[[...path]].page.tsx
  37. 5 4
      packages/app/src/pages/utils/commons.ts
  38. 8 0
      packages/app/src/server/middlewares/certify-brand-logo.ts
  39. 6 0
      packages/app/src/server/middlewares/login-required.js
  40. 0 3
      packages/app/src/server/models/config.ts
  41. 0 14
      packages/app/src/server/routes/apiv3/customize-setting.js
  42. 10 0
      packages/app/src/server/routes/attachment.js
  43. 4 0
      packages/app/src/server/routes/index.js
  44. 11 0
      packages/app/src/server/service/attachment.js
  45. 1 1
      packages/app/src/services/renderer/rehype-plugins/add-line-number-attribute.ts
  46. 8 4
      packages/app/src/stores/context.tsx
  47. 1 1
      packages/app/src/stores/editor.tsx
  48. 3 1
      packages/app/src/stores/in-app-notification.ts
  49. 18 7
      packages/app/src/stores/page.tsx
  50. 5 4
      packages/app/src/stores/personal-settings.tsx
  51. 5 1
      packages/app/src/stores/user.tsx
  52. 1 3
      packages/app/test/cypress/integration/10-install/10-install--install.spec.ts
  53. 26 32
      packages/app/test/cypress/integration/50-sidebar/50-sidebar--access-to-side-bar.spec.ts
  54. 10 4
      packages/app/test/cypress/integration/50-sidebar/50-sidebar--switching-sidebar-mode.spec.ts
  55. 14 0
      packages/app/test/cypress/support/blackout.ts
  56. 6 1
      packages/app/test/cypress/support/screenshot.ts
  57. 1 1
      packages/codemirror-textlint/package.json
  58. 1 1
      packages/core/package.json
  59. 1 1
      packages/hackmd/package.json
  60. 1 1
      packages/preset-themes/package.json
  61. 1 1
      packages/remark-drawio/package.json
  62. 1 1
      packages/remark-growi-directive/package.json
  63. 4 4
      packages/remark-lsx/package.json
  64. 1 1
      packages/slack/package.json
  65. 2 2
      packages/slackbot-proxy/package.json
  66. 2 2
      packages/ui/package.json

+ 2 - 2
.github/workflows/release.yml

@@ -116,8 +116,8 @@ jobs:
         source_branch: support/prepare-v${{ steps.package-json.outputs.packageVersion }}
         destination_branch: master
         pr_title: Prepare v${{ steps.package-json.outputs.packageVersion }}
-        pr_label: exclude from changelog
-        pr_body: "An automated PR generated by ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
+        pr_label: exclude from changelog,prepare next version
+        pr_body: "An automated PR generated by create-pr-for-next-rc"
         github_token: ${{ secrets.GITHUB_TOKEN }}
 
 

+ 11 - 0
.mergify.yml

@@ -11,3 +11,14 @@ pull_request_rules:
     actions:
       merge:
         method: merge
+
+  - name: Automatic merge for Preparing next version
+    conditions:
+      - author = github-actions[bot]
+      - '#approved-reviews-by >= 1'
+      - check-skipped = "test (16.x)"
+      - check-skipped = "test-prod-node14 / launch-prod"
+      - check-skipped = "test-prod-node16 / launch-prod"
+    actions:
+      merge:
+        method: merge

+ 36 - 2
CHANGELOG.md

@@ -1,10 +1,36 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.0.0...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.0.1...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
-## [v6.0.0](https://github.com/weseek/growi/compare/v5.1.7...v6.0.0) - 2022-12-27
+## [v6.0.1](https://github.com/weseek/growi/compare/v6.0.0...v6.0.1) - 2023-01-07
+
+### 🚀 Improvement
+
+- imprv: Reduce frequent API calling by SWR (#7218) @yuki-takei
+- imprv: Do not use api for fetching pages when using shared pages (#7213) @miya
+
+### 🐛 Bug Fixes
+
+- fix: Custom logo not displayed on shared page (#7205) @miya
+- fix: Attach i18n User Setting and TagEditModal (#7216) @jam411
+- fix: Make PLANTUML_URI v5.x compatible (#7215) @yuki-takei
+- fix: Launch with PROMSTER_ENABLED=true failed (#7210) @yuki-takei
+- fix: Lsx performs with strange behavior (#7209) @yuki-takei
+
+### 🧰 Maintenance
+
+- support: Arm architecture (#7212) @yuki-takei
+- ci(deps): bump anothrNick/github-tag-action from 1.38.0 to 1.56.0 (#7195) @dependabot
+- ci(deps): bump google-github-actions/setup-gcloud from 0 to 1 (#7193) @dependabot
+- ci(deps): bump github/codeql-action from 1 to 2 (#7194) @dependabot
+- ci(deps): bump flat from 5.0.0 to 5.0.2 (#7200) @dependabot
+- ci(deps): bump json5 from 1.0.1 to 1.0.2 (#7201) @dependabot
+- ci(Mergify): configuration update (#7202) @yuki-takei
+- support: Uninstall swig-template (#7192) @yuki-takei
+
+## [v6.0.0](https://github.com/weseek/growi/compare/v5.1.8...v6.0.0) - 2022-12-27
 
 ### 💎 Features
 
@@ -17,6 +43,14 @@
 - support: Request scoped SWR (#6742) @yuki-takei
 - support: Build preset themes within external package (#7057) @yuki-takei
 
+## [v5.1.8](https://github.com/weseek/growi/compare/v5.1.7...v5.1.8) - 2022-11-17
+
+### 🐛 Bug Fixes
+
+- fix: Put back page from trash (#6835) @yukendev
+- fix: Updating page content width is not working (#6914) @yukendev
+- fix: Create page at installer (#6930) @hakumizuki @yuki-takei
+
 ## [v5.1.7](https://github.com/weseek/growi/compare/v5.1.6...v5.1.7) - 2022-10-26
 
 ### 🐛 Bug Fixes

+ 1 - 1
lerna.json

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

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "6.0.1-RC.0",
+  "version": "6.0.2-RC.0",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

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

@@ -10,7 +10,7 @@ GROWI Official docker image
 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)
+* [`6.0.1`, `6.0`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.0.1/packages/app/docker/Dockerfile)
 * [`5.1.7`, `5.1`, `5`](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
 * [`5.1.7-nocdn`, `5.1-nocdn`, `5-nocdn`](https://github.com/weseek/growi/blob/v5.1.7/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)

+ 10 - 10
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "6.0.1-RC.0",
+  "version": "6.0.2-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -66,14 +66,14 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^6.0.1-RC.0",
-    "@growi/core": "^6.0.1-RC.0",
-    "@growi/hackmd": "^6.0.1-RC.0",
-    "@growi/preset-themes": "^6.0.1-RC.0",
-    "@growi/remark-drawio": "^6.0.1-RC.0",
-    "@growi/remark-growi-directive": "^6.0.1-RC.0",
-    "@growi/remark-lsx": "^6.0.1-RC.0",
-    "@growi/slack": "^6.0.1-RC.0",
+    "@growi/codemirror-textlint": "^6.0.2-RC.0",
+    "@growi/core": "^6.0.2-RC.0",
+    "@growi/hackmd": "^6.0.2-RC.0",
+    "@growi/preset-themes": "^6.0.2-RC.0",
+    "@growi/remark-drawio": "^6.0.2-RC.0",
+    "@growi/remark-growi-directive": "^6.0.2-RC.0",
+    "@growi/remark-lsx": "^6.0.2-RC.0",
+    "@growi/slack": "^6.0.2-RC.0",
     "@promster/express": "^7.0.6",
     "@promster/server": "^7.0.8",
     "@slack/web-api": "^6.2.4",
@@ -200,7 +200,7 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   "devDependencies": {
-    "@growi/ui": "^6.0.1-RC.0",
+    "@growi/ui": "^6.0.2-RC.0",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@next/bundle-analyzer": "^12.2.3",

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

@@ -798,5 +798,12 @@
   "v5_page_migration": {
     "page_tree_not_avaliable" : "Page tree feature is not available yet.",
     "go_to_settings": "Go to settings to enable the feature"
+  },
+  "tag_edit_modal": {
+    "edit_tags": "Edit Tags",
+    "done": "Done",
+    "tags_input": {
+      "tag_name": "tag name"
+    }
   }
 }

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

@@ -80,6 +80,7 @@
   "View diff": "差分を表示",
   "No diff": "差分なし",
   "User ID": "ユーザーID",
+  "User Settings": "ユーザー設定",
   "User Information": "ユーザー情報",
   "Basic Info": "ユーザーの基本情報",
   "Name": "名前",
@@ -797,5 +798,12 @@
   "v5_page_migration": {
     "page_tree_not_avaliable" : "Page Tree 機能は現在使用できません。",
     "go_to_settings": "設定する"
+  },
+  "tag_edit_modal": {
+    "edit_tags": "タグの編集",
+    "done": "完了",
+    "tags_input": {
+      "tag_name": "タグ名"
+    }
   }
 }

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

@@ -802,5 +802,12 @@
   "v5_page_migration": {
     "page_tree_not_avaliable": "Page Tree 功能不可用",
     "go_to_settings": "进入设置,启用该功能"
+  },
+  "tag_edit_modal": {
+    "edit_tags": "编辑标签",
+    "done": "完毕",
+    "tags_input": {
+      "tag_name": "标签名称"
+    }
   }
 }

+ 2 - 3
packages/app/src/client/services/layout.ts

@@ -1,4 +1,4 @@
-import { useIsContainerFluid, useShareLinkId } from '~/stores/context';
+import { useIsContainerFluid } from '~/stores/context';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useEditorMode } from '~/stores/ui';
 
@@ -17,8 +17,7 @@ export const useEditorModeClassName = (): string => {
 };
 
 export const useCurrentGrowiLayoutFluidClassName = (): string => {
-  const { data: shareLinkId } = useShareLinkId();
-  const { data: currentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
+  const { data: currentPage } = useSWRxCurrentPage();
 
   const { data: dataIsContainerFluid } = useIsContainerFluid();
 

+ 1 - 1
packages/app/src/components/Admin/AdminHome/SystemInfomationTable.tsx

@@ -25,7 +25,7 @@ const SystemInformationTable = (props: Props) => {
       <tbody>
         <tr>
           <th>GROWI</th>
-          <td data-hide-in-vrt>{ growiVersion }</td>
+          <td data-vrt-blackout>{ growiVersion }</td>
         </tr>
         <tr>
           <th>node.js</th>

+ 31 - 42
packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx

@@ -1,45 +1,34 @@
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useCallback, useMemo, useState } from 'react';
 
 import { useTranslation } from 'react-i18next';
 
 import { toastError, toastSuccess } from '~/client/util/apiNotification';
 import {
-  apiv3Delete, apiv3Get, apiv3PostForm, apiv3Put,
+  apiv3Delete, apiv3PostForm, apiv3Put,
 } from '~/client/util/apiv3-client';
 import ImageCropModal from '~/components/Common/ImageCropModal';
+import { useIsDefaultLogo, useIsCustomizedLogoUploaded } from '~/stores/context';
 
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
+
 const DEFAULT_LOGO = '/images/logo.svg';
+const CUSTOMIZED_LOGO = '/attachment/brand-logo';
 
 const CustomizeLogoSetting = (): JSX.Element => {
 
   const { t } = useTranslation();
+  const { data: isDefaultLogo } = useIsDefaultLogo();
+  const { data: isCustomizedLogoUploaded, mutate: mutateIsCustomizedLogoUploaded } = useIsCustomizedLogoUploaded();
 
   const [uploadLogoSrc, setUploadLogoSrc] = useState<ArrayBuffer | string | null>(null);
   const [isImageCropModalShow, setIsImageCropModalShow] = useState<boolean>(false);
-  const [isDefaultLogo, setIsDefaultLogo] = useState<boolean>(true);
+  const [isDefaultLogoSelected, setIsDefaultLogoSelected] = useState<boolean>(isDefaultLogo ?? true);
   const [retrieveError, setRetrieveError] = useState<any>();
-  const [customizedLogoSrc, setCustomizedLogoSrc] = useState< string | null >(null);
-
-  const retrieveData = useCallback(async() => {
-    try {
-      const response = await apiv3Get('/customize-setting/customize-logo');
-      const { isDefaultLogo: _isDefaultLogo, customizedLogoSrc } = response.data;
-      const isDefaultLogo = _isDefaultLogo ?? true;
-
-      setIsDefaultLogo(isDefaultLogo);
-      setCustomizedLogoSrc(customizedLogoSrc);
-    }
-    catch (err) {
-      setRetrieveError(err);
-      throw new Error('Failed to fetch data');
-    }
-  }, []);
 
-  useEffect(() => {
-    retrieveData();
-  }, [retrieveData]);
+  const currentLogo = useMemo(() => {
+    return isDefaultLogo ? DEFAULT_LOGO : CUSTOMIZED_LOGO;
+  }, [isDefaultLogo]);
 
   const onSelectFile = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
     if (e.target.files != null && e.target.files.length > 0) {
@@ -52,22 +41,18 @@ const CustomizeLogoSetting = (): JSX.Element => {
 
   const onClickSubmit = useCallback(async() => {
     try {
-      const response = await apiv3Put('/customize-setting/customize-logo', {
-        isDefaultLogo,
-      });
-      const { customizedParams } = response.data;
-      setIsDefaultLogo(customizedParams.isDefaultLogo);
+      await apiv3Put('/customize-setting/customize-logo', { isDefaultLogo: isDefaultLogoSelected });
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_logo'), ns: 'commons' }));
     }
     catch (err) {
       toastError(err);
     }
-  }, [t, isDefaultLogo]);
+  }, [t, isDefaultLogoSelected]);
 
   const onClickDeleteBtn = useCallback(async() => {
     try {
       await apiv3Delete('/customize-setting/delete-brand-logo');
-      setCustomizedLogoSrc(null);
+      mutateIsCustomizedLogoUploaded(false);
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo'), ns: 'commons' }));
     }
     catch (err) {
@@ -75,15 +60,15 @@ const CustomizeLogoSetting = (): JSX.Element => {
       setRetrieveError(err);
       throw new Error('Failed to delete logo');
     }
-  }, [t]);
+  }, [mutateIsCustomizedLogoUploaded, t]);
 
 
   const processImageCompletedHandler = useCallback(async(croppedImage) => {
     try {
       const formData = new FormData();
       formData.append('file', croppedImage);
-      const { data } = await apiv3PostForm('/customize-setting/upload-brand-logo', formData);
-      setCustomizedLogoSrc(data.attachment.filePathProxied);
+      await apiv3PostForm('/customize-setting/upload-brand-logo', formData);
+      mutateIsCustomizedLogoUploaded(true);
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo'), ns: 'commons' }));
     }
     catch (err) {
@@ -91,7 +76,7 @@ const CustomizeLogoSetting = (): JSX.Element => {
       setRetrieveError(err);
       throw new Error('Failed to upload brand logo');
     }
-  }, [t]);
+  }, [mutateIsCustomizedLogoUploaded, t]);
 
   return (
     <React.Fragment>
@@ -109,8 +94,8 @@ const CustomizeLogoSetting = (): JSX.Element => {
                       className="custom-control-input"
                       form="formImageType"
                       name="imagetypeForm[isDefaultLogo]"
-                      checked={isDefaultLogo}
-                      onChange={() => { setIsDefaultLogo(true) }}
+                      checked={isDefaultLogoSelected}
+                      onChange={() => { setIsDefaultLogoSelected(true) }}
                     />
                     <label className="custom-control-label" htmlFor="radioDefaultLogo">
                       {t('admin:customize_settings.default_logo')}
@@ -128,8 +113,8 @@ const CustomizeLogoSetting = (): JSX.Element => {
                       className="custom-control-input"
                       form="formImageType"
                       name="imagetypeForm[isDefaultLogo]"
-                      checked={!isDefaultLogo}
-                      onChange={() => { setIsDefaultLogo(false) }}
+                      checked={!isDefaultLogoSelected}
+                      onChange={() => { setIsDefaultLogoSelected(false) }}
                     />
                     <label className="custom-control-label" htmlFor="radioUploadLogo">
                       { t('admin:customize_settings.upload_logo') }
@@ -141,11 +126,15 @@ const CustomizeLogoSetting = (): JSX.Element => {
                     { t('admin:customize_settings.current_logo') }
                   </label>
                   <div className="col-sm-8 col-12">
-                    <p><img src={customizedLogoSrc || DEFAULT_LOGO} className="picture picture-lg " id="settingBrandLogo" width="64" /></p>
-                    {(customizedLogoSrc != null) && (
-                      <button type="button" className="btn btn-danger" onClick={onClickDeleteBtn}>
-                        { t('admin:customize_settings.delete_logo') }
-                      </button>
+                    {isCustomizedLogoUploaded && (
+                      <>
+                        <p>
+                          <img src='/attachment/brand-logo' className="picture picture-lg " id="settingBrandLogo" width="64" />
+                        </p>
+                        <button type="button" className="btn btn-danger" onClick={onClickDeleteBtn}>
+                          { t('admin:customize_settings.delete_logo') }
+                        </button>
+                      </>
                     )}
                   </div>
                 </div>

+ 4 - 1
packages/app/src/components/InAppNotification/InAppNotificationDropdown.tsx

@@ -26,7 +26,10 @@ export const InAppNotificationDropdown = (): JSX.Element => {
   const limit = 6;
 
   const { data: socket } = useDefaultSocket();
-  const { data: inAppNotificationData, mutate: mutateInAppNotificationData } = useSWRxInAppNotifications(limit);
+  const { data: inAppNotificationData, mutate: mutateInAppNotificationData } = useSWRxInAppNotifications(
+    limit, undefined, undefined,
+    { revalidateOnFocus: isOpen },
+  );
   const { data: inAppNotificationUnreadStatusCount, mutate: mutateInAppNotificationUnreadStatusCount } = useSWRxInAppNotificationStatus();
 
   // ripple

+ 1 - 1
packages/app/src/components/Layout/MainPane.tsx

@@ -24,7 +24,7 @@ export const MainPane = (props: Props): JSX.Element => {
                   <div className="flex-grow-1 flex-basis-0 mw-0">
                     {children}
                   </div>
-                  <div className="grw-side-contents-container d-edit-none">
+                  <div className="grw-side-contents-container d-edit-none" data-vrt-blackout-side-contents>
                     <div className="grw-side-contents-sticky-container">
                       {sideContents}
                     </div>

+ 1 - 1
packages/app/src/components/Me/ApiSettings.tsx

@@ -39,7 +39,7 @@ const ApiSettings = React.memo((): JSX.Element => {
             ? (
               <input
                 data-testid="grw-api-settings-input"
-                data-hide-in-vrt
+                data-vrt-blackout
                 className="form-control"
                 type="text"
                 name="apiToken"

+ 2 - 2
packages/app/src/components/Me/ProfileImageSettings.tsx

@@ -105,14 +105,14 @@ const ProfileImageSettings = (): JSX.Element => {
                 onChange={() => setGravatarEnabled(true)}
               />
               <label className="custom-control-label" htmlFor="radioGravatar">
-                <img src={GRAVATAR_DEFAULT} data-hide-in-vrt /> Gravatar
+                <img src={GRAVATAR_DEFAULT} data-vrt-blackout-profile /> Gravatar
               </label>
               <a href="https://gravatar.com/">
                 <small><i className="icon-arrow-right-circle" aria-hidden="true"></i></small>
               </a>
             </div>
           </h4>
-          <img src={generateGravatarSrc(currentUser.email)} width="64" data-hide-in-vrt />
+          <img src={generateGravatarSrc(currentUser.email)} width="64" data-vrt-blackout-profile />
         </div>
 
         <div className="col-md-6 col-12">

+ 1 - 1
packages/app/src/components/Navbar/AuthorInfo.tsx

@@ -66,7 +66,7 @@ export const AuthorInfo = (props: AuthorInfoProps): JSX.Element => {
       </div>
       <div>
         <div>{infoLabelForSubNav} {userLabel}</div>
-        <div className="text-muted text-date" data-hide-in-vrt>
+        <div className="text-muted text-date" data-vrt-blackout-datetime>
           {renderParsedDate()}
         </div>
       </div>

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

@@ -1,6 +1,8 @@
 import React, { useState, useEffect, useCallback } from 'react';
 
-import { isPopulated, IUser, pagePathUtils } from '@growi/core';
+import {
+  isPopulated, IUser, pagePathUtils, IPagePopulatedToShowRevision,
+} from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import { useRouter } from 'next/router';
@@ -182,16 +184,19 @@ const CreateTemplateMenuItems = (props: CreateTemplateMenuItemsProps): JSX.Eleme
 };
 
 type GrowiContextualSubNavigationProps = {
+  currentPage?: IPagePopulatedToShowRevision,
   isCompactMode?: boolean,
   isLinkSharingDisabled: boolean,
 };
 
 const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps): JSX.Element => {
 
+  const { currentPage } = props;
+
   const router = useRouter();
 
   const { data: shareLinkId } = useShareLinkId();
-  const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
+  const { mutate: mutateCurrentPage } = useSWRxCurrentPage();
 
   const { data: currentPathname } = useCurrentPathname();
   const isSharedPage = pagePathUtils.isSharedPage(currentPathname ?? '');
@@ -314,9 +319,11 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   }, [currentPathname, openDeleteModal, router]);
 
   const switchContentWidthHandler = useCallback(async(pageId: string, value: boolean) => {
-    await updateContentWidth(pageId, value);
-    mutateCurrentPage();
-  }, [mutateCurrentPage]);
+    if (!isSharedPage) {
+      await updateContentWidth(pageId, value);
+      mutateCurrentPage();
+    }
+  }, [isSharedPage, mutateCurrentPage]);
 
   const templateMenuItemClickHandler = useCallback(() => {
     setIsPageTempleteModalShown(true);

+ 8 - 8
packages/app/src/components/Navbar/GrowiNavbar.tsx

@@ -11,7 +11,7 @@ import { useRipple } from 'react-use-ripple';
 import { UncontrolledTooltip } from 'reactstrap';
 
 import {
-  useIsSearchPage, useIsGuestUser, useIsSearchServiceConfigured, useAppTitle, useConfidential, useCustomizedLogoSrc,
+  useIsSearchPage, useIsGuestUser, useIsSearchServiceConfigured, useAppTitle, useConfidential, useIsDefaultLogo,
 } from '~/stores/context';
 import { usePageCreateModal } from '~/stores/modal';
 import { useCurrentPagePath } from '~/stores/page';
@@ -122,16 +122,16 @@ const Confidential: FC<ConfidentialProps> = memo((props: ConfidentialProps): JSX
 Confidential.displayName = 'Confidential';
 
 interface NavbarLogoProps {
-  logoSrc?: string,
+  isDefaultLogo?: boolean
 }
 
 const GrowiNavbarLogo: FC<NavbarLogoProps> = memo((props: NavbarLogoProps) => {
-  const { logoSrc } = props;
+  const { isDefaultLogo } = props;
 
-  return logoSrc != null
+  return isDefaultLogo
+    ? <GrowiLogo />
     // eslint-disable-next-line @next/next/no-img-element
-    ? (<img src={logoSrc} alt="custom logo" className="picture picture-lg p-2 mx-2" id="settingBrandLogo" width="32" />)
-    : <GrowiLogo />;
+    : (<img src='/attachment/brand-logo' alt="custom logo" className="picture picture-lg p-2 mx-2" id="settingBrandLogo" width="32" />);
 });
 
 GrowiNavbarLogo.displayName = 'GrowiNavbarLogo';
@@ -151,7 +151,7 @@ export const GrowiNavbar = (props: Props): JSX.Element => {
   const { data: isSearchServiceConfigured } = useIsSearchServiceConfigured();
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
   const { data: isSearchPage } = useIsSearchPage();
-  const { data: customizedLogoSrc } = useCustomizedLogoSrc();
+  const { data: isDefaultLogo } = useIsDefaultLogo();
 
   return (
     <nav id="grw-navbar" className={`navbar grw-navbar ${styles['grw-navbar']} navbar-expand navbar-dark sticky-top mb-0 px-0`}>
@@ -159,7 +159,7 @@ export const GrowiNavbar = (props: Props): JSX.Element => {
       <div className="navbar-brand mr-0">
         <Link href="/" prefetch={false}>
           <a className="grw-logo d-block">
-            <GrowiNavbarLogo logoSrc={customizedLogoSrc}/>
+            <GrowiNavbarLogo isDefaultLogo={isDefaultLogo} />
           </a>
         </Link>
       </div>

+ 4 - 1
packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.jsx

@@ -9,6 +9,8 @@ import { debounce } from 'throttle-debounce';
 import { useSidebarCollapsed } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 
+import { useSWRxCurrentPage } from '~/stores/page';
+
 import GrowiContextualSubNavigation from './GrowiContextualSubNavigation';
 
 import styles from './GrowiSubNavigationSwitcher.module.scss';
@@ -27,6 +29,7 @@ const logger = loggerFactory('growi:cli:GrowiSubNavigationSticky');
  */
 const GrowiSubNavigationSwitcher = (props) => {
 
+  const { data: currentPage } = useSWRxCurrentPage();
   const { data: isSidebarCollapsed } = useSidebarCollapsed();
 
   const [isVisible, setVisible] = useState(false);
@@ -123,7 +126,7 @@ const GrowiSubNavigationSwitcher = (props) => {
         ref={fixedContainerRef}
         style={{ width }}
       >
-        <GrowiContextualSubNavigation isCompactMode isLinkSharingDisabled />
+        <GrowiContextualSubNavigation currentPage isCompactMode isLinkSharingDisabled />
       </div>
     </div>
   );

+ 16 - 8
packages/app/src/components/Page.tsx

@@ -1,11 +1,11 @@
 import React, {
-  useCallback,
+  FC, useCallback,
   useEffect, useRef,
 } from 'react';
 
 import EventEmitter from 'events';
 
-import { pagePathUtils } from '@growi/core';
+import { pagePathUtils, IPagePopulatedToShowRevision } from '@growi/core';
 import { DrawioEditByViewerProps } from '@growi/remark-drawio';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
@@ -48,9 +48,13 @@ const LinkEditModal = dynamic(() => import('./PageEditor/LinkEditModal'), { ssr:
 
 const logger = loggerFactory('growi:Page');
 
+type Props = {
+  currentPage?: IPagePopulatedToShowRevision,
+}
 
-export const Page = (props) => {
+export const Page: FC<Props> = (props: Props) => {
   const { t } = useTranslation();
+  const { currentPage } = props;
 
   // Pass tocRef to generateViewOptions (=> rehypePlugin => customizeTOC) to call mutateCurrentPageTocNode when tocRef.current changes.
   // The toc node passed by customizeTOC is assigned to tocRef.current.
@@ -64,7 +68,7 @@ export const Page = (props) => {
   const isSharedPage = pagePathUtils.isSharedPage(currentPathname ?? '');
 
   const { data: shareLinkId } = useShareLinkId();
-  const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
+  const { mutate: mutateCurrentPage } = useSWRxCurrentPage();
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { data: tagsInfo } = useSWRxTagsInfo(!isSharedPage ? currentPage?._id : undefined);
   const { data: isGuestUser } = useIsGuestUser();
@@ -128,14 +132,16 @@ export const Page = (props) => {
       toastSuccess(t('toaster.save_succeeded'));
 
       // rerender
-      mutateCurrentPage();
+      if (!isSharedPage) {
+        mutateCurrentPage();
+      }
       mutateEditingMarkdown(newMarkdown);
     }
     catch (error) {
       logger.error('failed to save', error);
       toastError(error);
     }
-  }, [currentPage, mutateCurrentPage, mutateEditingMarkdown, saveOrUpdate, shareLinkId, t, tagsInfo]);
+  }, [currentPage, isSharedPage, mutateCurrentPage, mutateEditingMarkdown, saveOrUpdate, shareLinkId, t, tagsInfo]);
 
   // set handler to open DrawioModal
   useEffect(() => {
@@ -182,14 +188,16 @@ export const Page = (props) => {
       toastSuccess(t('toaster.save_succeeded'));
 
       // rerender
-      mutateCurrentPage();
+      if (!isSharedPage) {
+        mutateCurrentPage();
+      }
       mutateEditingMarkdown(newMarkdown);
     }
     catch (error) {
       logger.error('failed to save', error);
       toastError(error);
     }
-  }, [currentPage, mutateCurrentPage, mutateEditingMarkdown, saveOrUpdate, shareLinkId, t, tagsInfo]);
+  }, [currentPage, isSharedPage, mutateCurrentPage, mutateEditingMarkdown, saveOrUpdate, shareLinkId, t, tagsInfo]);
 
   // set handler to open HandsonTableModal
   useEffect(() => {

+ 2 - 2
packages/app/src/components/Page/DisplaySwitcher.tsx

@@ -34,7 +34,7 @@ const PageView = React.memo((): JSX.Element => {
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: shareLinkId } = useShareLinkId();
   const { data: isNotFound } = useIsNotFound();
-  const { data: currentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
+  const { data: currentPage } = useSWRxCurrentPage();
   const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
 
   const { mutate: mutateIsHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
@@ -92,7 +92,7 @@ const PageView = React.memo((): JSX.Element => {
   return (
     <>
       { isUsersHomePagePath && <UserInfo author={currentPage?.creator} /> }
-      { !isNotFound && <Page /> }
+      { !isNotFound && <Page currentPage={currentPage ?? undefined} /> }
       { isNotFound && <NotFoundPage /> }
     </>
   );

+ 5 - 3
packages/app/src/components/Page/TagEditModal.jsx

@@ -1,6 +1,7 @@
 import React, { useState, useEffect } from 'react';
-import PropTypes from 'prop-types';
 
+import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
@@ -9,6 +10,7 @@ import TagsInput from './TagsInput';
 
 function TagEditModal(props) {
   const [tags, setTags] = useState([]);
+  const { t } = useTranslation();
 
   function onTagsUpdatedByTagsInput(tags) {
     setTags(tags);
@@ -37,14 +39,14 @@ function TagEditModal(props) {
   return (
     <Modal isOpen={props.isOpen} toggle={closeModalHandler} id="edit-tag-modal" autoFocus={false}>
       <ModalHeader tag="h4" toggle={closeModalHandler} className="bg-primary text-light">
-        Edit Tags
+        {t('tag_edit_modal.edit_tags')}
       </ModalHeader>
       <ModalBody>
         <TagsInput tags={tags} onTagsUpdated={onTagsUpdatedByTagsInput} autoFocus />
       </ModalBody>
       <ModalFooter>
         <button type="button" className="btn btn-primary" onClick={handleSubmit}>
-          Done
+          {t('tag_edit_modal.done')}
         </button>
       </ModalFooter>
     </Modal>

+ 3 - 1
packages/app/src/components/Page/TagsInput.tsx

@@ -2,6 +2,7 @@ import React, {
   FC, useRef, useState, useCallback,
 } from 'react';
 
+import { useTranslation } from 'next-i18next';
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 
 import { useSWRxTagsSearch } from '~/stores/tag';
@@ -20,6 +21,7 @@ type Props = {
 }
 
 const TagsInput: FC<Props> = (props: Props) => {
+  const { t } = useTranslation();
   const tagsInputRef = useRef<TypeaheadInstance>(null);
 
   const [resultTags, setResultTags] = useState<string[]>([]);
@@ -71,7 +73,7 @@ const TagsInput: FC<Props> = (props: Props) => {
         onSearch={searchHandler}
         onKeyDown={keyDownHandler}
         options={resultTags} // Search result (Some tag names)
-        placeholder="tag name"
+        placeholder={t('tag_edit_modal.tags_input.tag_name')}
         autoFocus={props.autoFocus}
       />
     </div>

+ 1 - 1
packages/app/src/components/PageAlert/TrashPageAlert.tsx

@@ -114,7 +114,7 @@ export const TrashPageAlert = (): JSX.Element => {
           <br />
           <UserPicture user={deleteUser} />
           <span className="ml-2">
-            Deleted by { deleteUser?.name } at {deletedAt || pageData?.updatedAt}
+            Deleted by { deleteUser?.name } at <span data-vrt-blackout-datetime>{deletedAt || pageData?.updatedAt}</span>
           </span>
         </div>
         <div className="pt-1 d-flex align-items-end align-items-lg-center">

+ 6 - 1
packages/app/src/components/PageEditor/DrawioModal.tsx

@@ -37,7 +37,12 @@ const drawioConfig = {
 
 export const DrawioModal = (): JSX.Element => {
   const { data: drawioUri } = useDrawioUri();
-  const { data: personalSettingsInfo } = usePersonalSettings();
+  const { data: personalSettingsInfo } = usePersonalSettings({
+    // make immutable
+    revalidateIfStale: false,
+    revalidateOnFocus: false,
+    revalidateOnReconnect: false,
+  });
 
   const { data: drawioModalData, close: closeDrawioModal } = useDrawioModal();
   const isOpened = drawioModalData?.isOpened ?? false;

+ 6 - 1
packages/app/src/components/PageEditor/ScrollSyncHelper.js

@@ -77,6 +77,11 @@ class ScrollSyncHelper {
     }
 
     const hiElement = lines[hi];
+
+    if (hiElement == null) {
+      return {};
+    }
+
     if (hi >= 1 && hiElement.element.getBoundingClientRect().top > position) {
       const loElement = lines[lo];
       const bounds = loElement.element.getBoundingClientRect();
@@ -95,7 +100,7 @@ class ScrollSyncHelper {
 
   getEditorLineNumberForPageOffset(parentElement, offset) {
     const { previous, next } = this.getLineElementsAtPageOffset(parentElement, offset);
-    if (previous) {
+    if (previous != null) {
       if (next) {
         const betweenProgress = (
           offset - parentElement.scrollTop - previous.element.getBoundingClientRect().top)

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

@@ -63,7 +63,7 @@ const SearchResultListHead = React.memo((props: SearchResultListHeadProps): JSX.
         <span className="ml-3">{`${leftNum}-${rightNum}`} / {total}</span>
         { took != null && (
           // blackout 70px rectangle in VRT
-          <span data-hide-in-vrt className="ml-3 text-muted d-inline-block" style={{ minWidth: '70px' }}>({took}ms)</span>
+          <span data-vrt-blackout className="ml-3 text-muted d-inline-block" style={{ minWidth: '70px' }}>({took}ms)</span>
         ) }
       </div>
       <div className="input-group flex-nowrap search-result-select-group ml-auto d-md-flex d-none">

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

@@ -42,7 +42,7 @@ const PageItemLower = memo(({ page }: PageItemLowerProps): 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" data-hide-in-vrt>
+      <div className="grw-formatted-distance-date small mt-auto" data-vrt-blackout-datetime>
         <FormattedDistanceDate id={page._id} date={page.updatedAt} />
       </div>
     </div>

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

@@ -85,7 +85,7 @@ export const SidebarNav: FC<Props> = (props: Props) => {
   const { onItemSelected } = props;
 
   return (
-    <div className={`grw-sidebar-nav ${styles['grw-sidebar-nav']}`}>
+    <div className={`grw-sidebar-nav ${styles['grw-sidebar-nav']}`} data-vrt-blackout-sidebar-nav>
       <div className="grw-sidebar-nav-primary-container">
         {/* eslint-disable max-len */}
         <PrimaryItem contents={SidebarContentsType.TREE} label="Page Tree" iconName="format_list_bulleted" onItemSelected={onItemSelected} />

+ 1 - 1
packages/app/src/components/User/UserDate.jsx

@@ -16,7 +16,7 @@ export default class UserDate extends React.Component {
     const dt = format(date, this.props.format);
 
     return (
-      <span className={this.props.className} data-hide-in-vrt>
+      <span className={this.props.className} data-vrt-blackout-datetime>
         {dt}
       </span>
     );

+ 3 - 3
packages/app/src/pages/[[...path]].page.tsx

@@ -71,7 +71,7 @@ import {
   useIsAclEnabled, useIsSearchPage, useTemplateTagData, useTemplateBodyData, useIsEnabledAttachTitleHeader,
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
   useIsSlackConfigured, useRendererConfig,
-  useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useCustomizedLogoSrc, useIsContainerFluid, useIsNotCreatable,
+  useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useIsContainerFluid, useIsNotCreatable,
 } from '../stores/context';
 
 import { NextPageWithLayout } from './_app.page';
@@ -265,7 +265,7 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   useHasDraftOnHackmd(pageWithMeta?.data.hasDraftOnHackmd ?? false);
   useCurrentPathname(props.currentPathname);
 
-  useSWRxCurrentPage(undefined, pageWithMeta?.data ?? null); // store initial data
+  useSWRxCurrentPage(pageWithMeta?.data ?? null); // store initial data
 
   useEditingMarkdown(pageWithMeta?.data.revision?.body);
 
@@ -328,7 +328,7 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
       <div className={`dynamic-layout-root ${growiLayoutFluidClass} h-100 d-flex flex-column justify-content-between`}>
         <header className="py-0 position-relative">
           <div id="grw-subnav-container">
-            <GrowiContextualSubNavigation isLinkSharingDisabled={props.disableLinkSharing} />
+            <GrowiContextualSubNavigation currentPage={pageWithMeta?.data} isLinkSharingDisabled={props.disableLinkSharing} />
           </div>
         </header>
         <div className="d-edit-none">

+ 2 - 2
packages/app/src/pages/_app.page.tsx

@@ -11,7 +11,7 @@ import * as nextI18nConfig from '^/config/next-i18next.config';
 import { ActivatePluginService } from '~/client/services/activate-plugin';
 import { useI18nextHMR } from '~/services/i18next-hmr';
 import {
-  useAppTitle, useConfidential, useGrowiVersion, useSiteUrl, useCustomizedLogoSrc,
+  useAppTitle, useConfidential, useGrowiVersion, useSiteUrl, useIsDefaultLogo,
 } from '~/stores/context';
 import { SWRConfigValue, swrGlobalConfiguration } from '~/utils/swr-utils';
 
@@ -63,7 +63,7 @@ function GrowiApp({ Component, pageProps }: GrowiAppProps): JSX.Element {
   useSiteUrl(commonPageProps.siteUrl);
   useConfidential(commonPageProps.confidential);
   useGrowiVersion(commonPageProps.growiVersion);
-  useCustomizedLogoSrc(commonPageProps.customizedLogoSrc);
+  useIsDefaultLogo(commonPageProps.isDefaultLogo);
 
   // Use the layout defined at the page level, if available
   const getLayout = Component.getLayout ?? (page => page);

+ 4 - 1
packages/app/src/pages/admin/customize.page.tsx

@@ -10,7 +10,7 @@ import { Container, Provider } from 'unstated';
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
-import { useCustomizeTitle, useCurrentUser } from '~/stores/context';
+import { useCustomizeTitle, useCurrentUser, useIsCustomizedLogoUploaded } from '~/stores/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
@@ -20,6 +20,7 @@ const CustomizeSettingContents = dynamic(() => import('~/components/Admin/Custom
 
 type Props = CommonProps & {
   customizeTitle: string,
+  isCustomizedLogoUploaded: boolean,
 };
 
 
@@ -27,6 +28,7 @@ const AdminCustomizeSettingsPage: NextPage<Props> = (props) => {
   const { t } = useTranslation('admin');
   useCustomizeTitle(props.customizeTitle);
   useCurrentUser(props.currentUser ?? null);
+  useIsCustomizedLogoUploaded(props.isCustomizedLogoUploaded);
 
   const componentTitle = t('customize_settings.customize_settings');
   const pageTitle = generateCustomTitle(props, componentTitle);
@@ -57,6 +59,7 @@ const injectServerConfigurations = async(context: GetServerSidePropsContext, pro
   const { crowi } = req;
 
   props.customizeTitle = crowi.configManager.getConfig('crowi', 'customize:title');
+  props.isCustomizedLogoUploaded = await crowi.attachmentService.isBrandLogoExist();
 };
 
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {

+ 24 - 4
packages/app/src/pages/share/[[...path]].page.tsx

@@ -1,12 +1,13 @@
 import React from 'react';
 
-import { IUserHasId } from '@growi/core';
+import { IUserHasId, IPagePopulatedToShowRevision } from '@growi/core';
 import {
   GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import superjson from 'superjson';
 
 import { useCurrentGrowiLayoutFluidClassName } from '~/client/services/layout';
 import { MainPane } from '~/components/Layout/MainPane';
@@ -19,6 +20,7 @@ import { SupportedAction, SupportedActionType } from '~/interfaces/activity';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { RendererConfig } from '~/interfaces/services/renderer';
 import { IShareLinkHasId } from '~/interfaces/share-link';
+import type { PageDocument } from '~/server/models/page';
 import {
   useCurrentUser, useCurrentPathname, useCurrentPageId, useRendererConfig, useIsSearchPage,
   useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault, useDrawioUri, useIsContainerFluid,
@@ -38,6 +40,7 @@ const ShareLinkAlert = dynamic(() => import('~/components/Page/ShareLinkAlert'),
 const ForbiddenPage = dynamic(() => import('~/components/ForbiddenPage'), { ssr: false });
 
 type Props = CommonProps & {
+  shareLinkRelatedPage?: IShareLinkRelatedPage,
   shareLink?: IShareLinkHasId,
   isExpired: boolean,
   disableLinkSharing: boolean,
@@ -48,6 +51,23 @@ type Props = CommonProps & {
   rendererConfig: RendererConfig,
 };
 
+type IShareLinkRelatedPage = IPagePopulatedToShowRevision & PageDocument;
+
+superjson.registerCustom<IShareLinkRelatedPage, string>(
+  {
+    isApplicable: (v): v is IShareLinkRelatedPage => {
+      return v != null
+        && v.toObject != null
+        && v.lastUpdateUser != null
+        && v.creator != null
+        && v.revision != null;
+    },
+    serialize: (v) => { return superjson.stringify(v.toObject()) },
+    deserialize: (v) => { return superjson.parse(v) },
+  },
+  'IShareLinkRelatedPageTransformer',
+);
+
 const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
   useIsSearchPage(false);
   useShareLinkId(props.shareLink?._id);
@@ -91,7 +111,7 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
 
       <div className={`dynamic-layout-root ${growiLayoutFluidClass} h-100 d-flex flex-column justify-content-between`}>
         <header className="py-0 position-relative">
-          {isShowSharedPage && <GrowiContextualSubNavigation isLinkSharingDisabled={props.disableLinkSharing} />}
+          {isShowSharedPage && <GrowiContextualSubNavigation currentPage={props.shareLinkRelatedPage} isLinkSharingDisabled={props.disableLinkSharing} />}
         </header>
 
         <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
@@ -128,7 +148,7 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
           {(isShowSharedPage && shareLink != null) && (
             <>
               <ShareLinkAlert expiredAt={shareLink.expiredAt} createdAt={shareLink.createdAt} />
-              <Page />
+              <Page currentPage={props.shareLinkRelatedPage} />
             </>
           )}
         </MainPane>
@@ -227,7 +247,7 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
     const ShareLinkModel = crowi.model('ShareLink');
     const shareLink = await ShareLinkModel.findOne({ _id: params.linkId }).populate('relatedPage');
     if (shareLink != null) {
-      await shareLink.relatedPage.populateDataToShowRevision();
+      props.shareLinkRelatedPage = await shareLink.relatedPage.populateDataToShowRevision();
       props.isExpired = shareLink.isExpired();
       props.shareLink = shareLink.toObject();
     }

+ 5 - 4
packages/app/src/pages/utils/commons.ts

@@ -20,7 +20,7 @@ export type CommonProps = {
   growiVersion: string,
   isMaintenanceMode: boolean,
   redirectDestination: string | null,
-  customizedLogoSrc?: string,
+  isDefaultLogo: boolean,
   currentUser?: IUser,
 } & Partial<SSRConfig>;
 
@@ -30,7 +30,7 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
   const req = context.req as CrowiRequest<IUserHasId & any>;
   const { crowi, user } = req;
   const {
-    appService, configManager, customizeService,
+    appService, configManager, customizeService, attachmentService,
   } = crowi;
 
   const url = new URL(context.resolvedUrl, 'http://example.com');
@@ -45,7 +45,8 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
 
   // eslint-disable-next-line max-len, no-nested-ternary
   const redirectDestination = !isMaintenanceMode && currentPathname === '/maintenance' ? '/' : isMaintenanceMode && !currentPathname.match('/admin/*') && !(currentPathname === '/maintenance') ? '/maintenance' : null;
-  const isDefaultLogo = crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo');
+  const isCustomizedLogoUploaded = await attachmentService.isBrandLogoExist();
+  const isDefaultLogo = crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo') || !isCustomizedLogoUploaded;
 
   const props: CommonProps = {
     namespacesRequired: ['translation'],
@@ -59,8 +60,8 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
     growiVersion: crowi.version,
     isMaintenanceMode,
     redirectDestination,
-    customizedLogoSrc: isDefaultLogo ? null : configManager.getConfig('crowi', 'customize:customizedLogoSrc'),
     currentUser,
+    isDefaultLogo,
   };
 
   return { props };

+ 8 - 0
packages/app/src/server/middlewares/certify-brand-logo.ts

@@ -0,0 +1,8 @@
+export const generateCertifyBrandLogoMiddleware = (crowi) => {
+
+  return async(req, res, next) => {
+    req.isBrandLogo = true;
+    next();
+  };
+
+};

+ 6 - 0
packages/app/src/server/middlewares/login-required.js

@@ -43,6 +43,12 @@ module.exports = (crowi, isGuestAllowed = false, fallback = null) => {
       return next();
     }
 
+    // Check if it is a Brand logo
+    if (req.isBrandLogo) {
+      logger.debug('Target is Brand logo');
+      return next();
+    }
+
     // is api path
     const baseUrl = req.baseUrl || '';
     if (baseUrl.match(/^\/_api\/.+$/)) {

+ 0 - 3
packages/app/src/server/models/config.ts

@@ -241,9 +241,6 @@ schema.statics.getLocalconfig = function(crowi) {
     globalLang: crowi.configManager.getConfig('crowi', 'app:globalLang'),
     pageLimitationL: crowi.configManager.getConfig('crowi', 'customize:showPageLimitationL'),
     pageLimitationXL: crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL'),
-    customizedLogoSrc: isDefaultLogo != null && !isDefaultLogo
-      ? crowi.configManager.getConfig('crowi', 'customize:customizedLogoSrc')
-      : null,
     auditLogEnabled: crowi.configManager.getConfig('crowi', 'app:auditLogEnabled'),
     activityExpirationSeconds: crowi.configManager.getConfig('crowi', 'app:activityExpirationSeconds'),
     auditLogAvailableActions: crowi.activityService.getAvailableActions(false),

+ 0 - 14
packages/app/src/server/routes/apiv3/customize-setting.js

@@ -660,12 +660,6 @@ module.exports = (crowi) => {
     }
   });
 
-  router.get('/customize-logo', loginRequiredStrictly, adminRequired, async(req, res) => {
-    const isDefaultLogo = await crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo');
-    const customizedLogoSrc = await crowi.configManager.getConfig('crowi', 'customize:customizedLogoSrc');
-    return res.apiv3({ isDefaultLogo, customizedLogoSrc });
-  });
-
   router.put('/customize-logo', loginRequiredStrictly, adminRequired, validator.logo, apiV3FormValidator, async(req, res) => {
 
     const {
@@ -717,11 +711,6 @@ module.exports = (crowi) => {
       let attachment;
       try {
         attachment = await attachmentService.createAttachment(file, req.user, null, AttachmentType.BRAND_LOGO);
-        const attachmentConfigParams = {
-          'customize:customizedLogoSrc': attachment.filePathProxied,
-        };
-
-        await crowi.configManager.updateConfigsInTheSameNamespace('crowi', attachmentConfigParams);
       }
       catch (err) {
         logger.error(err);
@@ -741,9 +730,6 @@ module.exports = (crowi) => {
 
     try {
       await attachmentService.removeAllAttachments(attachments);
-      // update attachmentId immediately
-      const attachmentConfigParams = { 'customize:customizedLogoSrc': null };
-      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', attachmentConfigParams);
     }
     catch (err) {
       logger.error(err);

+ 10 - 0
packages/app/src/server/routes/attachment.js

@@ -296,6 +296,16 @@ module.exports = function(crowi, app) {
     return responseForAttachment(req, res, attachment);
   };
 
+  api.getBrandLogo = async function(req, res) {
+    const brandLogoAttachment = await Attachment.findOne({ attachmentType: AttachmentType.BRAND_LOGO });
+
+    if (brandLogoAttachment == null) {
+      return res.status(404).json(ApiResponse.error('Brand logo does not exist'));
+    }
+
+    return responseForAttachment(req, res, brandLogoAttachment);
+  };
+
   /**
    * @api {get} /attachments.obsoletedGetForMongoDB get attachments from mongoDB
    * @apiName get

+ 4 - 0
packages/app/src/server/routes/index.js

@@ -3,6 +3,7 @@ import express from 'express';
 
 import { generateAddActivityMiddleware } from '../middlewares/add-activity';
 import apiV1FormValidator from '../middlewares/apiv1-form-validator';
+import { generateCertifyBrandLogoMiddleware } from '../middlewares/certify-brand-logo';
 import injectResetOrderByTokenMiddleware from '../middlewares/inject-reset-order-by-token-middleware';
 import injectUserRegistrationOrderByTokenMiddleware from '../middlewares/inject-user-registration-order-by-token-middleware';
 import * as loginFormValidator from '../middlewares/login-form-validator';
@@ -30,6 +31,7 @@ module.exports = function(crowi, app) {
   const loginRequired = require('../middlewares/login-required')(crowi, true);
   const adminRequired = require('../middlewares/admin-required')(crowi);
   const certifySharedFile = require('../middlewares/certify-shared-file')(crowi);
+  const certifyBrandLogo = generateCertifyBrandLogoMiddleware(crowi);
   const rateLimiter = require('../middlewares/rate-limiter')();
   const addActivity = generateAddActivityMiddleware(crowi);
 
@@ -106,6 +108,8 @@ module.exports = function(crowi, app) {
   app.post('/_api/admin/import/qiita'           , loginRequiredStrictly , adminRequired , csrfProtection, addActivity, admin.api.importDataFromQiita);
   app.post('/_api/admin/import/testQiitaAPI'    , loginRequiredStrictly , adminRequired , csrfProtection, addActivity, admin.api.testQiitaAPI);
 
+  app.get('/attachment/brand-logo' , certifyBrandLogo, loginRequired, attachment.api.getBrandLogo);
+
   /*
    * Routes below are unavailable when maintenance mode
    */

+ 11 - 0
packages/app/src/server/service/attachment.js

@@ -1,5 +1,7 @@
 import loggerFactory from '~/utils/logger';
 
+import { AttachmentType } from '../interfaces/attachment';
+
 const fs = require('fs');
 
 const mongoose = require('mongoose');
@@ -77,6 +79,15 @@ class AttachmentService {
     return;
   }
 
+  async isBrandLogoExist() {
+    const Attachment = this.crowi.model('Attachment');
+
+    const query = { attachmentType: AttachmentType.BRAND_LOGO };
+    const count = await Attachment.countDocuments(query);
+
+    return count >= 1;
+  }
+
 }
 
 module.exports = AttachmentService;

+ 1 - 1
packages/app/src/services/renderer/rehype-plugins/add-line-number-attribute.ts

@@ -5,7 +5,7 @@ import { visit } from 'unist-util-visit';
 
 import { addClassToProperties } from './add-class';
 
-const REGEXP_TARGET_TAGNAMES = new RegExp(/^(h1|h2|h3|h4|h5|h6|p|img|pre|blockquote|hr|ol|ul)$/);
+const REGEXP_TARGET_TAGNAMES = new RegExp(/^(h1|h2|h3|h4|h5|h6|p|img|pre|blockquote|hr|ol|ul|table|tr)$/);
 
 export const rehypePlugin: Plugin = () => {
   return (tree) => {

+ 8 - 4
packages/app/src/stores/context.tsx

@@ -76,8 +76,8 @@ export const useIsSharedUser = (initialData?: boolean): SWRResponse<boolean, Err
   return useContextSWR<boolean, Error>('isSharedUser', initialData);
 };
 
-export const useShareLinkId = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
-  return useContextSWR<Nullable<string>, Error>('shareLinkId', initialData);
+export const useShareLinkId = (initialData?: string): SWRResponse<string, Error> => {
+  return useContextSWR('shareLinkId', initialData);
 };
 
 export const useDisableLinkSharing = (initialData?: Nullable<boolean>): SWRResponse<Nullable<boolean>, Error> => {
@@ -201,8 +201,12 @@ export const useCustomizeTitle = (initialData?: string): SWRResponse<string, Err
   return useContextSWR('CustomizeTitle', initialData);
 };
 
-export const useCustomizedLogoSrc = (initialData?: string): SWRResponse<string, Error> => {
-  return useContextSWR('customizedLogoSrc', initialData);
+export const useIsDefaultLogo = (initialData?: boolean): SWRResponse<boolean, Error> => {
+  return useContextSWR('isDefaultLogo', initialData);
+};
+
+export const useIsCustomizedLogoUploaded = (initialData?: boolean): SWRResponse<boolean, Error> => {
+  return useStaticSWR('isCustomizedLogoUploaded', initialData);
 };
 
 export const useGrowiCloudUri = (initialData?: string): SWRResponse<string, Error> => {

+ 1 - 1
packages/app/src/stores/editor.tsx

@@ -89,7 +89,7 @@ export const useCurrentIndentSize = (): SWRResponse<number, Error> => {
 */
 export const useSWRxSlackChannels = (currentPagePath: Nullable<string>): SWRResponse<string[], Error> => {
   const shouldFetch: boolean = currentPagePath != null;
-  return useSWR(
+  return useSWRImmutable(
     shouldFetch ? ['/pages.updatePost', currentPagePath] : null,
     (endpoint, path) => apiGet(endpoint, { path }).then((response: SlackChannels) => response.updatePost),
     { fallbackData: [''] },

+ 3 - 1
packages/app/src/stores/in-app-notification.ts

@@ -1,4 +1,4 @@
-import useSWR, { SWRResponse } from 'swr';
+import useSWR, { SWRConfiguration, SWRResponse } from 'swr';
 
 import type { InAppNotificationStatuses, IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
 import { parseSnapshot } from '~/models/serializers/in-app-notification-snapshot/page';
@@ -15,6 +15,7 @@ export const useSWRxInAppNotifications = <Data, Error>(
   limit: number,
   offset?: number,
   status?: InAppNotificationStatuses,
+  config?: SWRConfiguration,
 ): SWRResponse<PaginateResult<IInAppNotification>, Error> => {
   return useSWR(
     ['/in-app-notification/list', limit, offset, status],
@@ -30,6 +31,7 @@ export const useSWRxInAppNotifications = <Data, Error>(
       });
       return inAppNotificationPaginateResult;
     }),
+    config,
   );
 };
 

+ 18 - 7
packages/app/src/stores/page.tsx

@@ -4,7 +4,7 @@ import type {
   IPageInfoForEntity, IPagePopulatedToShowRevision, Nullable,
 } from '@growi/core';
 import { isClient, pagePathUtils } from '@growi/core';
-import useSWR, { Key, SWRResponse } from 'swr';
+import useSWR, { Key, SWRConfiguration, SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
 import { apiGet } from '~/client/util/apiv1-client';
@@ -17,20 +17,22 @@ import { IRevisionsForPagination } from '~/interfaces/revision';
 
 import { IPageTagsInfo } from '../interfaces/tag';
 
-import { useCurrentPageId, useCurrentPathname } from './context';
+import { useCurrentPageId, useCurrentPathname, useShareLinkId } from './context';
 import { ITermNumberManagerUtil, useTermNumberManager } from './use-static-swr';
 
 const { isPermalink: _isPermalink } = pagePathUtils;
 
-
 export const useSWRxPage = (
     pageId?: string|null,
     shareLinkId?: string,
     revisionId?: string,
     initialData?: IPagePopulatedToShowRevision|null,
+    config?: SWRConfiguration,
 ): SWRResponse<IPagePopulatedToShowRevision|null, Error> => {
   const swrResponse = useSWRImmutable<IPagePopulatedToShowRevision|null, Error>(
     pageId != null ? ['/page', pageId, shareLinkId, revisionId] : null,
+    // TODO: upgrade SWR to v2 and use useSWRMutation
+    //        in order to trigger mutation manually
     (endpoint, pageId, shareLinkId, revisionId) => apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { pageId, shareLinkId, revisionId })
       .then(result => result.data.page)
       .catch((errs) => {
@@ -42,6 +44,7 @@ export const useSWRxPage = (
         }
         throw Error('failed to get page');
       }),
+    config,
   );
 
   useEffect(() => {
@@ -61,10 +64,9 @@ export const useSWRxPageByPath = (path?: string): SWRResponse<IPagePopulatedToSh
   );
 };
 
-export const useSWRxCurrentPage = (
-    shareLinkId?: string, initialData?: IPagePopulatedToShowRevision|null,
-): SWRResponse<IPagePopulatedToShowRevision|null, Error> => {
+export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision|null): SWRResponse<IPagePopulatedToShowRevision|null, Error> => {
   const { data: currentPageId } = useCurrentPageId();
+  const { data: shareLinkId } = useShareLinkId();
 
   // Get URL parameter for specific revisionId
   let revisionId: string|undefined;
@@ -74,7 +76,16 @@ export const useSWRxCurrentPage = (
     revisionId = requestRevisionId != null ? requestRevisionId : undefined;
   }
 
-  const swrResult = useSWRxPage(currentPageId, shareLinkId, revisionId, initialData);
+  const swrResult = useSWRxPage(
+    currentPageId, shareLinkId, revisionId,
+    initialData,
+    // overwrite fetcher if the current page is share link
+    shareLinkId == null
+      ? undefined
+      : {
+        fetcher: () => null,
+      },
+  );
 
   return swrResult;
 };

+ 5 - 4
packages/app/src/stores/personal-settings.tsx

@@ -1,5 +1,5 @@
 import { useTranslation } from 'next-i18next';
-import useSWR, { SWRResponse } from 'swr';
+import useSWR, { SWRConfiguration, SWRResponse } from 'swr';
 
 
 import { IExternalAccount } from '~/interfaces/external-account';
@@ -13,10 +13,11 @@ import { useStaticSWR } from './use-static-swr';
 const logger = loggerFactory('growi:stores:personal-settings');
 
 
-export const useSWRxPersonalSettings = (): SWRResponse<IUser, Error> => {
+export const useSWRxPersonalSettings = (config?: SWRConfiguration): SWRResponse<IUser, Error> => {
   return useSWR(
     '/personal-setting',
     endpoint => apiv3Get(endpoint).then(response => response.data.currentUser),
+    config,
   );
 };
 
@@ -27,9 +28,9 @@ export type IPersonalSettingsInfoOption = {
   disassociateLdapAccount: (account: { providerType: string, accountId: string }) => Promise<void>,
 }
 
-export const usePersonalSettings = (): SWRResponse<IUser, Error> & IPersonalSettingsInfoOption => {
+export const usePersonalSettings = (config?: SWRConfiguration): SWRResponse<IUser, Error> & IPersonalSettingsInfoOption => {
   const { i18n } = useTranslation();
-  const { data: personalSettingsDataFromDB, mutate: revalidate } = useSWRxPersonalSettings();
+  const { data: personalSettingsDataFromDB, mutate: revalidate } = useSWRxPersonalSettings(config);
   const key = personalSettingsDataFromDB != null ? 'personalSettingsInfo' : null;
 
   const swrResult = useStaticSWR<IUser, Error>(key, undefined, { fallbackData: personalSettingsDataFromDB });

+ 5 - 1
packages/app/src/stores/user.tsx

@@ -12,7 +12,11 @@ export const useSWRxUsersList = (userIds: string[]): SWRResponse<IUserHasId[], E
     (endpoint, userIds) => apiv3Get(endpoint, { userIds: userIds.join(',') }).then((response) => {
       return response.data.users;
     }),
-    { use: [checkAndUpdateImageUrlCached] },
+    {
+      use: [checkAndUpdateImageUrlCached],
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
+    },
   );
 };
 

+ 1 - 3
packages/app/test/cypress/integration/10-install/10-install--install.spec.ts

@@ -53,8 +53,6 @@ describe('Install', () => {
     cy.getByTestid('grw-pagetree-item-container', { timeout: 20000 }).should('be.visible');
 
     cy.waitUntilSkeletonDisappear();
-    cy.screenshot(`${ssPrefix}-installed-redirect-to-root-page`, {
-      blackout: ['[data-hide-in-vrt=true]']
-    });
+    cy.screenshot(`${ssPrefix}-installed-redirect-to-root-page`);
   });
 });

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

@@ -1,3 +1,11 @@
+import { BlackoutGroup } from "../../support/blackout";
+
+// Blackout for recalculation of toc content hight
+const blackoutOverride = [
+  ...BlackoutGroup.BASIS,
+  ...BlackoutGroup.SIDE_CONTENTS,
+];
+
 describe('Access to sidebar', () => {
   const ssPrefix = 'access-to-sidebar-';
 
@@ -29,8 +37,7 @@ describe('Access to sidebar', () => {
           cy.waitUntilSkeletonDisappear();
           cy.screenshot(`${ssPrefix}1-sidebar-shown`, {
             capture: 'viewport',
-            // Blackout for recalculation of toc content hight
-            blackout: ['.grw-side-contents-container', '[data-hide-in-vrt=true]'],
+            blackout: blackoutOverride,
           });
         });
 
@@ -42,8 +49,7 @@ describe('Access to sidebar', () => {
           cy.waitUntilSkeletonDisappear();
           cy.screenshot(`${ssPrefix}2-sidebar-collapsed`, {
             capture: 'viewport',
-            // Blackout for recalculation of toc content hight
-            blackout: ['.grw-side-contents-container', '[data-hide-in-vrt=true]'],
+            blackout: blackoutOverride,
           });
         });
       });
@@ -64,7 +70,7 @@ describe('Access to sidebar', () => {
             cy.getByTestid('grw-pagetree-item-container').should('be.visible');
 
             cy.waitUntilSkeletonDisappear();
-            cy.screenshot(`${ssPrefix}page-tree-1-access-to-page-tree`);
+            cy.screenshot(`${ssPrefix}page-tree-1-access-to-page-tree`, { blackout: blackoutOverride });
           });
         });
 
@@ -75,7 +81,7 @@ describe('Access to sidebar', () => {
             // hide page tree tiems
             cy.get('.grw-pagetree-triangle-btn').first().click();
 
-            cy.screenshot(`${ssPrefix}page-tree-2-hide-page-tree-items`);
+            cy.screenshot(`${ssPrefix}page-tree-2-hide-page-tree-items`, { blackout: blackoutOverride });
           });
         });
 
@@ -91,10 +97,7 @@ describe('Access to sidebar', () => {
             return cy.get('.dropdown-menu.show').then($elem => $elem.is(':visible'));
           });
 
-          cy.screenshot(`${ssPrefix}page-tree-3-before-click-button`, {
-            // Blackout for recalculation of toc content hight
-            blackout: ['.grw-side-contents-container', '[data-hide-in-vrt=true]'],
-          });
+          cy.screenshot(`${ssPrefix}page-tree-3-before-click-button`, { blackout: blackoutOverride });
 
           // click add remove bookmark btn
           cy.getByTestid('page-item-control-menu').should('have.class', 'show')
@@ -112,10 +115,7 @@ describe('Access to sidebar', () => {
             return cy.get('.dropdown-menu.show').then($elem => $elem.is(':visible'));
           });
 
-          cy.screenshot(`${ssPrefix}page-tree-4-after-click-button`, {
-            // Blackout for recalculation of toc content hight
-            blackout: ['.grw-side-contents-container', '[data-hide-in-vrt=true]'],
-          });
+          cy.screenshot(`${ssPrefix}page-tree-4-after-click-button`, { blackout: blackoutOverride });
         });
 
         it('Successfully show duplicate page modal', () => {
@@ -137,7 +137,7 @@ describe('Access to sidebar', () => {
           cy.getByTestid('page-duplicate-modal').should('be.visible').within(() => {
             cy.get('.form-control').type('_test');
 
-            cy.screenshot(`${ssPrefix}page-tree-5-duplicate-page-modal`);
+            cy.screenshot(`${ssPrefix}page-tree-5-duplicate-page-modal`, { blackout: blackoutOverride });
 
             cy.get('.modal-header > button').click();
           });
@@ -163,7 +163,7 @@ describe('Access to sidebar', () => {
             cy.getByTestid('closable-text-input').type('_newname');
           })
 
-          cy.screenshot(`${ssPrefix}page-tree-6-rename-page`);
+          cy.screenshot(`${ssPrefix}page-tree-6-rename-page`, { blackout: blackoutOverride });
         });
 
         it('Successfully show delete page modal', () => {
@@ -183,7 +183,7 @@ describe('Access to sidebar', () => {
           });
 
           cy.getByTestid('page-delete-modal').should('be.visible').within(() => {
-            cy.screenshot(`${ssPrefix}page-tree-7-delete-page-modal`);
+            cy.screenshot(`${ssPrefix}page-tree-7-delete-page-modal`, { blackout: blackoutOverride });
             cy.get('.modal-header > button').click();
           });
         });
@@ -205,7 +205,7 @@ describe('Access to sidebar', () => {
             cy.get('.grw-sidebar-content-header > h3').find('a');
 
             cy.waitUntilSkeletonDisappear();
-            cy.screenshot(`${ssPrefix}custom-sidebar-1-access-to-custom-sidebar`);
+            cy.screenshot(`${ssPrefix}custom-sidebar-1-access-to-custom-sidebar`, { blackout: blackoutOverride });
           });
         });
 
@@ -217,7 +217,7 @@ describe('Access to sidebar', () => {
           cy.get('.layout-root').should('have.class', 'editing');
           cy.get('.CodeMirror textarea').type(content, {force: true});
 
-          cy.screenshot(`${ssPrefix}custom-sidebar-2-redirect-to-editor`);
+          cy.screenshot(`${ssPrefix}custom-sidebar-2-redirect-to-editor`, { blackout: blackoutOverride });
 
           cy.getByTestid('save-page-btn').click();
           cy.get('.layout-root').should('not.have.class', 'editing');
@@ -229,7 +229,7 @@ describe('Access to sidebar', () => {
             .should('have.class', 'active');
 
           cy.waitUntilSkeletonDisappear();
-          cy.screenshot(`${ssPrefix}custom-sidebar-3-content-created`);
+          cy.screenshot(`${ssPrefix}custom-sidebar-3-content-created`, { blackout: blackoutOverride });
         });
       });
 
@@ -249,10 +249,7 @@ describe('Access to sidebar', () => {
           cy.get('.list-group-item').should('be.visible');
 
           // The scope of the screenshot is not narrowed because the blackout is shifted
-          cy.screenshot(`${ssPrefix}recent-changes-1-access-to-recent-changes`, {
-            // Blackout for recalculation of toc content hight
-            blackout: ['.grw-side-contents-container', '[data-hide-in-vrt=true]'],
-          });
+          cy.screenshot(`${ssPrefix}recent-changes-1-access-to-recent-changes`, { blackout: blackoutOverride });
         });
 
         it('Successfully switch content size', () => {
@@ -262,10 +259,7 @@ describe('Access to sidebar', () => {
           });
 
           // The scope of the screenshot is not narrowed because the blackout is shifted
-          cy.screenshot(`${ssPrefix}recent-changes-2-switch-content-size`, {
-            // Blackout for recalculation of toc content hight
-            blackout: ['.grw-side-contents-container', '[data-hide-in-vrt=true]'],
-          });
+          cy.screenshot(`${ssPrefix}recent-changes-2-switch-content-size`, { blackout: blackoutOverride });
         });
       });
 
@@ -284,7 +278,7 @@ describe('Access to sidebar', () => {
           cy.getByTestid('grw-contextual-navigation-sub').within(() => {
             cy.getByTestid('grw-tags-list').should('be.visible');
 
-            cy.screenshot(`${ssPrefix}tags-1-access-to-tags`);
+            cy.screenshot(`${ssPrefix}tags-1-access-to-tags`, { blackout: blackoutOverride });
           });
         });
 
@@ -293,7 +287,7 @@ describe('Access to sidebar', () => {
           cy.collapseSidebar(true);
           cy.getByTestid('grw-tags-list').should('be.visible');
 
-          cy.screenshot(`${ssPrefix}tags-2-click-all-tags-button`);
+          cy.screenshot(`${ssPrefix}tags-2-click-all-tags-button`, { blackout: blackoutOverride });
         });
       });
 
@@ -304,7 +298,7 @@ describe('Access to sidebar', () => {
       //   cy.get('.grw-sidebar-nav-secondary-container').within(() => {
       //     cy.get('a[href*="/me/drafts"]').click();
       //   });
-      //   cy.screenshot(`${ssPrefix}access-to-drafts-page`);
+      //   cy.screenshot(`${ssPrefix}access-to-drafts-page`, { blackout: blackoutOverride });
       // });
 
       describe('Test access to GROWI Docs page', () => {
@@ -328,7 +322,7 @@ describe('Access to sidebar', () => {
           cy.get('.grw-page-path-hierarchical-link').should('be.visible');
           cy.get('.grw-custom-nav-tab').should('be.visible');
 
-          cy.screenshot(`${ssPrefix}access-to-trash-page`);
+          cy.screenshot(`${ssPrefix}access-to-trash-page`, { blackout: blackoutOverride });
         });
       });
     });

+ 10 - 4
packages/app/test/cypress/integration/50-sidebar/50-sidebar--switching-sidebar-mode.spec.ts

@@ -1,3 +1,11 @@
+import { BlackoutGroup } from "../../support/blackout";
+
+// Blackout for recalculation of toc content hight
+const blackoutOverride = [
+  ...BlackoutGroup.BASIS,
+  ...BlackoutGroup.SIDE_CONTENTS,
+];
+
 context('Switch sidebar mode', () => {
   const ssPrefix = 'switch-sidebar-mode-';
 
@@ -28,15 +36,13 @@ context('Switch sidebar mode', () => {
     cy.get('[for="swSidebarMode"]').click({force: true});
     cy.get('.grw-sidebar-nav').should('not.be.visible');
     cy.screenshot(`${ssPrefix}-switch-sidebar-mode`, {
-      // Blackout for recalculation of toc content hight
-      blackout: ['.grw-side-contents-container', '[data-hide-in-vrt=true]'],
+      blackout: blackoutOverride,
     });
 
     cy.get('[for="swSidebarMode"]').click({force: true});
     cy.get('.grw-sidebar-nav').should('be.visible');
     cy.screenshot(`${ssPrefix}-switch-sidebar-mode-back`, {
-      // Blackout for recalculation of toc content hight
-      blackout: ['.grw-side-contents-container','[data-hide-in-vrt=true]'],
+      blackout: blackoutOverride,
     });
   });
 

+ 14 - 0
packages/app/test/cypress/support/blackout.ts

@@ -0,0 +1,14 @@
+export const BlackoutGroup = {
+  BASIS: [
+    '[data-vrt-blackout=true]',
+    '[data-vrt-blackout-hash=true]',
+    '[data-vrt-blackout-profile=true]',
+    '[data-vrt-blackout-datetime=true]',
+  ],
+  SIDEBAR_NAV: [
+    '[data-vrt-blackout-sidebar-nav=true]',
+  ],
+  SIDE_CONTENTS: [
+    '[data-vrt-blackout-side-contents=true]',
+  ],
+} as const;

+ 6 - 1
packages/app/test/cypress/support/screenshot.ts

@@ -1,4 +1,9 @@
+import { BlackoutGroup } from "./blackout";
+
 Cypress.Screenshot.defaults({
-  blackout: ['[data-hide-in-vrt=true]'],
+  blackout: [
+    ...BlackoutGroup.BASIS,
+    ...BlackoutGroup.SIDEBAR_NAV,
+  ],
   capture: 'viewport',
 })

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

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

+ 1 - 1
packages/core/package.json

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

+ 1 - 1
packages/hackmd/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/hackmd",
-  "version": "6.0.1-RC.0",
+  "version": "6.0.2-RC.0",
   "description": "GROWI js and css files to use hackmd",
   "license": "MIT",
   "main": "dist/index.js",

+ 1 - 1
packages/preset-themes/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@growi/preset-themes",
   "description": "GROWI preset themes",
-  "version": "6.0.1-RC.0",
+  "version": "6.0.2-RC.0",
   "license": "MIT",
   "main": "dist/libs/index.js",
   "files": [

+ 1 - 1
packages/remark-drawio/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-drawio",
-  "version": "6.0.1-RC.0",
+  "version": "6.0.2-RC.0",
   "description": "remark plugin to draw diagrams with draw.io (diagrams.net)",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/remark-growi-directive/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-growi-directive",
-  "version": "6.0.1-RC.0",
+  "version": "6.0.2-RC.0",
   "description": "remark plugin to support GROWI plugin (forked from remark-directive@2.0.1)",
   "license": "MIT",
   "keywords": [

+ 4 - 4
packages/remark-lsx/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-lsx",
-  "version": "6.0.1-RC.0",
+  "version": "6.0.2-RC.0",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "keywords": ["growi", "growi-plugin"],
@@ -23,9 +23,9 @@
     "test": ""
   },
   "dependencies": {
-    "@growi/core": "^6.0.1-RC.0",
-    "@growi/remark-growi-directive": "^6.0.1-RC.0",
-    "@growi/ui": "^6.0.1-RC.0",
+    "@growi/core": "^6.0.2-RC.0",
+    "@growi/remark-growi-directive": "^6.0.2-RC.0",
+    "@growi/ui": "^6.0.2-RC.0",
     "swr": "^1.3.0"
   },
   "devDependencies": {

+ 1 - 1
packages/slack/package.json

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

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

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "6.0.1-slackbot-proxy.0",
+  "version": "6.0.2-slackbot-proxy.0",
   "license": "MIT",
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
@@ -26,7 +26,7 @@
   },
   "dependencies": {
     "@godaddy/terminus": "^4.9.0",
-    "@growi/slack": "^6.0.1-RC.0",
+    "@growi/slack": "^6.0.2-RC.0",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",

+ 2 - 2
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/ui",
-  "version": "6.0.1-RC.0",
+  "version": "6.0.2-RC.0",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": ["growi"],
@@ -17,7 +17,7 @@
     "test": "jest --verbose"
   },
   "dependencies": {
-    "@growi/core": "^6.0.1-RC.0"
+    "@growi/core": "^6.0.2-RC.0"
   },
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",