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

Merge branch 'master' into feat/ldap-group-sync

Futa Arai 2 лет назад
Родитель
Сommit
08a02960e3
76 измененных файлов с 821 добавлено и 524 удалено
  1. 4 4
      .mergify.yml
  2. 23 1
      CHANGELOG.md
  3. 2 2
      apps/app/package.json
  4. 1 1
      apps/app/public/static/locales/en_US/translation.json
  5. 1 1
      apps/app/public/static/locales/ja_JP/translation.json
  6. 1 1
      apps/app/public/static/locales/zh_CN/translation.json
  7. 0 34
      apps/app/src/client/services/ShowPageAccessoriesModal.tsx
  8. 1 1
      apps/app/src/components/Admin/Security/ShareLinkSetting.tsx
  9. 11 5
      apps/app/src/components/Comments.tsx
  10. 2 2
      apps/app/src/components/Layout/BasicLayout.tsx
  11. 2 3
      apps/app/src/components/Navbar/PageEditorModeManager.jsx
  12. 1 1
      apps/app/src/components/NotCreatablePage.tsx
  13. 14 9
      apps/app/src/components/Page/PageView.tsx
  14. 0 0
      apps/app/src/components/PageAccessoriesModal/PageAccessoriesModal.module.scss
  15. 25 57
      apps/app/src/components/PageAccessoriesModal/PageAccessoriesModal.tsx
  16. 2 2
      apps/app/src/components/PageAccessoriesModal/PageAttachment.tsx
  17. 8 12
      apps/app/src/components/PageAccessoriesModal/PageHistory.tsx
  18. 1 3
      apps/app/src/components/PageAccessoriesModal/ShareLink/ShareLink.tsx
  19. 0 0
      apps/app/src/components/PageAccessoriesModal/ShareLink/ShareLinkForm.tsx
  20. 1 1
      apps/app/src/components/PageAccessoriesModal/ShareLink/ShareLinkList.tsx
  21. 1 0
      apps/app/src/components/PageAccessoriesModal/ShareLink/index.ts
  22. 79 0
      apps/app/src/components/PageAccessoriesModal/hooks.tsx
  23. 1 0
      apps/app/src/components/PageAccessoriesModal/index.ts
  24. 9 6
      apps/app/src/components/PageAlert/TrashPageAlert.tsx
  25. 20 14
      apps/app/src/components/PageComment.tsx
  26. 5 3
      apps/app/src/components/PageComment/CommentEditor.tsx
  27. 10 2
      apps/app/src/components/PageSideContents.tsx
  28. 2 3
      apps/app/src/components/PrivateLegacyPages.tsx
  29. 1 1
      apps/app/src/components/ReactMarkdownComponents/Header.tsx
  30. 1 2
      apps/app/src/components/SearchPage/SearchPageBase.tsx
  31. 7 7
      apps/app/src/components/ShareLinkPageView.tsx
  32. 1 1
      apps/app/src/components/Sidebar.tsx
  33. 3 10
      apps/app/src/components/Sidebar/SidebarNav.tsx
  34. 9 3
      apps/app/src/components/TemplateModal/TemplateModal.tsx
  35. 1 1
      apps/app/src/pages/share/[[...path]].page.tsx
  36. 2 0
      apps/app/src/server/service/page.ts
  37. 15 0
      apps/app/src/stores/context.tsx
  38. 13 4
      apps/app/src/stores/modal.tsx
  39. 35 17
      apps/app/test/cypress/e2e/20-basic-features/20-basic-features--access-to-page.cy.ts
  40. 2 4
      apps/app/test/cypress/e2e/20-basic-features/20-basic-features--use-tools.cy.ts
  41. 10 6
      apps/app/test/cypress/e2e/21-basic-features-for-guest/21-basic-features-for-guest--access-to-page.cy.ts
  42. 3 6
      apps/app/test/cypress/e2e/22-sharelink/22-sharelink--access-to-sharelink.cy.ts
  43. 6 5
      apps/app/test/cypress/e2e/23-editor/23-editor--saving.cy.ts
  44. 1 7
      apps/app/test/cypress/e2e/50-sidebar/50-sidebar--access-to-side-bar.cy.ts
  45. 0 13
      apps/app/test/cypress/e2e/50-sidebar/50-sidebar--switching-sidebar-mode.cy.ts
  46. 21 0
      apps/app/test/cypress/support/assertions.ts
  47. 22 19
      apps/app/test/cypress/support/commands.ts
  48. 1 0
      apps/app/test/cypress/support/index.ts
  49. 4 1
      apps/app/test/cypress/tsconfig.json
  50. 1 1
      apps/slackbot-proxy/package.json
  51. 7 7
      package.json
  52. 1 1
      packages/core/package.json
  53. 2 0
      packages/core/src/interfaces/page.ts
  54. 1 1
      packages/core/vite.config.ts
  55. 1 1
      packages/hackmd/package.json
  56. 1 1
      packages/hackmd/vite.config.js
  57. 1 1
      packages/pluginkit/vite.config.ts
  58. 1 1
      packages/presentation/package.json
  59. 1 1
      packages/presentation/vite.config.ts
  60. 1 1
      packages/preset-templates/package.json
  61. 1 1
      packages/preset-themes/package.json
  62. 1 1
      packages/preset-themes/vite.libs.config.ts
  63. 1 1
      packages/remark-attachment-refs/package.json
  64. 1 1
      packages/remark-attachment-refs/vite.client.config.ts
  65. 1 1
      packages/remark-attachment-refs/vite.server.config.ts
  66. 1 1
      packages/remark-drawio/package.json
  67. 1 1
      packages/remark-drawio/vite.config.ts
  68. 1 1
      packages/remark-growi-directive/package.json
  69. 2 2
      packages/remark-lsx/package.json
  70. 1 1
      packages/remark-lsx/vite.client.config.ts
  71. 1 1
      packages/remark-lsx/vite.server.config.ts
  72. 1 1
      packages/slack/package.json
  73. 1 1
      packages/slack/vite.config.ts
  74. 1 1
      packages/ui/package.json
  75. 1 1
      packages/ui/vite.config.ts
  76. 402 217
      yarn.lock

+ 4 - 4
.mergify.yml

@@ -3,11 +3,11 @@ pull_request_rules:
     conditions:
     conditions:
       - author = dependabot[bot]
       - author = dependabot[bot]
       - '#approved-reviews-by >= 1'
       - '#approved-reviews-by >= 1'
-      - check-success = "lint (16.x)"
-      - check-success = "test (16.x)"
-      - check-success = "launch-dev (16.x)"
-      - check-success = "test-prod-node14 / launch-prod"
+      - check-success = "lint (18.x)"
+      - check-success = "test (18.x)"
+      - check-success = "launch-dev (18.x)"
       - check-success = "test-prod-node16 / launch-prod"
       - check-success = "test-prod-node16 / launch-prod"
+      - check-success = "test-prod-node18 / launch-prod"
     actions:
     actions:
       merge:
       merge:
         method: merge
         method: merge

+ 23 - 1
CHANGELOG.md

@@ -1,9 +1,31 @@
 # Changelog
 # Changelog
 
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.1.6...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.1.7...HEAD)
 
 
 *Please do not manually update this file. We've automated the process.*
 *Please do not manually update this file. We've automated the process.*
 
 
+## [v6.1.7](https://github.com/weseek/growi/compare/v6.1.6...v6.1.7) - 2023-07-19
+
+### 💎 Features
+
+- feat: Authentication settings cannot be disabled if there will be no administrator user available to log in (#7761) @mudana-grune
+
+### 🚀 Improvement
+
+- imprv: Show spinner while installing and logging-in (#7823) @soumaeda
+- imprv: Routing with next link (#7880) @yuki-takei
+
+### 🐛 Bug Fixes
+
+- fix: Auto popup PageAccessoriesModal and show page history (#7888) @yuki-takei
+- fix: Auto-scroll does not work when accessing the page when the header string is CJK (#7882) @yuki-takei
+- fix: Avoid unnecessary next routing (#7863) @miya
+- fix: Work put back page on bookmark sidebar (#7698) @mudana-grune
+
+### 🧰 Maintenance
+
+- support: Render SearchPageBase in CSR (#7889) @yuki-takei
+
 ## [v6.1.6](https://github.com/weseek/growi/compare/v6.1.5...v6.1.6) - 2023-07-12
 ## [v6.1.6](https://github.com/weseek/growi/compare/v6.1.5...v6.1.6) - 2023-07-12
 
 
 ### 🐛 Bug Fixes
 ### 🐛 Bug Fixes

+ 2 - 2
apps/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "6.1.7-RC.0",
+  "version": "6.1.8-RC.0",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "//// for production": "",
     "//// for production": "",
@@ -133,7 +133,7 @@
     "method-override": "^3.0.0",
     "method-override": "^3.0.0",
     "migrate-mongo": "^8.2.3",
     "migrate-mongo": "^8.2.3",
     "mkdirp": "^1.0.3",
     "mkdirp": "^1.0.3",
-    "mongoose": "^6.5.0",
+    "mongoose": "^6.11.3",
     "mongoose-gridfs": "^1.2.42",
     "mongoose-gridfs": "^1.2.42",
     "mongoose-paginate-v2": "^1.3.9",
     "mongoose-paginate-v2": "^1.3.9",
     "mongoose-unique-validator": "^2.0.3",
     "mongoose-unique-validator": "^2.0.3",

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

@@ -165,7 +165,7 @@
   "folder_name": "Folder name",
   "folder_name": "Folder name",
   "field": "field",
   "field": "field",
   "not_creatable_page": {
   "not_creatable_page": {
-    "could_not_creata_path": "Couldn't create path."
+    "message": "Page contents cannot be created in this path."
   },
   },
   "custom_navigation": {
   "custom_navigation": {
     "no_pages_under_this_page": "There are no pages under this page."
     "no_pages_under_this_page": "There are no pages under this page."

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

@@ -166,7 +166,7 @@
   "folder_name": "フォルダ名",
   "folder_name": "フォルダ名",
   "field": "フィールド",
   "field": "フィールド",
   "not_creatable_page": {
   "not_creatable_page": {
-    "could_not_creata_path": "パスを作成できませんでした。"
+    "message": "このパスではページ コンテンツを作成できません。"
   },
   },
   "custom_navigation": {
   "custom_navigation": {
     "no_pages_under_this_page": "このページの配下にはページが存在しません。"
     "no_pages_under_this_page": "このページの配下にはページが存在しません。"

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

@@ -172,7 +172,7 @@
   "folder_name": "文件夹名称",
   "folder_name": "文件夹名称",
   "field": "字段",
   "field": "字段",
   "not_creatable_page": {
   "not_creatable_page": {
-    "could_not_creata_path": "无法创建路径"
+    "message": "无法在此路径中创建页面内容。"
   },
   },
   "custom_navigation": {
   "custom_navigation": {
     "no_pages_under_this_page": "There are no pages under this page."
     "no_pages_under_this_page": "There are no pages under this page."

+ 0 - 34
apps/app/src/client/services/ShowPageAccessoriesModal.tsx

@@ -1,34 +0,0 @@
-import React, { useEffect, useState } from 'react';
-
-import { usePageAccessoriesModal, PageAccessoriesModalContents } from '~/stores/modal';
-
-function getURLQueryParamValue(key: string) {
-// window.location.href is page URL;
-  const queryStr: URLSearchParams = new URL(window.location.href).searchParams;
-  return queryStr.get(key);
-}
-
-const queryCompareFormat = new RegExp(/([a-z0-9]){24}...([a-z0-9]){24}/);
-
-const ShowPageAccessoriesModal = (): JSX.Element => {
-  const { data: status, open: openPageAccessories } = usePageAccessoriesModal();
-  const [isArleadyMounted, setIsArleadyMounted] = useState(false);
-  useEffect(() => {
-    const pageIdParams = getURLQueryParamValue('compare');
-    if (status == null || status.isOpened === true) {
-      return;
-    }
-    if (isArleadyMounted === true) {
-      return;
-    }
-    if (pageIdParams != null) {
-      if (queryCompareFormat.test(pageIdParams)) {
-        openPageAccessories(PageAccessoriesModalContents.PageHistory);
-      }
-    }
-    setIsArleadyMounted(true);
-  }, [openPageAccessories, status, isArleadyMounted]);
-  return <></>;
-};
-
-export default ShowPageAccessoriesModal;

+ 1 - 1
apps/app/src/components/Admin/Security/ShareLinkSetting.tsx

@@ -8,8 +8,8 @@ import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurit
 import { apiv3Delete } from '~/client/util/apiv3-client';
 import { apiv3Delete } from '~/client/util/apiv3-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 
 
+import ShareLinkList from '../../PageAccessoriesModal/ShareLink/ShareLinkList';
 import PaginationWrapper from '../../PaginationWrapper';
 import PaginationWrapper from '../../PaginationWrapper';
-import ShareLinkList from '../../ShareLink/ShareLinkList';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
 import DeleteAllShareLinksModal from './DeleteAllShareLinksModal';
 import DeleteAllShareLinksModal from './DeleteAllShareLinksModal';

+ 11 - 5
apps/app/src/components/Comments.tsx

@@ -5,7 +5,7 @@ import dynamic from 'next/dynamic';
 
 
 import { ROOT_ELEM_ID as PageCommentRootElemId, type PageCommentProps } from '~/components/PageComment';
 import { ROOT_ELEM_ID as PageCommentRootElemId, type PageCommentProps } from '~/components/PageComment';
 import { useSWRxPageComment } from '~/stores/comment';
 import { useSWRxPageComment } from '~/stores/comment';
-import { useIsTrashPage } from '~/stores/page';
+import { useIsTrashPage, useSWRMUTxPageInfo } from '~/stores/page';
 
 
 import { useCurrentUser } from '../stores/context';
 import { useCurrentUser } from '../stores/context';
 
 
@@ -32,6 +32,7 @@ export const Comments = (props: CommentsProps): JSX.Element => {
   } = props;
   } = props;
 
 
   const { mutate } = useSWRxPageComment(pageId);
   const { mutate } = useSWRxPageComment(pageId);
+  const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(pageId);
   const { data: isDeleted } = useIsTrashPage();
   const { data: isDeleted } = useIsTrashPage();
   const { data: currentUser } = useCurrentUser();
   const { data: currentUser } = useCurrentUser();
 
 
@@ -41,8 +42,8 @@ export const Comments = (props: CommentsProps): JSX.Element => {
     const parent = pageCommentParentRef.current;
     const parent = pageCommentParentRef.current;
     if (parent == null) return;
     if (parent == null) return;
 
 
-    const observerCallback = (mutationRecords:MutationRecord[]) => {
-      mutationRecords.forEach((record:MutationRecord) => {
+    const observerCallback = (mutationRecords: MutationRecord[]) => {
+      mutationRecords.forEach((record: MutationRecord) => {
         const target = record.target as HTMLElement;
         const target = record.target as HTMLElement;
 
 
         for (const child of Array.from(target.children)) {
         for (const child of Array.from(target.children)) {
@@ -69,6 +70,11 @@ export const Comments = (props: CommentsProps): JSX.Element => {
     return <></>;
     return <></>;
   }
   }
 
 
+  const onCommentButtonClickHandler = () => {
+    mutate();
+    mutatePageInfo();
+  };
+
   return (
   return (
     <div className="page-comments-row mt-5 py-4 d-edit-none d-print-none">
     <div className="page-comments-row mt-5 py-4 d-edit-none d-print-none">
       <div className="container-lg">
       <div className="container-lg">
@@ -83,12 +89,12 @@ export const Comments = (props: CommentsProps): JSX.Element => {
             hideIfEmpty={false}
             hideIfEmpty={false}
           />
           />
         </div>
         </div>
-        { !isDeleted && (
+        {!isDeleted && (
           <div id="page-comment-write">
           <div id="page-comment-write">
             <CommentEditor
             <CommentEditor
               pageId={pageId}
               pageId={pageId}
               isForNewComment
               isForNewComment
-              onCommentButtonClicked={mutate}
+              onCommentButtonClicked={onCommentButtonClickHandler}
               revisionId={revision._id}
               revisionId={revision._id}
             />
             />
           </div>
           </div>

+ 2 - 2
apps/app/src/components/Layout/BasicLayout.tsx

@@ -23,7 +23,7 @@ const PageDuplicateModal = dynamic(() => import('../PageDuplicateModal'), { ssr:
 const PageDeleteModal = dynamic(() => import('../PageDeleteModal'), { ssr: false });
 const PageDeleteModal = dynamic(() => import('../PageDeleteModal'), { ssr: false });
 const PageRenameModal = dynamic(() => import('../PageRenameModal'), { ssr: false });
 const PageRenameModal = dynamic(() => import('../PageRenameModal'), { ssr: false });
 const PagePresentationModal = dynamic(() => import('../PagePresentationModal'), { ssr: false });
 const PagePresentationModal = dynamic(() => import('../PagePresentationModal'), { ssr: false });
-const PageAccessoriesModal = dynamic(() => import('../PageAccessoriesModal'), { ssr: false });
+const PageAccessoriesModal = dynamic(() => import('../PageAccessoriesModal').then(mod => mod.PageAccessoriesModal), { ssr: false });
 const DeleteBookmarkFolderModal = dynamic(() => import('../DeleteBookmarkFolderModal').then(mod => mod.DeleteBookmarkFolderModal), { ssr: false });
 const DeleteBookmarkFolderModal = dynamic(() => import('../DeleteBookmarkFolderModal').then(mod => mod.DeleteBookmarkFolderModal), { ssr: false });
 // Fab
 // Fab
 const Fab = dynamic(() => import('../Fab').then(mod => mod.Fab), { ssr: false });
 const Fab = dynamic(() => import('../Fab').then(mod => mod.Fab), { ssr: false });
@@ -41,7 +41,7 @@ export const BasicLayout = ({ children, className }: Props): JSX.Element => {
         <GrowiNavbar />
         <GrowiNavbar />
 
 
         <div className="page-wrapper d-flex d-print-block">
         <div className="page-wrapper d-flex d-print-block">
-          <div className="grw-sidebar-wrapper" data-testid="grw-sidebar-wrapper">
+          <div className="grw-sidebar-wrapper">
             <Sidebar />
             <Sidebar />
           </div>
           </div>
 
 

+ 2 - 3
apps/app/src/components/Navbar/PageEditorModeManager.jsx

@@ -4,7 +4,7 @@ import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
 
 
-import { useCurrentUser, useHackmdUri } from '~/stores/context';
+import { useIsAdmin, useHackmdUri } from '~/stores/context';
 import { EditorMode, useIsDeviceSmallerThanMd } from '~/stores/ui';
 import { EditorMode, useIsDeviceSmallerThanMd } from '~/stores/ui';
 
 
 import styles from './PageEditorModeManager.module.scss';
 import styles from './PageEditorModeManager.module.scss';
@@ -47,10 +47,9 @@ function PageEditorModeManager(props) {
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
-  const { data: currentUser } = useCurrentUser();
   const { data: hackmdUri } = useHackmdUri();
   const { data: hackmdUri } = useHackmdUri();
 
 
-  const isAdmin = currentUser?.admin;
+  const { data: isAdmin } = useIsAdmin();
   const isHackmdEnabled = hackmdUri != null;
   const isHackmdEnabled = hackmdUri != null;
   const showHackmdBtn = isHackmdEnabled || isAdmin;
   const showHackmdBtn = isHackmdEnabled || isAdmin;
 
 

+ 1 - 1
apps/app/src/components/NotCreatablePage.tsx

@@ -10,7 +10,7 @@ export const NotCreatablePage: FC = () => {
       <div className="col-md-12">
       <div className="col-md-12">
         <h2 className="text-muted">
         <h2 className="text-muted">
           <i className="icon-ban mr-1" aria-hidden="true"></i>
           <i className="icon-ban mr-1" aria-hidden="true"></i>
-          { t('not_creatable_page.could_not_creata_path') }
+          { t('not_creatable_page.message') }
         </h2>
         </h2>
       </div>
       </div>
     </div>
     </div>

+ 14 - 9
apps/app/src/components/Page/PageView.tsx

@@ -83,7 +83,7 @@ export const PageView = (props: Props): JSX.Element => {
 
 
     const targetId = hash.slice(1);
     const targetId = hash.slice(1);
 
 
-    const target = document.getElementById(targetId);
+    const target = document.getElementById(decodeURIComponent(targetId));
     target?.scrollIntoView();
     target?.scrollIntoView();
 
 
   }, [isCommentsLoaded]);
   }, [isCommentsLoaded]);
@@ -111,11 +111,16 @@ export const PageView = (props: Props): JSX.Element => {
     ? (
     ? (
       <>
       <>
         <div id="comments-container" ref={commentsContainerRef}>
         <div id="comments-container" ref={commentsContainerRef}>
-          <Comments pageId={page._id} pagePath={pagePath} revision={page.revision} onLoaded={() => setCommentsLoaded(true)} />
+          <Comments
+            pageId={page._id}
+            pagePath={pagePath}
+            revision={page.revision}
+            onLoaded={() => setCommentsLoaded(true)}
+          />
         </div>
         </div>
-        { (isUsersHomePagePath && page.creator != null) && (
-          <UsersHomePageFooter creatorId={page.creator._id}/>
-        ) }
+        {(isUsersHomePagePath && page.creator != null) && (
+          <UsersHomePageFooter creatorId={page.creator._id} />
+        )}
         <PageContentFooter page={page} />
         <PageContentFooter page={page} />
       </>
       </>
     )
     )
@@ -144,15 +149,15 @@ export const PageView = (props: Props): JSX.Element => {
     >
     >
       <PageAlerts />
       <PageAlerts />
 
 
-      { specialContents }
-      { specialContents == null && (
+      {specialContents}
+      {specialContents == null && (
         <>
         <>
-          { (isUsersHomePagePath && page?.creator != null) && <UserInfo author={page.creator} /> }
+          {(isUsersHomePagePath && page?.creator != null) && <UserInfo author={page.creator} />}
           <div className={`mb-5 ${isMobile ? `page-mobile ${styles['page-mobile']}` : ''}`}>
           <div className={`mb-5 ${isMobile ? `page-mobile ${styles['page-mobile']}` : ''}`}>
             <Contents />
             <Contents />
           </div>
           </div>
         </>
         </>
-      ) }
+      )}
 
 
     </MainPane>
     </MainPane>
   );
   );

+ 0 - 0
apps/app/src/components/PageAccessoriesModal.module.scss → apps/app/src/components/PageAccessoriesModal/PageAccessoriesModal.module.scss


+ 25 - 57
apps/app/src/components/PageAccessoriesModal.tsx → apps/app/src/components/PageAccessoriesModal/PageAccessoriesModal.tsx

@@ -1,6 +1,7 @@
-import React, { useEffect, useMemo, useState } from 'react';
+import React, { useMemo, useState } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import dynamic from 'next/dynamic';
 import {
 import {
   Modal, ModalBody, ModalHeader,
   Modal, ModalBody, ModalHeader,
 } from 'reactstrap';
 } from 'reactstrap';
@@ -10,25 +11,26 @@ import {
 } from '~/stores/context';
 } from '~/stores/context';
 import { usePageAccessoriesModal, PageAccessoriesModalContents } from '~/stores/modal';
 import { usePageAccessoriesModal, PageAccessoriesModalContents } from '~/stores/modal';
 
 
-import { CustomNavTab } from './CustomNavigation/CustomNav';
-import CustomTabContent from './CustomNavigation/CustomTabContent';
-import ExpandOrContractButton from './ExpandOrContractButton';
-import AttachmentIcon from './Icons/AttachmentIcon';
-import HistoryIcon from './Icons/HistoryIcon';
-import ShareLinkIcon from './Icons/ShareLinkIcon';
-import PageAttachment from './PageAttachment';
-import { PageHistory, getQueryParam } from './PageHistory';
-import ShareLink from './ShareLink/ShareLink';
+import { CustomNavTab } from '../CustomNavigation/CustomNav';
+import CustomTabContent from '../CustomNavigation/CustomTabContent';
+import ExpandOrContractButton from '../ExpandOrContractButton';
+import AttachmentIcon from '../Icons/AttachmentIcon';
+import HistoryIcon from '../Icons/HistoryIcon';
+import ShareLinkIcon from '../Icons/ShareLinkIcon';
+
+import { useAutoOpenModalByQueryParam } from './hooks';
 
 
 import styles from './PageAccessoriesModal.module.scss';
 import styles from './PageAccessoriesModal.module.scss';
 
 
-const PageAccessoriesModal = (): JSX.Element => {
 
 
-  const { t } = useTranslation();
+const PageAttachment = dynamic(() => import('./PageAttachment'), { ssr: false });
+const PageHistory = dynamic(() => import('./PageHistory').then(mod => mod.PageHistory), { ssr: false });
+const ShareLink = dynamic(() => import('./ShareLink').then(mod => mod.ShareLink), { ssr: false });
+
 
 
-  const [activeTab, setActiveTab] = useState<PageAccessoriesModalContents>();
-  const [sourceRevisionId, setSourceRevisionId] = useState<string>();
-  const [targetRevisionId, setTargetRevisionId] = useState<string>();
+export const PageAccessoriesModal = (): JSX.Element => {
+
+  const { t } = useTranslation();
 
 
   const [isWindowExpanded, setIsWindowExpanded] = useState(false);
   const [isWindowExpanded, setIsWindowExpanded] = useState(false);
 
 
@@ -37,46 +39,16 @@ const PageAccessoriesModal = (): JSX.Element => {
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isLinkSharingDisabled } = useDisableLinkSharing();
   const { data: isLinkSharingDisabled } = useDisableLinkSharing();
 
 
-  const { data: status, mutate, close } = usePageAccessoriesModal();
-
-  // activate tab when open
-  useEffect(() => {
-    if (status == null) return;
+  const { data: status, close, selectContents } = usePageAccessoriesModal();
 
 
-    const { isOpened, activatedContents } = status;
-    if (isOpened && activatedContents != null) {
-      setActiveTab(activatedContents);
-    }
-  }, [status]);
-
-  // Set sourceRevisionId and targetRevisionId as state with valid object id string
-  useEffect(() => {
-    const queryParams = getQueryParam();
-    // https://regex101.com/r/YHTDsr/1
-    const regex = /^([0-9a-f]{24})...([0-9a-f]{24})$/i;
-
-    if (queryParams == null || !regex.test(queryParams)) {
-      return;
-    }
-
-    const matches = queryParams.match(regex);
-
-    if (matches == null) {
-      return;
-    }
-
-    const [, sourceRevisionId, targetRevisionId] = matches;
-    setSourceRevisionId(sourceRevisionId);
-    setTargetRevisionId(targetRevisionId);
-    mutate({ isOpened: true });
-  }, [mutate]);
+  useAutoOpenModalByQueryParam();
 
 
   const navTabMapping = useMemo(() => {
   const navTabMapping = useMemo(() => {
     return {
     return {
       [PageAccessoriesModalContents.PageHistory]: {
       [PageAccessoriesModalContents.PageHistory]: {
         Icon: HistoryIcon,
         Icon: HistoryIcon,
         Content: () => {
         Content: () => {
-          return <PageHistory onClose={close} sourceRevisionId={sourceRevisionId} targetRevisionId={targetRevisionId}/>;
+          return <PageHistory onClose={close} />;
         },
         },
         i18n: t('History'),
         i18n: t('History'),
         isLinkEnabled: () => !isGuestUser && !isSharedUser,
         isLinkEnabled: () => !isGuestUser && !isSharedUser,
@@ -97,7 +69,7 @@ const PageAccessoriesModal = (): JSX.Element => {
         isLinkEnabled: () => !isGuestUser && !isReadOnlyUser && !isSharedUser && !isLinkSharingDisabled,
         isLinkEnabled: () => !isGuestUser && !isReadOnlyUser && !isSharedUser && !isLinkSharingDisabled,
       },
       },
     };
     };
-  }, [t, close, sourceRevisionId, targetRevisionId, isGuestUser, isReadOnlyUser, isSharedUser, isLinkSharingDisabled]);
+  }, [t, close, isGuestUser, isReadOnlyUser, isSharedUser, isLinkSharingDisabled]);
 
 
   const buttons = useMemo(() => (
   const buttons = useMemo(() => (
     <div className="d-flex flex-nowrap">
     <div className="d-flex flex-nowrap">
@@ -112,7 +84,7 @@ const PageAccessoriesModal = (): JSX.Element => {
     </div>
     </div>
   ), [close, isWindowExpanded]);
   ), [close, isWindowExpanded]);
 
 
-  if (status == null || activeTab == null) {
+  if (status == null || status.activatedContents == null) {
     return <></>;
     return <></>;
   }
   }
 
 
@@ -128,20 +100,16 @@ const PageAccessoriesModal = (): JSX.Element => {
     >
     >
       <ModalHeader className="p-0" toggle={close} close={buttons}>
       <ModalHeader className="p-0" toggle={close} close={buttons}>
         <CustomNavTab
         <CustomNavTab
-          activeTab={activeTab}
+          activeTab={status.activatedContents}
           navTabMapping={navTabMapping}
           navTabMapping={navTabMapping}
           breakpointToHideInactiveTabsDown="md"
           breakpointToHideInactiveTabsDown="md"
-          onNavSelected={(v: PageAccessoriesModalContents) => {
-            setActiveTab(v);
-          }}
+          onNavSelected={selectContents}
           hideBorderBottom
           hideBorderBottom
         />
         />
       </ModalHeader>
       </ModalHeader>
       <ModalBody className="overflow-auto grw-modal-body-style">
       <ModalBody className="overflow-auto grw-modal-body-style">
-        <CustomTabContent activeTab={activeTab} navTabMapping={navTabMapping} />
+        <CustomTabContent activeTab={status.activatedContents} navTabMapping={navTabMapping} />
       </ModalBody>
       </ModalBody>
     </Modal>
     </Modal>
   );
   );
 };
 };
-
-export default PageAccessoriesModal;

+ 2 - 2
apps/app/src/components/PageAttachment.tsx → apps/app/src/components/PageAccessoriesModal/PageAttachment.tsx

@@ -9,8 +9,8 @@ import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useDeleteAttachmentModal } from '~/stores/modal';
 import { useDeleteAttachmentModal } from '~/stores/modal';
 import { useSWRxCurrentPage, useCurrentPageId } from '~/stores/page';
 import { useSWRxCurrentPage, useCurrentPageId } from '~/stores/page';
 
 
-import { PageAttachmentList } from './PageAttachment/PageAttachmentList';
-import PaginationWrapper from './PaginationWrapper';
+import { PageAttachmentList } from '../PageAttachment/PageAttachmentList';
+import PaginationWrapper from '../PaginationWrapper';
 
 
 // Utility
 // Utility
 const checkIfFileInUse = (markdown: string, attachment): boolean => {
 const checkIfFileInUse = (markdown: string, attachment): boolean => {

+ 8 - 12
apps/app/src/components/PageHistory.tsx → apps/app/src/components/PageAccessoriesModal/PageHistory.tsx

@@ -3,34 +3,30 @@ import React from 'react';
 import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
 import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { PageRevisionTable } from './PageHistory/PageRevisionTable';
+import { PageRevisionTable } from '../PageHistory/PageRevisionTable';
+
+import { useAutoComparingRevisionsByQueryParam } from './hooks';
 
 
 const logger = loggerFactory('growi:PageHistory');
 const logger = loggerFactory('growi:PageHistory');
 
 
 type PageHistoryProps = {
 type PageHistoryProps = {
-  sourceRevisionId?: string,
-  targetRevisionId?: string
   onClose: () => void
   onClose: () => void
 }
 }
 
 
-// Get string from 'compare' query params
-export const getQueryParam = (): string | null => {
-  const query: URLSearchParams = new URL(window.location.href).searchParams;
-  return query.get('compare');
-};
-
 export const PageHistory: React.FC<PageHistoryProps> = (props: PageHistoryProps) => {
 export const PageHistory: React.FC<PageHistoryProps> = (props: PageHistoryProps) => {
-  const { sourceRevisionId, targetRevisionId, onClose } = props;
+  const { onClose } = props;
 
 
   const { data: currentPageId } = useCurrentPageId();
   const { data: currentPageId } = useCurrentPageId();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
 
 
+  const comparingRevisions = useAutoComparingRevisionsByQueryParam();
+
   return (
   return (
     <div className="revision-history" data-testid="page-history">
     <div className="revision-history" data-testid="page-history">
       {currentPageId != null && currentPagePath != null && (
       {currentPageId != null && currentPagePath != null && (
         <PageRevisionTable
         <PageRevisionTable
-          sourceRevisionId={sourceRevisionId}
-          targetRevisionId={targetRevisionId}
+          sourceRevisionId={comparingRevisions?.sourceRevisionId}
+          targetRevisionId={comparingRevisions?.targetRevisionId}
           currentPageId={currentPageId}
           currentPageId={currentPageId}
           currentPagePath={currentPagePath}
           currentPagePath={currentPagePath}
           onClose={onClose}
           onClose={onClose}

+ 1 - 3
apps/app/src/components/ShareLink/ShareLink.tsx → apps/app/src/components/PageAccessoriesModal/ShareLink/ShareLink.tsx

@@ -12,7 +12,7 @@ import { useSWRxSharelink } from '~/stores/share-link';
 import { ShareLinkForm } from './ShareLinkForm';
 import { ShareLinkForm } from './ShareLinkForm';
 import ShareLinkList from './ShareLinkList';
 import ShareLinkList from './ShareLinkList';
 
 
-const ShareLink = (): JSX.Element => {
+export const ShareLink = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const [isOpenShareLinkForm, setIsOpenShareLinkForm] = useState<boolean>(false);
   const [isOpenShareLinkForm, setIsOpenShareLinkForm] = useState<boolean>(false);
 
 
@@ -73,5 +73,3 @@ const ShareLink = (): JSX.Element => {
     </div>
     </div>
   );
   );
 };
 };
-
-export default ShareLink;

+ 0 - 0
apps/app/src/components/ShareLink/ShareLinkForm.tsx → apps/app/src/components/PageAccessoriesModal/ShareLink/ShareLinkForm.tsx


+ 1 - 1
apps/app/src/components/ShareLink/ShareLinkList.tsx → apps/app/src/components/PageAccessoriesModal/ShareLink/ShareLinkList.tsx

@@ -3,7 +3,7 @@ import React from 'react';
 import dateFnsFormat from 'date-fns/format';
 import dateFnsFormat from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
-import CopyDropdown from '../Page/CopyDropdown';
+import CopyDropdown from '../../Page/CopyDropdown';
 
 
 
 
 type ShareLinkTrProps = {
 type ShareLinkTrProps = {

+ 1 - 0
apps/app/src/components/PageAccessoriesModal/ShareLink/index.ts

@@ -0,0 +1 @@
+export * from './ShareLink';

+ 79 - 0
apps/app/src/components/PageAccessoriesModal/hooks.tsx

@@ -0,0 +1,79 @@
+import { useEffect, useState } from 'react';
+
+import { usePageAccessoriesModal, PageAccessoriesModalContents } from '~/stores/modal';
+
+function getURLQueryParamValue(key: string) {
+// window.location.href is page URL;
+  const queryStr: URLSearchParams = new URL(window.location.href).searchParams;
+  return queryStr.get(key);
+}
+
+// https://regex101.com/r/YHTDsr/1
+const queryCompareFormat = /^([0-9a-f]{24})...([0-9a-f]{24})$/i;
+
+
+export const useAutoOpenModalByQueryParam = (): void => {
+  const [isArleadyMounted, setIsArleadyMounted] = useState(false);
+
+  const { data: status, open: openPageAccessories } = usePageAccessoriesModal();
+
+  useEffect(() => {
+    if (isArleadyMounted) {
+      return;
+    }
+
+    if (status == null || status.isOpened === true) {
+      return;
+    }
+
+    const pageIdParams = getURLQueryParamValue('compare');
+    if (pageIdParams != null) {
+      const matches = pageIdParams.match(queryCompareFormat);
+
+      if (matches == null) {
+        return;
+      }
+
+      // open History
+      openPageAccessories(PageAccessoriesModalContents.PageHistory);
+    }
+
+    setIsArleadyMounted(true);
+  }, [openPageAccessories, status, isArleadyMounted]);
+
+};
+
+type ComparingRevisionIds = {
+  sourceRevisionId: string,
+  targetRevisionId: string,
+}
+
+export const useAutoComparingRevisionsByQueryParam = (): ComparingRevisionIds | null => {
+  const [isArleadyMounted, setIsArleadyMounted] = useState(false);
+
+  const [sourceRevisionId, setSourceRevisionId] = useState<string>();
+  const [targetRevisionId, setTargetRevisionId] = useState<string>();
+
+  useEffect(() => {
+    if (isArleadyMounted) {
+      return;
+    }
+
+    const pageIdParams = getURLQueryParamValue('compare');
+    if (pageIdParams != null) {
+      const matches = pageIdParams.match(queryCompareFormat);
+
+      if (matches != null) {
+        const [, source, target] = matches;
+        setSourceRevisionId(source);
+        setTargetRevisionId(target);
+      }
+    }
+
+    setIsArleadyMounted(true);
+  }, [isArleadyMounted]);
+
+  return sourceRevisionId != null && targetRevisionId != null
+    ? { sourceRevisionId, targetRevisionId }
+    : null;
+};

+ 1 - 0
apps/app/src/components/PageAccessoriesModal/index.ts

@@ -0,0 +1 @@
+export * from './PageAccessoriesModal';

+ 9 - 6
apps/app/src/components/PageAlert/TrashPageAlert.tsx

@@ -42,10 +42,11 @@ export const TrashPageAlert = (): JSX.Element => {
   const deleteUser = pageData?.deleteUser;
   const deleteUser = pageData?.deleteUser;
   const deletedAt = pageData?.deletedAt ? format(new Date(pageData?.deletedAt), 'yyyy/MM/dd HH:mm') : '';
   const deletedAt = pageData?.deletedAt ? format(new Date(pageData?.deletedAt), 'yyyy/MM/dd HH:mm') : '';
   const revisionId = pageData?.revision?._id;
   const revisionId = pageData?.revision?._id;
-
+  const isEmptyPage = pageId == null || revisionId == null || pagePath == null;
 
 
   const openPutbackPageModalHandler = useCallback(() => {
   const openPutbackPageModalHandler = useCallback(() => {
-    if (pageId == null || pagePath == null) {
+    // User cannot operate empty page.
+    if (isEmptyPage) {
       return;
       return;
     }
     }
     const putBackedHandler = () => {
     const putBackedHandler = () => {
@@ -62,10 +63,11 @@ export const TrashPageAlert = (): JSX.Element => {
       }
       }
     };
     };
     openPutBackPageModal({ pageId, path: pagePath }, { onPutBacked: putBackedHandler });
     openPutBackPageModal({ pageId, path: pagePath }, { onPutBacked: putBackedHandler });
-  }, [currentPagePath, mutateCurrentPage, openPutBackPageModal, pageId, pagePath, router]);
+  }, [currentPagePath, mutateCurrentPage, openPutBackPageModal, pageId, pagePath, router, isEmptyPage]);
 
 
   const openPageDeleteModalHandler = useCallback(() => {
   const openPageDeleteModalHandler = useCallback(() => {
-    if (pageId === undefined || revisionId === undefined || pagePath === undefined) {
+    // User cannot operate empty page.
+    if (isEmptyPage) {
       return;
       return;
     }
     }
     const pageToDelete = {
     const pageToDelete = {
@@ -77,7 +79,7 @@ export const TrashPageAlert = (): JSX.Element => {
       meta: pageInfo,
       meta: pageInfo,
     };
     };
     openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });
     openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });
-  }, [openDeleteModal, pageId, pageInfo, pagePath, revisionId]);
+  }, [openDeleteModal, pageId, pageInfo, pagePath, revisionId, isEmptyPage]);
 
 
   const renderTrashPageManagementButtons = useCallback(() => {
   const renderTrashPageManagementButtons = useCallback(() => {
     return (
     return (
@@ -103,7 +105,8 @@ export const TrashPageAlert = (): JSX.Element => {
     );
     );
   }, [openPageDeleteModalHandler, openPutbackPageModalHandler, pageInfo?.isAbleToDeleteCompletely, t]);
   }, [openPageDeleteModalHandler, openPutbackPageModalHandler, pageInfo?.isAbleToDeleteCompletely, t]);
 
 
-  if (!isTrashPage) {
+  // Show this alert only for non-empty pages in trash.
+  if (!isTrashPage || isEmptyPage) {
     return <></>;
     return <></>;
   }
   }
 
 

+ 20 - 14
apps/app/src/components/PageComment.tsx

@@ -9,6 +9,7 @@ import { Button } from 'reactstrap';
 import { apiPost } from '~/client/util/apiv1-client';
 import { apiPost } from '~/client/util/apiv1-client';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
 import { RendererOptions } from '~/interfaces/renderer-options';
 import { RendererOptions } from '~/interfaces/renderer-options';
+import { useSWRMUTxPageInfo } from '~/stores/page';
 import { useCommentForCurrentPageOptions } from '~/stores/renderer';
 import { useCommentForCurrentPageOptions } from '~/stores/renderer';
 
 
 import { ICommentHasId, ICommentHasIdList } from '../interfaces/comment';
 import { ICommentHasId, ICommentHasIdList } from '../interfaces/comment';
@@ -42,7 +43,7 @@ export type PageCommentProps = {
   hideIfEmpty?: boolean,
   hideIfEmpty?: boolean,
 }
 }
 
 
-export const PageComment: FC<PageCommentProps> = memo((props:PageCommentProps): JSX.Element => {
+export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps): JSX.Element => {
 
 
   const {
   const {
     rendererOptions: rendererOptionsByProps,
     rendererOptions: rendererOptionsByProps,
@@ -56,6 +57,7 @@ export const PageComment: FC<PageCommentProps> = memo((props:PageCommentProps):
   const [isDeleteConfirmModalShown, setIsDeleteConfirmModalShown] = useState<boolean>(false);
   const [isDeleteConfirmModalShown, setIsDeleteConfirmModalShown] = useState<boolean>(false);
   const [showEditorIds, setShowEditorIds] = useState<Set<string>>(new Set());
   const [showEditorIds, setShowEditorIds] = useState<Set<string>>(new Set());
   const [errorMessageOnDelete, setErrorMessageOnDelete] = useState<string>('');
   const [errorMessageOnDelete, setErrorMessageOnDelete] = useState<string>('');
+  const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(pageId);
 
 
   const commentsFromOldest = useMemo(() => (comments != null ? [...comments].reverse() : null), [comments]);
   const commentsFromOldest = useMemo(() => (comments != null ? [...comments].reverse() : null), [comments]);
   const commentsExceptReply: ICommentHasIdList | undefined = useMemo(
   const commentsExceptReply: ICommentHasIdList | undefined = useMemo(
@@ -84,7 +86,8 @@ export const PageComment: FC<PageCommentProps> = memo((props:PageCommentProps):
   const onDeleteCommentAfterOperation = useCallback(() => {
   const onDeleteCommentAfterOperation = useCallback(() => {
     onCancelDeleteComment();
     onCancelDeleteComment();
     mutate();
     mutate();
-  }, [mutate, onCancelDeleteComment]);
+    mutatePageInfo();
+  }, [mutate, onCancelDeleteComment, mutatePageInfo]);
 
 
   const onDeleteComment = useCallback(async() => {
   const onDeleteComment = useCallback(async() => {
     if (commentToBeDeleted == null) return;
     if (commentToBeDeleted == null) return;
@@ -92,7 +95,7 @@ export const PageComment: FC<PageCommentProps> = memo((props:PageCommentProps):
       await apiPost('/comments.remove', { comment_id: commentToBeDeleted._id });
       await apiPost('/comments.remove', { comment_id: commentToBeDeleted._id });
       onDeleteCommentAfterOperation();
       onDeleteCommentAfterOperation();
     }
     }
-    catch (error:unknown) {
+    catch (error: unknown) {
       setErrorMessageOnDelete(error as string);
       setErrorMessageOnDelete(error as string);
       toastError(`error: ${error}`);
       toastError(`error: ${error}`);
     }
     }
@@ -100,12 +103,20 @@ export const PageComment: FC<PageCommentProps> = memo((props:PageCommentProps):
 
 
   const removeShowEditorId = useCallback((commentId: string) => {
   const removeShowEditorId = useCallback((commentId: string) => {
     setShowEditorIds((previousState) => {
     setShowEditorIds((previousState) => {
-      const previousShowEditorIds = new Set(...previousState);
-      previousShowEditorIds.delete(commentId);
-      return previousShowEditorIds;
+      return new Set([...previousState].filter(id => id !== commentId));
     });
     });
   }, []);
   }, []);
 
 
+  const onReplyButtonClickHandler = useCallback((commentId: string) => {
+    setShowEditorIds(previousState => new Set([...previousState, commentId]));
+  }, []);
+
+  const onCommentButtonClickHandler = useCallback((commentId: string) => {
+    removeShowEditorId(commentId);
+    mutate();
+    mutatePageInfo();
+  }, [removeShowEditorId, mutate, mutatePageInfo]);
+
   if (hideIfEmpty && comments?.length === 0) {
   if (hideIfEmpty && comments?.length === 0) {
     return <PageCommentRoot />;
     return <PageCommentRoot />;
   }
   }
@@ -163,7 +174,7 @@ export const PageComment: FC<PageCommentProps> = memo((props:PageCommentProps):
         <div className="page-comments">
         <div className="page-comments">
           <h2 className={commentTitleClasses}><i className="icon-fw icon-bubbles"></i>Comments</h2>
           <h2 className={commentTitleClasses}><i className="icon-fw icon-bubbles"></i>Comments</h2>
           <div className="page-comments-list" id="page-comments-list">
           <div className="page-comments-list" id="page-comments-list">
-            { commentsExceptReply.map((comment) => {
+            {commentsExceptReply.map((comment) => {
 
 
               const defaultCommentThreadClasses = 'page-comment-thread pb-5';
               const defaultCommentThreadClasses = 'page-comment-thread pb-5';
               const hasReply: boolean = Object.keys(allReplies).includes(comment._id);
               const hasReply: boolean = Object.keys(allReplies).includes(comment._id);
@@ -184,9 +195,7 @@ export const PageComment: FC<PageCommentProps> = memo((props:PageCommentProps):
                             color="secondary"
                             color="secondary"
                             size="sm"
                             size="sm"
                             className="btn-comment-reply"
                             className="btn-comment-reply"
-                            onClick={() => {
-                              setShowEditorIds(previousState => new Set(previousState.add(comment._id)));
-                            }}
+                            onClick={() => onReplyButtonClickHandler(comment._id)}
                           >
                           >
                             <i className="icon-fw icon-action-undo"></i> Reply
                             <i className="icon-fw icon-action-undo"></i> Reply
                           </Button>
                           </Button>
@@ -201,10 +210,7 @@ export const PageComment: FC<PageCommentProps> = memo((props:PageCommentProps):
                       onCancelButtonClicked={() => {
                       onCancelButtonClicked={() => {
                         removeShowEditorId(comment._id);
                         removeShowEditorId(comment._id);
                       }}
                       }}
-                      onCommentButtonClicked={() => {
-                        removeShowEditorId(comment._id);
-                        mutate();
-                      }}
+                      onCommentButtonClicked={() => onCommentButtonClickHandler(comment._id)}
                       revisionId={revisionId}
                       revisionId={revisionId}
                     />
                     />
                   )}
                   )}

+ 5 - 3
apps/app/src/components/PageComment/CommentEditor.tsx

@@ -109,6 +109,8 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
   }, []);
   }, []);
 
 
   const initializeEditor = useCallback(async() => {
   const initializeEditor = useCallback(async() => {
+    const editingCommentsNum = comment !== '' ? await decrementEditingCommentsNum() : undefined;
+
     setComment('');
     setComment('');
     setActiveTab('comment_editor');
     setActiveTab('comment_editor');
     setError(undefined);
     setError(undefined);
@@ -116,11 +118,11 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     // reset value
     // reset value
     if (editorRef.current == null) { return }
     if (editorRef.current == null) { return }
     editorRef.current.setValue('');
     editorRef.current.setValue('');
-    const editingCommentsNum = await decrementEditingCommentsNum();
-    if (editingCommentsNum === 0) {
+
+    if (editingCommentsNum != null && editingCommentsNum === 0) {
       mutateIsEnabledUnsavedWarning(false); // must be after clearing comment or else onChange will override bool
       mutateIsEnabledUnsavedWarning(false); // must be after clearing comment or else onChange will override bool
     }
     }
-  }, [initializeSlackEnabled, mutateIsEnabledUnsavedWarning, decrementEditingCommentsNum]);
+  }, [initializeSlackEnabled, comment, decrementEditingCommentsNum, mutateIsEnabledUnsavedWarning]);
 
 
   const cancelButtonClickedHandler = useCallback(() => {
   const cancelButtonClickedHandler = useCallback(() => {
     // change state to not ready
     // change state to not ready

+ 10 - 2
apps/app/src/components/PageSideContents.tsx

@@ -4,7 +4,9 @@ import { IPageHasId, pagePathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { Link } from 'react-scroll';
 import { Link } from 'react-scroll';
 
 
+import { IPageInfoForOperation } from '~/interfaces/page';
 import { useDescendantsPageListModal } from '~/stores/modal';
 import { useDescendantsPageListModal } from '~/stores/modal';
+import { useSWRxPageInfo } from '~/stores/page';
 
 
 import CountBadge from './Common/CountBadge';
 import CountBadge from './Common/CountBadge';
 import { ContentLinkButtons } from './ContentLinkButtons';
 import { ContentLinkButtons } from './ContentLinkButtons';
@@ -29,6 +31,8 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
 
 
   const { page, isSharedUser } = props;
   const { page, isSharedUser } = props;
 
 
+  const { data: pageInfo } = useSWRxPageInfo(page._id);
+
   const pagePath = page.path;
   const pagePath = page.path;
   const isTopPagePath = isTopPage(pagePath);
   const isTopPagePath = isTopPage(pagePath);
   const isUsersHomePagePath = isUsersHomePage(pagePath);
   const isUsersHomePagePath = isUsersHomePage(pagePath);
@@ -51,7 +55,9 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
             {t('page_list')}
             {t('page_list')}
 
 
             {/* Do not display CountBadge if '/trash/*': https://github.com/weseek/growi/pull/7600 */}
             {/* Do not display CountBadge if '/trash/*': https://github.com/weseek/growi/pull/7600 */}
-            { !isTrash ? <CountBadge count={page?.descendantCount} offset={1} /> : <div className='px-2'></div>}
+            { !isTrash && pageInfo != null
+              ? <CountBadge count={(pageInfo as IPageInfoForOperation).descendantCount} offset={1} />
+              : <div className='px-2'></div>}
           </button>
           </button>
         )}
         )}
       </div>
       </div>
@@ -67,7 +73,9 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
             >
             >
               <i className="icon-fw icon-bubbles grw-page-accessories-control-icon"></i>
               <i className="icon-fw icon-bubbles grw-page-accessories-control-icon"></i>
               <span>Comments</span>
               <span>Comments</span>
-              <CountBadge count={page.commentCount} />
+              { pageInfo != null
+                ? <CountBadge count={(pageInfo as IPageInfoForOperation).commentCount} />
+                : <div className='px-2'></div>}
             </button>
             </button>
           </Link>
           </Link>
         </div>
         </div>

+ 2 - 3
apps/app/src/components/PrivateLegacyPages.tsx

@@ -14,7 +14,7 @@ import { V5ConversionErrCode } from '~/interfaces/errors/v5-conversion-error';
 import { V5MigrationStatus } from '~/interfaces/page-listing-results';
 import { V5MigrationStatus } from '~/interfaces/page-listing-results';
 import { IFormattedSearchResult } from '~/interfaces/search';
 import { IFormattedSearchResult } from '~/interfaces/search';
 import { PageMigrationErrorData, SocketEventName } from '~/interfaces/websocket';
 import { PageMigrationErrorData, SocketEventName } from '~/interfaces/websocket';
-import { useCurrentUser } from '~/stores/context';
+import { useIsAdmin } from '~/stores/context';
 import {
 import {
   ILegacyPrivatePage, usePrivateLegacyPagesMigrationModal,
   ILegacyPrivatePage, usePrivateLegacyPagesMigrationModal,
 } from '~/stores/modal';
 } from '~/stores/modal';
@@ -191,9 +191,8 @@ ConvertByPathModal.displayName = 'ConvertByPathModal';
 
 
 const PrivateLegacyPages = (): JSX.Element => {
 const PrivateLegacyPages = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { data: currentUser } = useCurrentUser();
 
 
-  const isAdmin = currentUser?.admin;
+  const { data: isAdmin } = useIsAdmin();
 
 
   const [keyword, setKeyword] = useState<string>(initQ);
   const [keyword, setKeyword] = useState<string>(initQ);
   const [offset, setOffset] = useState<number>(0);
   const [offset, setOffset] = useState<number>(0);

+ 1 - 1
apps/app/src/components/ReactMarkdownComponents/Header.tsx

@@ -75,7 +75,7 @@ export const Header = (props: HeaderProps): JSX.Element => {
   const activateByHash = useCallback((url: string) => {
   const activateByHash = useCallback((url: string) => {
     try {
     try {
       const hash = (new URL(url, 'https://example.com')).hash.slice(1);
       const hash = (new URL(url, 'https://example.com')).hash.slice(1);
-      setActive(hash === id);
+      setActive(decodeURIComponent(hash) === id);
     }
     }
     catch (err) {
     catch (err) {
       logger.debug(err);
       logger.debug(err);

+ 1 - 2
apps/app/src/components/SearchPage/SearchPageBase.tsx

@@ -17,8 +17,6 @@ import { mutatePageTree } from '~/stores/page-listing';
 
 
 import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 
 
-import { SearchResultList } from './SearchResultList';
-
 import styles from './SearchPageBase.module.scss';
 import styles from './SearchPageBase.module.scss';
 
 
 // https://regex101.com/r/brrkBu/1
 // https://regex101.com/r/brrkBu/1
@@ -44,6 +42,7 @@ type Props = {
 }
 }
 
 
 
 
+const SearchResultList = dynamic(() => import('./SearchResultList').then(mod => mod.SearchResultList), { ssr: false });
 const SearchResultContent = dynamic(() => import('./SearchResultContent').then(mod => mod.SearchResultContent), {
 const SearchResultContent = dynamic(() => import('./SearchResultContent').then(mod => mod.SearchResultContent), {
   ssr: false,
   ssr: false,
   loading: () => <></>,
   loading: () => <></>,

+ 7 - 7
apps/app/src/components/ShareLink/ShareLinkPageView.tsx → apps/app/src/components/ShareLinkPageView.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useMemo } from 'react';
+import React, { useMemo } from 'react';
 
 
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
@@ -10,17 +10,17 @@ import { useIsNotFound } from '~/stores/page';
 import { useViewOptions } from '~/stores/renderer';
 import { useViewOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { MainPane } from '../Layout/MainPane';
-import RevisionRenderer from '../Page/RevisionRenderer';
-import ShareLinkAlert from '../Page/ShareLinkAlert';
-import type { PageSideContentsProps } from '../PageSideContents';
+import { MainPane } from './Layout/MainPane';
+import RevisionRenderer from './Page/RevisionRenderer';
+import ShareLinkAlert from './Page/ShareLinkAlert';
+import type { PageSideContentsProps } from './PageSideContents';
 
 
 
 
 const logger = loggerFactory('growi:Page');
 const logger = loggerFactory('growi:Page');
 
 
 
 
-const PageSideContents = dynamic<PageSideContentsProps>(() => import('../PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
-const ForbiddenPage = dynamic(() => import('../ForbiddenPage'), { ssr: false });
+const PageSideContents = dynamic<PageSideContentsProps>(() => import('./PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
+const ForbiddenPage = dynamic(() => import('./ForbiddenPage'), { ssr: false });
 
 
 
 
 type Props = {
 type Props = {

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

@@ -297,7 +297,7 @@ const Sidebar = memo((): JSX.Element => {
   const isOpenClass = `${isDrawerOpened ? 'open' : ''}`;
   const isOpenClass = `${isDrawerOpened ? 'open' : ''}`;
   return (
   return (
     <>
     <>
-      <div className={`${grwSidebarClass} ${sidebarModeClass} ${isOpenClass} d-print-none`}>
+      <div className={`${grwSidebarClass} ${sidebarModeClass} ${isOpenClass} d-print-none`} data-testid="grw-sidebar">
         <div className="data-layout-container">
         <div className="data-layout-container">
           <div
           <div
             className='navigation transition-enabled'
             className='navigation transition-enabled'

+ 3 - 10
apps/app/src/components/Sidebar/SidebarNav.tsx

@@ -1,12 +1,12 @@
 import React, {
 import React, {
-  FC, memo, useCallback, useEffect, useState,
+  FC, memo, useCallback,
 } from 'react';
 } from 'react';
 
 
 import Link from 'next/link';
 import Link from 'next/link';
 
 
 import { useUserUISettings } from '~/client/services/user-ui-settings';
 import { useUserUISettings } from '~/client/services/user-ui-settings';
 import { SidebarContentsType } from '~/interfaces/ui';
 import { SidebarContentsType } from '~/interfaces/ui';
-import { useCurrentUser, useGrowiCloudUri } from '~/stores/context';
+import { useIsAdmin, useGrowiCloudUri } from '~/stores/context';
 import { useCurrentSidebarContents } from '~/stores/ui';
 import { useCurrentSidebarContents } from '~/stores/ui';
 
 
 import styles from './SidebarNav.module.scss';
 import styles from './SidebarNav.module.scss';
@@ -82,18 +82,11 @@ type Props = {
 }
 }
 
 
 export const SidebarNav: FC<Props> = (props: Props) => {
 export const SidebarNav: FC<Props> = (props: Props) => {
-
-  const { data: currentUser } = useCurrentUser();
+  const { data: isAdmin } = useIsAdmin();
   const { data: growiCloudUri } = useGrowiCloudUri();
   const { data: growiCloudUri } = useGrowiCloudUri();
 
 
-  const [isAdmin, setAdmin] = useState(false);
-
   const { onItemSelected } = props;
   const { onItemSelected } = props;
 
 
-  useEffect(() => {
-    setAdmin(currentUser?.admin === true);
-  }, [currentUser?.admin]);
-
   return (
   return (
     <div className={`grw-sidebar-nav ${styles['grw-sidebar-nav']}`} data-vrt-blackout-sidebar-nav>
     <div className={`grw-sidebar-nav ${styles['grw-sidebar-nav']}`} data-vrt-blackout-sidebar-nav>
       <div className="grw-sidebar-nav-primary-container">
       <div className="grw-sidebar-nav-primary-container">

+ 9 - 3
apps/app/src/components/TemplateModal/TemplateModal.tsx

@@ -68,7 +68,10 @@ const TemplateListGroupItem: React.FC<TemplateSummaryItemProps> = ({
       onClick={onClick}
       onClick={onClick}
       aria-current="true"
       aria-current="true"
     >
     >
-      <h4 className="mb-1">{localizedTemplate.title}</h4>
+      <h4 className="mb-1 d-flex">
+        <span className="d-inline-block text-truncate">{localizedTemplate.title}</span>
+        {localizedTemplate.pluginId != null ? <i className="icon-fw icon-puzzle ml-2 text-muted small"></i> : ''}
+      </h4>
       <p className="mb-2">{localizedTemplate.desc}</p>
       <p className="mb-2">{localizedTemplate.desc}</p>
       { templateLocales != null && Array.from(templateLocales).map(locale => (
       { templateLocales != null && Array.from(templateLocales).map(locale => (
         <span key={locale} className="badge border rounded-pill text-muted mr-1">{locale}</span>
         <span key={locale} className="badge border rounded-pill text-muted mr-1">{locale}</span>
@@ -94,7 +97,10 @@ const TemplateDropdownItem: React.FC<TemplateSummaryItemProps> = ({
       onClick={onClick}
       onClick={onClick}
       className="px-4 py-3"
       className="px-4 py-3"
     >
     >
-      <h4 className="mb-1 text-wrap">{localizedTemplate.title}</h4>
+      <h4 className="mb-1 d-flex">
+        <span className="d-inline-block text-truncate">{localizedTemplate.title}</span>
+        {localizedTemplate.pluginId != null ? <i className="icon-fw icon-puzzle ml-2 text-muted small"></i> : ''}
+      </h4>
       <p className="mb-1 text-wrap">{localizedTemplate.desc}</p>
       <p className="mb-1 text-wrap">{localizedTemplate.desc}</p>
       { templateLocales != null && Array.from(templateLocales).map(locale => (
       { templateLocales != null && Array.from(templateLocales).map(locale => (
         <span key={locale} className="badge border rounded-pill text-muted mr-1">{locale}</span>
         <span key={locale} className="badge border rounded-pill text-muted mr-1">{locale}</span>
@@ -217,7 +223,7 @@ const TemplateModalSubstance = (props: TemplateModalSubstanceProps): JSX.Element
                   })() }
                   })() }
                 </span>
                 </span>
               </DropdownToggle>
               </DropdownToggle>
-              <DropdownMenu role="menu" className={`p-0 ${styles['dm-templates']}`}>
+              <DropdownMenu role="menu" className={`p-0 mw-100 ${styles['dm-templates']}`}>
                 { templateSummaries != null && templateSummaries.map((templateSummary) => {
                 { templateSummaries != null && templateSummaries.map((templateSummary) => {
                   const templateId = constructTemplateId(templateSummary);
                   const templateId = constructTemplateId(templateSummary);
 
 

+ 1 - 1
apps/app/src/pages/share/[[...path]].page.tsx

@@ -12,7 +12,7 @@ import { useCurrentGrowiLayoutFluidClassName } from '~/client/services/layout';
 import { ShareLinkLayout } from '~/components/Layout/ShareLinkLayout';
 import { ShareLinkLayout } from '~/components/Layout/ShareLinkLayout';
 import GrowiContextualSubNavigationSubstance from '~/components/Navbar/GrowiContextualSubNavigation';
 import GrowiContextualSubNavigationSubstance from '~/components/Navbar/GrowiContextualSubNavigation';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
-import { ShareLinkPageView } from '~/components/ShareLink/ShareLinkPageView';
+import { ShareLinkPageView } from '~/components/ShareLinkPageView';
 import { SupportedAction, SupportedActionType } from '~/interfaces/activity';
 import { SupportedAction, SupportedActionType } from '~/interfaces/activity';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';

+ 2 - 0
apps/app/src/server/service/page.ts

@@ -2319,6 +2319,8 @@ class PageService {
       isAbleToDeleteCompletely: false,
       isAbleToDeleteCompletely: false,
       isRevertible: isTrashPage(page.path),
       isRevertible: isTrashPage(page.path),
       contentAge: page.getContentAge(),
       contentAge: page.getContentAge(),
+      descendantCount: page.descendantCount,
+      commentCount: page.commentCount,
     };
     };
 
 
   }
   }

+ 15 - 0
apps/app/src/stores/context.tsx

@@ -229,6 +229,21 @@ export const useIsReadOnlyUser = (): SWRResponse<boolean, Error> => {
   );
   );
 };
 };
 
 
+export const useIsAdmin = (): SWRResponse<boolean, Error> => {
+  const { data: currentUser, isLoading } = useCurrentUser();
+
+  const isAdminUser = currentUser != null ? currentUser.admin : false;
+
+  return useSWRImmutable(
+    isLoading ? null : ['isAdminUser', currentUser?._id],
+    () => isAdminUser,
+    {
+      fallbackData: isAdminUser,
+      keepPreviousData: true,
+    },
+  );
+};
+
 export const useIsEditable = (): SWRResponse<boolean, Error> => {
 export const useIsEditable = (): SWRResponse<boolean, Error> => {
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();

+ 13 - 4
apps/app/src/stores/modal.tsx

@@ -357,6 +357,7 @@ type PageAccessoriesModalStatus = {
 type PageAccessoriesModalUtils = {
 type PageAccessoriesModalUtils = {
   open(activatedContents: PageAccessoriesModalContents): void
   open(activatedContents: PageAccessoriesModalContents): void
   close(): void
   close(): void
+  selectContents(activatedContents: PageAccessoriesModalContents): void
 }
 }
 
 
 export const usePageAccessoriesModal = (): SWRResponse<PageAccessoriesModalStatus, Error> & PageAccessoriesModalUtils => {
 export const usePageAccessoriesModal = (): SWRResponse<PageAccessoriesModalStatus, Error> & PageAccessoriesModalUtils => {
@@ -364,9 +365,8 @@ export const usePageAccessoriesModal = (): SWRResponse<PageAccessoriesModalStatu
   const initialStatus = { isOpened: false };
   const initialStatus = { isOpened: false };
   const swrResponse = useStaticSWR<PageAccessoriesModalStatus, Error>('pageAccessoriesModalStatus', undefined, { fallbackData: initialStatus });
   const swrResponse = useStaticSWR<PageAccessoriesModalStatus, Error>('pageAccessoriesModalStatus', undefined, { fallbackData: initialStatus });
 
 
-  return {
-    ...swrResponse,
-    open: (activatedContents: PageAccessoriesModalContents) => {
+  return Object.assign(swrResponse, {
+    open: (activatedContents) => {
       if (swrResponse.data == null) {
       if (swrResponse.data == null) {
         return;
         return;
       }
       }
@@ -381,7 +381,16 @@ export const usePageAccessoriesModal = (): SWRResponse<PageAccessoriesModalStatu
       }
       }
       swrResponse.mutate({ isOpened: false });
       swrResponse.mutate({ isOpened: false });
     },
     },
-  };
+    selectContents: (activatedContents) => {
+      if (swrResponse.data == null) {
+        return;
+      }
+      swrResponse.mutate({
+        isOpened: swrResponse.data.isOpened,
+        activatedContents,
+      });
+    },
+  });
 };
 };
 
 
 /*
 /*

+ 35 - 17
apps/app/test/cypress/e2e/20-basic-features/20-basic-features--access-to-page.cy.ts

@@ -7,10 +7,25 @@ function openEditor() {
     });
     });
     // until
     // until
     return cy.get('.layout-root').then($elem => $elem.hasClass('editing'));
     return cy.get('.layout-root').then($elem => $elem.hasClass('editing'));
-  })
+  });
   cy.get('.CodeMirror').should('be.visible');
   cy.get('.CodeMirror').should('be.visible');
 }
 }
 
 
+function appendTextToEditorUntilContains(inputText: string) {
+  const lines: string[] = [];
+  cy.waitUntil(() => {
+    // do
+    cy.get('.CodeMirror textarea').type(inputText, { force: true });
+    // until
+    return cy.get('.CodeMirror-line')
+      .each(($item) => {
+        lines.push($item.text());
+      }).then(() => {
+        return lines.join('\n').endsWith(inputText);
+      });
+  });
+}
+
 context('Access to page', () => {
 context('Access to page', () => {
   const ssPrefix = 'access-to-page-';
   const ssPrefix = 'access-to-page-';
 
 
@@ -23,20 +38,20 @@ context('Access to page', () => {
 
 
   it('/Sandbox is successfully loaded', () => {
   it('/Sandbox is successfully loaded', () => {
     cy.visit('/Sandbox');
     cy.visit('/Sandbox');
-    cy.waitUntilSkeletonDisappear();
+    cy.collapseSidebar(true, true);
 
 
     // for check download toc data
     // for check download toc data
     // https://redmine.weseek.co.jp/issues/111384
     // https://redmine.weseek.co.jp/issues/111384
     // cy.get('.toc-link').should('be.visible');
     // cy.get('.toc-link').should('be.visible');
 
 
-    cy.collapseSidebar(true, true);
+    cy.waitUntilSkeletonDisappear();
     cy.screenshot(`${ssPrefix}-sandbox`);
     cy.screenshot(`${ssPrefix}-sandbox`);
   });
   });
 
 
   // TODO: https://redmine.weseek.co.jp/issues/109939
   // TODO: https://redmine.weseek.co.jp/issues/109939
   it('/Sandbox with anchor hash is successfully loaded', () => {
   it('/Sandbox with anchor hash is successfully loaded', () => {
     cy.visit('/Sandbox#headers');
     cy.visit('/Sandbox#headers');
-    cy.waitUntilSkeletonDisappear();
+    cy.collapseSidebar(true);
 
 
     // for check download toc data
     // for check download toc data
     // https://redmine.weseek.co.jp/issues/111384
     // https://redmine.weseek.co.jp/issues/111384
@@ -45,18 +60,21 @@ context('Access to page', () => {
     // hide fab
     // hide fab
     cy.getByTestid('grw-fab-container').invoke('attr', 'style', 'display: none');
     cy.getByTestid('grw-fab-container').invoke('attr', 'style', 'display: none');
 
 
+    // assert the element is in viewport
+    cy.get('#headers').should('be.inViewport');
+
     // remove animation for screenshot
     // remove animation for screenshot
     // remove 'blink' class because ::after element cannot be operated
     // 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
     // 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.get('#headers').invoke('removeClass', 'blink');
 
 
-    cy.collapseSidebar(true);
+    cy.waitUntilSkeletonDisappear();
     cy.screenshot(`${ssPrefix}-sandbox-headers`);
     cy.screenshot(`${ssPrefix}-sandbox-headers`);
   });
   });
 
 
   it('/Sandbox/Math is successfully loaded', () => {
   it('/Sandbox/Math is successfully loaded', () => {
     cy.visit('/Sandbox/Math');
     cy.visit('/Sandbox/Math');
-    cy.waitUntilSkeletonDisappear();
+    cy.collapseSidebar(true);
 
 
     // for check download toc data
     // for check download toc data
     // https://redmine.weseek.co.jp/issues/111384
     // https://redmine.weseek.co.jp/issues/111384
@@ -64,41 +82,41 @@ context('Access to page', () => {
 
 
     cy.get('.math').should('be.visible');
     cy.get('.math').should('be.visible');
 
 
-    cy.collapseSidebar(true);
+    cy.waitUntilSkeletonDisappear();
     cy.screenshot(`${ssPrefix}-sandbox-math`);
     cy.screenshot(`${ssPrefix}-sandbox-math`);
   });
   });
 
 
   it('/Sandbox with edit is successfully loaded', () => {
   it('/Sandbox with edit is successfully loaded', () => {
     cy.visit('/Sandbox#edit');
     cy.visit('/Sandbox#edit');
-    cy.waitUntilSkeletonDisappear();
+    cy.collapseSidebar(true);
 
 
     cy.getByTestid('navbar-editor').should('be.visible');
     cy.getByTestid('navbar-editor').should('be.visible');
     cy.get('.grw-editor-navbar-bottom').should('be.visible');
     cy.get('.grw-editor-navbar-bottom').should('be.visible');
     cy.getByTestid('save-page-btn').should('be.visible');
     cy.getByTestid('save-page-btn').should('be.visible');
     cy.get('.grw-grant-selector').should('be.visible');
     cy.get('.grw-grant-selector').should('be.visible');
 
 
-    cy.collapseSidebar(true);
+    cy.waitUntilSkeletonDisappear();
     cy.screenshot(`${ssPrefix}-Sandbox-edit-page`);
     cy.screenshot(`${ssPrefix}-Sandbox-edit-page`);
   })
   })
 
 
   const body1 = 'hello';
   const body1 = 'hello';
   const body2 = ' world!';
   const body2 = ' world!';
-  it('View and Edit contents are successfully loaded', () => {
+  it('Edit and save with save-page-btn', () => {
     cy.visit('/Sandbox/testForUseEditingMarkdown');
     cy.visit('/Sandbox/testForUseEditingMarkdown');
 
 
     openEditor();
     openEditor();
 
 
     // check edited contents after save
     // check edited contents after save
-    cy.get('.CodeMirror textarea').type(body1, { force: true });
-    cy.get('.CodeMirror-code').should('contain.text', body1);
+    appendTextToEditorUntilContains(body1);
     cy.get('.page-editor-preview-body').should('contain.text', body1);
     cy.get('.page-editor-preview-body').should('contain.text', body1);
     cy.getByTestid('page-editor').should('be.visible');
     cy.getByTestid('page-editor').should('be.visible');
     cy.getByTestid('save-page-btn').click();
     cy.getByTestid('save-page-btn').click();
     cy.get('.wiki').should('be.visible');
     cy.get('.wiki').should('be.visible');
     cy.get('.wiki').children().first().should('have.text', body1);
     cy.get('.wiki').children().first().should('have.text', body1);
+    cy.screenshot(`${ssPrefix}-edit-and-save-with-save-page-btn`);
   })
   })
 
 
-  it('Editing contents are successfully loaded with shortcut key', () => {
+  it('Edit and save with shortcut key', () => {
     const savePageShortcutKey = '{ctrl+s}';
     const savePageShortcutKey = '{ctrl+s}';
 
 
     cy.visit('/Sandbox/testForUseEditingMarkdown');
     cy.visit('/Sandbox/testForUseEditingMarkdown');
@@ -106,25 +124,25 @@ context('Access to page', () => {
     openEditor();
     openEditor();
 
 
     // check editing contents with shortcut key
     // check editing contents with shortcut key
-    cy.get('.CodeMirror textarea').type(body2, { force: true });
-    cy.get('.CodeMirror-code').should('contain.text', body1+body2);
+    appendTextToEditorUntilContains(body2);
     cy.get('.page-editor-preview-body').should('contain.text', body1+body2);
     cy.get('.page-editor-preview-body').should('contain.text', body1+body2);
     cy.get('.CodeMirror').click().type(savePageShortcutKey);
     cy.get('.CodeMirror').click().type(savePageShortcutKey);
     cy.get('.CodeMirror-code').should('contain.text', body1+body2);
     cy.get('.CodeMirror-code').should('contain.text', body1+body2);
     cy.get('.page-editor-preview-body').should('contain.text', body1+body2);
     cy.get('.page-editor-preview-body').should('contain.text', body1+body2);
+    cy.screenshot(`${ssPrefix}-edit-and-save-with-shortcut-key`);
   })
   })
 
 
   it('/user/admin is successfully loaded', () => {
   it('/user/admin is successfully loaded', () => {
     cy.visit('/user/admin');
     cy.visit('/user/admin');
+    cy.collapseSidebar(true);
 
 
-    cy.waitUntilSkeletonDisappear();
     // for check download toc data
     // for check download toc data
     // https://redmine.weseek.co.jp/issues/111384
     // https://redmine.weseek.co.jp/issues/111384
     // cy.get('.toc-link').should('be.visible');
     // cy.get('.toc-link').should('be.visible');
 
 
     // eslint-disable-next-line cypress/no-unnecessary-waiting
     // eslint-disable-next-line cypress/no-unnecessary-waiting
     cy.wait(2000); // wait for calcViewHeight and rendering
     cy.wait(2000); // wait for calcViewHeight and rendering
-    cy.collapseSidebar(true);
+    cy.waitUntilSkeletonDisappear();
     cy.screenshot(`${ssPrefix}-user-admin`);
     cy.screenshot(`${ssPrefix}-user-admin`);
   });
   });
 
 

+ 2 - 4
apps/app/test/cypress/e2e/20-basic-features/20-basic-features--use-tools.cy.ts

@@ -120,6 +120,7 @@ context('Page Accessories Modal', () => {
     });
     });
 
 
     cy.visit('/');
     cy.visit('/');
+    cy.collapseSidebar(true, true);
 
 
     cy.waitUntil(() => {
     cy.waitUntil(() => {
       // do
       // do
@@ -141,7 +142,6 @@ context('Page Accessories Modal', () => {
 
 
     cy.getByTestid('page-history').should('be.visible');
     cy.getByTestid('page-history').should('be.visible');
 
 
-    cy.collapseSidebar(true, true);
     cy.waitUntilSpinnerDisappear();
     cy.waitUntilSpinnerDisappear();
     cy.screenshot(`${ssPrefix}-open-page-history-bootstrap4`);
     cy.screenshot(`${ssPrefix}-open-page-history-bootstrap4`);
   });
   });
@@ -154,7 +154,6 @@ context('Page Accessories Modal', () => {
     cy.waitUntilSpinnerDisappear();
     cy.waitUntilSpinnerDisappear();
     cy.getByTestid('page-attachment').should('be.visible').contains('No attachments yet.');
     cy.getByTestid('page-attachment').should('be.visible').contains('No attachments yet.');
 
 
-    cy.collapseSidebar(true);
     cy.screenshot(`${ssPrefix}-open-page-attachment-data-bootstrap4`);
     cy.screenshot(`${ssPrefix}-open-page-attachment-data-bootstrap4`);
   });
   });
 
 
@@ -167,7 +166,6 @@ context('Page Accessories Modal', () => {
     cy.getByTestid('page-accessories-modal').should('be.visible');
     cy.getByTestid('page-accessories-modal').should('be.visible');
     cy.getByTestid('share-link-management').should('be.visible');
     cy.getByTestid('share-link-management').should('be.visible');
 
 
-    cy.collapseSidebar(true);
     cy.screenshot(`${ssPrefix}-open-share-link-management-bootstrap4`);
     cy.screenshot(`${ssPrefix}-open-share-link-management-bootstrap4`);
   });
   });
 });
 });
@@ -186,6 +184,7 @@ context('Tag Oprations', { scrollBehavior: false }, () =>{
     const tag = 'we';
     const tag = 'we';
 
 
     cy.visit('/Sandbox/Bootstrap4');
     cy.visit('/Sandbox/Bootstrap4');
+    cy.collapseSidebar(true);
 
 
     // Add tag
     // Add tag
     cy.get('#edit-tags-btn-wrapper-for-tooltip').as('edit-tag-tooltip').should('be.visible');
     cy.get('#edit-tags-btn-wrapper-for-tooltip').as('edit-tag-tooltip').should('be.visible');
@@ -198,7 +197,6 @@ context('Tag Oprations', { scrollBehavior: false }, () =>{
       return cy.get('#edit-tag-modal').then($elem => $elem.is(':visible'));
       return cy.get('#edit-tag-modal').then($elem => $elem.is(':visible'));
     });
     });
 
 
-    cy.collapseSidebar(true);
     cy.get('#edit-tag-modal').should('be.visible').screenshot(`${ssPrefix}1-edit-tag-input`);
     cy.get('#edit-tag-modal').should('be.visible').screenshot(`${ssPrefix}1-edit-tag-input`);
 
 
     cy.get('#edit-tag-modal').should('be.visible').within(() => {
     cy.get('#edit-tag-modal').should('be.visible').within(() => {

+ 10 - 6
apps/app/test/cypress/e2e/21-basic-features-for-guest/21-basic-features-for-guest--access-to-page.cy.ts

@@ -11,23 +11,27 @@ context('Access to page by guest', () => {
 
 
   // TODO: https://redmine.weseek.co.jp/issues/109939
   // TODO: https://redmine.weseek.co.jp/issues/109939
   it('/Sandbox with anchor hash is successfully loaded', () => {
   it('/Sandbox with anchor hash is successfully loaded', () => {
-    cy.visit('/Sandbox#Headers');
-    cy.waitUntilSkeletonDisappear();
+    cy.visit('/Sandbox#headers');
+    cy.collapseSidebar(true);
 
 
     // hide fab
     // hide fab
     cy.getByTestid('grw-fab-container').invoke('attr', 'style', 'display: none');
     cy.getByTestid('grw-fab-container').invoke('attr', 'style', 'display: none');
 
 
+    // assert the element is in viewport
+    cy.get('#headers').should('be.inViewport');
+
     // remove animation for screenshot
     // remove animation for screenshot
     // remove 'blink' class because ::after element cannot be operated
     // 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
     // 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.get('#headers').invoke('removeClass', 'blink');
 
 
+    cy.waitUntilSkeletonDisappear();
     cy.screenshot(`${ssPrefix}-sandbox-headers`);
     cy.screenshot(`${ssPrefix}-sandbox-headers`);
   });
   });
 
 
   it('/Sandbox/Math is successfully loaded', () => {
   it('/Sandbox/Math is successfully loaded', () => {
     cy.visit('/Sandbox/Math');
     cy.visit('/Sandbox/Math');
-    cy.waitUntilSkeletonDisappear();
+    cy.collapseSidebar(true);
 
 
     // for check download toc data
     // for check download toc data
     // https://redmine.weseek.co.jp/issues/111384
     // https://redmine.weseek.co.jp/issues/111384
@@ -35,15 +39,15 @@ context('Access to page by guest', () => {
 
 
     cy.get('.math').should('be.visible');
     cy.get('.math').should('be.visible');
 
 
-    cy.collapseSidebar(true);
+    cy.waitUntilSkeletonDisappear();
     cy.screenshot(`${ssPrefix}-sandbox-math`);
     cy.screenshot(`${ssPrefix}-sandbox-math`);
   });
   });
 
 
   it('/Sandbox with edit is successfully loaded', () => {
   it('/Sandbox with edit is successfully loaded', () => {
     cy.visit('/Sandbox#edit');
     cy.visit('/Sandbox#edit');
-    cy.waitUntilSkeletonDisappear();
-
     cy.collapseSidebar(true);
     cy.collapseSidebar(true);
+
+    cy.waitUntilSkeletonDisappear();
     cy.screenshot(`${ssPrefix}-sandbox-with-edit-hash`);
     cy.screenshot(`${ssPrefix}-sandbox-with-edit-hash`);
   })
   })
 
 

+ 3 - 6
apps/app/test/cypress/e2e/22-sharelink/22-sharelink--access-to-sharelink.cy.ts

@@ -14,14 +14,11 @@ context('Access to sharelink by guest', () => {
     // open dropdown
     // open dropdown
     cy.waitUntil(() => {
     cy.waitUntil(() => {
       // do
       // do
-      cy.getByTestid('grw-contextual-sub-nav').should('be.visible').within(() => {
-        cy.waitUntilSkeletonDisappear();
-        cy.getByTestid('open-page-item-control-btn').find('button').first().as('btn').click();
+      cy.get('#grw-subnav-container').within(() => {
+        cy.getByTestid('open-page-item-control-btn', { timeout: 14000 }).find('button').click({force: true});
       });
       });
       // wait until
       // wait until
-      return cy.get('body').within(() => {
-        return Cypress.$('.dropdown-menu.show').is(':visible');
-      });
+      return cy.getByTestid('page-item-control-menu').then($elem => $elem.is(':visible'))
     });
     });
 
 
     // open modal
     // open modal

+ 6 - 5
apps/app/test/cypress/e2e/23-editor/23-editor--saving.cy.ts

@@ -11,6 +11,7 @@ context('PageCreateModal', () => {
 
 
   it("PageCreateModal is shown and closed successfully", () => {
   it("PageCreateModal is shown and closed successfully", () => {
     cy.visit('/');
     cy.visit('/');
+    cy.collapseSidebar(true, true);
 
 
     cy.waitUntil(() => {
     cy.waitUntil(() => {
       // do
       // do
@@ -24,13 +25,13 @@ context('PageCreateModal', () => {
       cy.get('button.close').click();
       cy.get('button.close').click();
     });
     });
 
 
-    cy.collapseSidebar(true, true);
     cy.screenshot(`${ssPrefix}page-create-modal-closed`);
     cy.screenshot(`${ssPrefix}page-create-modal-closed`);
   });
   });
 
 
   it("Successfully Create Today's page", () => {
   it("Successfully Create Today's page", () => {
     const pageName = "Today's page";
     const pageName = "Today's page";
     cy.visit('/');
     cy.visit('/');
+    cy.collapseSidebar(true);
 
 
     cy.waitUntil(() => {
     cy.waitUntil(() => {
       // do
       // do
@@ -55,7 +56,6 @@ context('PageCreateModal', () => {
     });
     });
     cy.get('.layout-root').should('not.have.class', 'editing');
     cy.get('.layout-root').should('not.have.class', 'editing');
 
 
-    cy.collapseSidebar(true);
     cy.waitUntilSkeletonDisappear();
     cy.waitUntilSkeletonDisappear();
     cy.screenshot(`${ssPrefix}create-today-page`);
     cy.screenshot(`${ssPrefix}create-today-page`);
   });
   });
@@ -64,6 +64,7 @@ context('PageCreateModal', () => {
     const pageName = 'child';
     const pageName = 'child';
 
 
     cy.visit('/foo/bar');
     cy.visit('/foo/bar');
+    cy.collapseSidebar(true);
 
 
     cy.waitUntil(() => {
     cy.waitUntil(() => {
       // do
       // do
@@ -94,12 +95,12 @@ context('PageCreateModal', () => {
     cy.getByTestid('grw-contextual-sub-nav').should('be.visible');
     cy.getByTestid('grw-contextual-sub-nav').should('be.visible');
 
 
     cy.waitUntilSkeletonDisappear();
     cy.waitUntilSkeletonDisappear();
-    cy.collapseSidebar(true);
     cy.screenshot(`${ssPrefix}create-page-under-specific-page`);
     cy.screenshot(`${ssPrefix}create-page-under-specific-page`);
   });
   });
 
 
   it('Trying to create template page under the root page fail', () => {
   it('Trying to create template page under the root page fail', () => {
     cy.visit('/');
     cy.visit('/');
+    cy.collapseSidebar(true);
 
 
     cy.waitUntil(() => {
     cy.waitUntil(() => {
       // do
       // do
@@ -116,8 +117,9 @@ context('PageCreateModal', () => {
       cy.getByTestid('grw-btn-edit-page').should('be.visible').click();
       cy.getByTestid('grw-btn-edit-page').should('be.visible').click();
     });
     });
     cy.get('.Toastify__toast').should('be.visible');
     cy.get('.Toastify__toast').should('be.visible');
-    cy.collapseSidebar(true);
+
     cy.screenshot(`${ssPrefix}create-template-for-children-error`);
     cy.screenshot(`${ssPrefix}create-template-for-children-error`);
+
     cy.get('.Toastify__toast').should('be.visible').within(() => {
     cy.get('.Toastify__toast').should('be.visible').within(() => {
       cy.get('.Toastify__close-button').should('be.visible').click();
       cy.get('.Toastify__close-button').should('be.visible').click();
       cy.get('.Toastify__progress-bar').invoke('attr', 'style', 'display: none')
       cy.get('.Toastify__progress-bar').invoke('attr', 'style', 'display: none')
@@ -129,7 +131,6 @@ context('PageCreateModal', () => {
       cy.getByTestid('grw-btn-edit-page').should('be.visible').click();
       cy.getByTestid('grw-btn-edit-page').should('be.visible').click();
     });
     });
     cy.get('.Toastify__toast').should('be.visible');
     cy.get('.Toastify__toast').should('be.visible');
-    cy.collapseSidebar(true);
     cy.screenshot(`${ssPrefix}create-template-for-descendants-error`);
     cy.screenshot(`${ssPrefix}create-template-for-descendants-error`);
   });
   });
 
 

+ 1 - 7
apps/app/test/cypress/e2e/50-sidebar/50-sidebar--access-to-side-bar.cy.ts

@@ -21,11 +21,6 @@ describe('Access to sidebar', () => {
       beforeEach(() => {
       beforeEach(() => {
         cy.visit('/');
         cy.visit('/');
 
 
-        // Workaround for waitinig initial open/close interaction
-        // TODO: remove this cy.wait() after SSR without the initial interaction is implemented
-        // eslint-disable-next-line cypress/no-unnecessary-waiting
-        cy.wait(2000);
-
         // Since this is a sidebar test, call collapseSidebar in beforeEach.
         // Since this is a sidebar test, call collapseSidebar in beforeEach.
         cy.collapseSidebar(false);
         cy.collapseSidebar(false);
       });
       });
@@ -212,7 +207,7 @@ describe('Access to sidebar', () => {
         it('Successfully redirect to editor', () => {
         it('Successfully redirect to editor', () => {
           const content = '# HELLO \n ## Hello\n ### Hello';
           const content = '# HELLO \n ## Hello\n ### Hello';
 
 
-          cy.get('.grw-sidebar-content-header > h3').find('a').click();
+          cy.get('.grw-sidebar-content-header > h3 > a').should('be.visible').click();
 
 
           cy.get('.layout-root').should('have.class', 'editing');
           cy.get('.layout-root').should('have.class', 'editing');
           cy.get('.CodeMirror textarea').type(content, {force: true});
           cy.get('.CodeMirror textarea').type(content, {force: true});
@@ -220,7 +215,6 @@ describe('Access to sidebar', () => {
           cy.screenshot(`${ssPrefix}custom-sidebar-2-redirect-to-editor`, { blackout: blackoutOverride });
           cy.screenshot(`${ssPrefix}custom-sidebar-2-redirect-to-editor`, { blackout: blackoutOverride });
 
 
           cy.getByTestid('save-page-btn').click();
           cy.getByTestid('save-page-btn').click();
-          cy.get('.layout-root').should('not.have.class', 'editing');
         });
         });
 
 
         it('Successfully create custom sidebar content', () => {
         it('Successfully create custom sidebar content', () => {

+ 0 - 13
apps/app/test/cypress/e2e/50-sidebar/50-sidebar--switching-sidebar-mode.cy.ts

@@ -9,28 +9,15 @@ const blackoutOverride = [
 context('Switch sidebar mode', () => {
 context('Switch sidebar mode', () => {
   const ssPrefix = 'switch-sidebar-mode-';
   const ssPrefix = 'switch-sidebar-mode-';
 
 
-  let connectSid: string | undefined;
-
   before(() => {
   before(() => {
     // login
     // login
     cy.fixture("user-admin.json").then(user => {
     cy.fixture("user-admin.json").then(user => {
       cy.login(user.username, user.password);
       cy.login(user.username, user.password);
     });
     });
-    cy.getCookie('connect.sid').then(cookie => {
-      connectSid = cookie?.value;
-    });
-  });
-
-  beforeEach(() => {
-    if (connectSid != null) {
-      cy.setCookie('connect.sid', connectSid);
-    }
   });
   });
 
 
   it('Switching sidebar mode', () => {
   it('Switching sidebar mode', () => {
     cy.visit('/');
     cy.visit('/');
-    // This test uses collapseSidebar here, because this test for the sidebar.
-    cy.collapseSidebar(true)
     cy.get('.grw-apperance-mode-dropdown').first().click();
     cy.get('.grw-apperance-mode-dropdown').first().click();
 
 
     cy.get('[for="swSidebarMode"]').click({force: true});
     cy.get('[for="swSidebarMode"]').click({force: true});

+ 21 - 0
apps/app/test/cypress/support/assertions.ts

@@ -0,0 +1,21 @@
+// from https://github.com/cypress-io/cypress/issues/877#issuecomment-538708750
+const isInViewport = (_chai) => {
+  function assertIsInViewport() {
+
+    const subject = this._obj;
+
+    const bottom = Cypress.config("viewportWidth");
+    const rect = subject[0].getBoundingClientRect();
+
+    this.assert(
+      rect.top < bottom && rect.bottom < bottom,
+      "expected #{this} to be in viewport",
+      "expected #{this} to not be in viewport",
+      this._obj
+    )
+  }
+
+  _chai.Assertion.addMethod('inViewport', assertIsInViewport)
+};
+
+chai.use(isInViewport);

+ 22 - 19
apps/app/test/cypress/support/commands.ts

@@ -70,31 +70,34 @@ Cypress.Commands.add('waitUntilSpinnerDisappear', () => {
 });
 });
 
 
 Cypress.Commands.add('collapseSidebar', (isCollapsed: boolean, waitUntilSaving = false) => {
 Cypress.Commands.add('collapseSidebar', (isCollapsed: boolean, waitUntilSaving = false) => {
-  cy.getByTestid('grw-sidebar-wrapper', { timeout: 5000 }).within(() => {
+  cy.getByTestid('grw-sidebar').within(($sidebar) => {
+
     // skip if .grw-sidebar-dock does not exist
     // skip if .grw-sidebar-dock does not exist
-    if (isHidden(Cypress.$('.grw-sidebar-dock'))) {
+    if (!$sidebar.hasClass('grw-sidebar-dock')) {
       return;
       return;
     }
     }
 
 
-    // process only when Dock Mode
-    cy.get('.grw-sidebar-dock').within(() => {
-      const isSidebarContextualNavigationHidden = isHiddenByTestId('grw-contextual-navigation-sub');
-      if (isSidebarContextualNavigationHidden === isCollapsed) {
-        return;
-      }
+  });
 
 
-      cy.waitUntil(() => {
-        // do
-        cy.getByTestid("grw-navigation-resize-button").click({force: true});
-        // wait until saving UserUISettings
-        if (waitUntilSaving) {
-          // eslint-disable-next-line cypress/no-unnecessary-waiting
-          cy.wait(1500);
-        }
+  cy.getByTestid('grw-sidebar').should('be.visible').within(() => {
 
 
-        // wait until
-        return cy.getByTestid('grw-contextual-navigation-sub').then($contents => isHidden($contents) === isCollapsed);
-      });
+    const isSidebarContextualNavigationHidden = isHiddenByTestId('grw-contextual-navigation-sub');
+    if (isSidebarContextualNavigationHidden === isCollapsed) {
+      return;
+    }
+
+    cy.waitUntil(() => {
+      // do
+      cy.getByTestid("grw-navigation-resize-button").click({force: true});
+      // wait until saving UserUISettings
+      if (waitUntilSaving) {
+        // eslint-disable-next-line cypress/no-unnecessary-waiting
+        cy.wait(1500);
+      }
+
+      // wait until
+      return cy.getByTestid('grw-contextual-navigation-sub').then($contents => isHidden($contents) === isCollapsed);
     });
     });
   });
   });
+
 });
 });

+ 1 - 0
apps/app/test/cypress/support/index.ts

@@ -14,6 +14,7 @@
 // ***********************************************************
 // ***********************************************************
 
 
 // Import commands.js using ES2015 syntax:
 // Import commands.js using ES2015 syntax:
+import './assertions'
 import './commands'
 import './commands'
 import './screenshot'
 import './screenshot'
 
 

+ 4 - 1
apps/app/test/cypress/tsconfig.json

@@ -4,7 +4,10 @@
     "noEmit": true,
     "noEmit": true,
     // be explicit about types included
     // be explicit about types included
     // to avoid clashing with Jest types
     // to avoid clashing with Jest types
-    "types": ["cypress"]
+    "types": ["cypress"],
+    // turn off sourceMap
+    // see: https://github.com/cypress-io/cypress/issues/26203
+    "sourceMap": false
   },
   },
   "include": [
   "include": [
     "../../node_modules/cypress",
     "../../node_modules/cypress",

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

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/slackbot-proxy",
   "name": "@growi/slackbot-proxy",
-  "version": "6.1.7-slackbot-proxy.0",
+  "version": "6.1.8-slackbot-proxy.0",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",

+ 7 - 7
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "6.1.7-RC.0",
+  "version": "6.1.8-RC.0",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",
@@ -52,7 +52,7 @@
     "@swc-node/register": "^1.6.2",
     "@swc-node/register": "^1.6.2",
     "@swc/core": "^1.3.36",
     "@swc/core": "^1.3.36",
     "@swc/helpers": "^0.4.14",
     "@swc/helpers": "^0.4.14",
-    "@testing-library/cypress": "^8.0.2",
+    "@testing-library/cypress": "^9.0.0",
     "@types/css-modules": "^1.0.2",
     "@types/css-modules": "^1.0.2",
     "@types/eslint": "^8.37.0",
     "@types/eslint": "^8.37.0",
     "@types/estree": "^1.0.1",
     "@types/estree": "^1.0.1",
@@ -60,10 +60,10 @@
     "@types/path-browserify": "^1.0.0",
     "@types/path-browserify": "^1.0.0",
     "@typescript-eslint/eslint-plugin": "^5.59.7",
     "@typescript-eslint/eslint-plugin": "^5.59.7",
     "@typescript-eslint/parser": "^5.59.7",
     "@typescript-eslint/parser": "^5.59.7",
-    "@vitejs/plugin-react": "^3.1.0",
+    "@vitejs/plugin-react": "^4.0.3",
     "@vitest/coverage-c8": "^0.31.1",
     "@vitest/coverage-c8": "^0.31.1",
     "@vitest/ui": "^0.31.1",
     "@vitest/ui": "^0.31.1",
-    "cypress": "^12.0.1",
+    "cypress": "^12.17.2",
     "cypress-wait-until": "^1.7.2",
     "cypress-wait-until": "^1.7.2",
     "eslint": "^8.41.0",
     "eslint": "^8.41.0",
     "eslint-config-next": "^12.1.6",
     "eslint-config-next": "^12.1.6",
@@ -89,9 +89,9 @@
     "stylelint-config-recess-order": "^3.0.0",
     "stylelint-config-recess-order": "^3.0.0",
     "ts-node-dev": "^2.0.0",
     "ts-node-dev": "^2.0.0",
     "tsconfig-paths": "^3.9.0",
     "tsconfig-paths": "^3.9.0",
-    "typescript": "~4.9",
-    "vite": "^4.3.8",
-    "vite-plugin-dts": "^2.0.0-beta.0",
+    "typescript": "~5.0.0",
+    "vite": "^4.4.0",
+    "vite-plugin-dts": "^2.3.0",
     "vite-tsconfig-paths": "^4.2.0",
     "vite-tsconfig-paths": "^4.2.0",
     "vitest": "^0.31.4",
     "vitest": "^0.31.4",
     "vitest-mock-extended": "^1.1.3"
     "vitest-mock-extended": "^1.1.3"

+ 1 - 1
packages/core/package.json

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

+ 2 - 0
packages/core/src/interfaces/page.ts

@@ -87,6 +87,8 @@ export type IPageInfoForEntity = IPageInfo & {
   sumOfSeenUsers: number,
   sumOfSeenUsers: number,
   seenUserIds: string[],
   seenUserIds: string[],
   contentAge: number,
   contentAge: number,
+  descendantCount: number,
+  commentCount: number,
 }
 }
 
 
 export type IPageInfoForOperation = IPageInfoForEntity & {
 export type IPageInfoForOperation = IPageInfoForEntity & {

+ 1 - 1
packages/core/vite.config.ts

@@ -7,7 +7,7 @@ import dts from 'vite-plugin-dts';
 // https://vitejs.dev/config/
 // https://vitejs.dev/config/
 export default defineConfig({
 export default defineConfig({
   plugins: [
   plugins: [
-    dts(),
+    dts({ copyDtsFiles: true }),
   ],
   ],
   build: {
   build: {
     outDir: 'dist',
     outDir: 'dist',

+ 1 - 1
packages/hackmd/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/hackmd",
   "name": "@growi/hackmd",
-  "version": "6.1.7-RC.0",
+  "version": "6.1.8-RC.0",
   "description": "GROWI js and css files to use hackmd",
   "description": "GROWI js and css files to use hackmd",
   "license": "MIT",
   "license": "MIT",
   "type": "module",
   "type": "module",

+ 1 - 1
packages/hackmd/vite.config.js

@@ -5,7 +5,7 @@ import dts from 'vite-plugin-dts';
 // https://vitejs.dev/config/
 // https://vitejs.dev/config/
 export default defineConfig({
 export default defineConfig({
   plugins: [
   plugins: [
-    dts(),
+    dts({ copyDtsFiles: true }),
   ],
   ],
   build: {
   build: {
     outDir: 'dist',
     outDir: 'dist',

+ 1 - 1
packages/pluginkit/vite.config.ts

@@ -8,7 +8,7 @@ import dts from 'vite-plugin-dts';
 // https://vitejs.dev/config/
 // https://vitejs.dev/config/
 export default defineConfig({
 export default defineConfig({
   plugins: [
   plugins: [
-    dts(),
+    dts({ copyDtsFiles: true }),
   ],
   ],
   build: {
   build: {
     outDir: 'dist',
     outDir: 'dist',

+ 1 - 1
packages/presentation/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/presentation",
   "name": "@growi/presentation",
-  "version": "6.1.7-RC.0",
+  "version": "6.1.8-RC.0",
   "description": "GROWI plugin for presentation",
   "description": "GROWI plugin for presentation",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

+ 1 - 1
packages/presentation/vite.config.ts

@@ -6,7 +6,7 @@ import dts from 'vite-plugin-dts';
 export default defineConfig({
 export default defineConfig({
   plugins: [
   plugins: [
     react(),
     react(),
-    dts(),
+    dts({ copyDtsFiles: true }),
   ],
   ],
   build: {
   build: {
     outDir: 'dist',
     outDir: 'dist',

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

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/preset-templates",
   "name": "@growi/preset-templates",
-  "version": "6.1.7-RC.0",
+  "version": "6.1.8-RC.0",
   "scripts": {
   "scripts": {
     "test": "vitest run",
     "test": "vitest run",
     "version": "yarn version --no-git-tag-version --preid=RC"
     "version": "yarn version --no-git-tag-version --preid=RC"

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

@@ -1,7 +1,7 @@
 {
 {
   "name": "@growi/preset-themes",
   "name": "@growi/preset-themes",
   "description": "GROWI preset themes",
   "description": "GROWI preset themes",
-  "version": "6.1.7-RC.0",
+  "version": "6.1.8-RC.0",
   "license": "MIT",
   "license": "MIT",
   "main": "dist/libs/preset-themes.umd.js",
   "main": "dist/libs/preset-themes.umd.js",
   "module": "dist/libs/preset-themes.mjs",
   "module": "dist/libs/preset-themes.mjs",

+ 1 - 1
packages/preset-themes/vite.libs.config.ts

@@ -4,7 +4,7 @@ import dts from 'vite-plugin-dts';
 // https://vitejs.dev/config/
 // https://vitejs.dev/config/
 export default defineConfig({
 export default defineConfig({
   plugins: [
   plugins: [
-    dts(),
+    dts({ copyDtsFiles: true }),
   ],
   ],
   build: {
   build: {
     outDir: 'dist/libs',
     outDir: 'dist/libs',

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

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/remark-attachment-refs",
   "name": "@growi/remark-attachment-refs",
-  "version": "6.1.7-RC.0",
+  "version": "6.1.8-RC.0",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

+ 1 - 1
packages/remark-attachment-refs/vite.client.config.ts

@@ -6,7 +6,7 @@ import dts from 'vite-plugin-dts';
 export default defineConfig({
 export default defineConfig({
   plugins: [
   plugins: [
     react(),
     react(),
-    dts(),
+    dts({ copyDtsFiles: true }),
   ],
   ],
   build: {
   build: {
     outDir: 'dist/client',
     outDir: 'dist/client',

+ 1 - 1
packages/remark-attachment-refs/vite.server.config.ts

@@ -4,7 +4,7 @@ import dts from 'vite-plugin-dts';
 // https://vitejs.dev/config/
 // https://vitejs.dev/config/
 export default defineConfig({
 export default defineConfig({
   plugins: [
   plugins: [
-    dts(),
+    dts({ copyDtsFiles: true }),
   ],
   ],
   build: {
   build: {
     outDir: 'dist/server',
     outDir: 'dist/server',

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

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

+ 1 - 1
packages/remark-drawio/vite.config.ts

@@ -6,7 +6,7 @@ import dts from 'vite-plugin-dts';
 export default defineConfig({
 export default defineConfig({
   plugins: [
   plugins: [
     react(),
     react(),
-    dts(),
+    dts({ copyDtsFiles: true }),
   ],
   ],
   build: {
   build: {
     outDir: 'dist',
     outDir: 'dist',

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

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

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

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/remark-lsx",
   "name": "@growi/remark-lsx",
-  "version": "6.1.7-RC.0",
+  "version": "6.1.8-RC.0",
   "description": "GROWI plugin to list pages",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [
@@ -37,7 +37,7 @@
     "@growi/ui": "link:../ui",
     "@growi/ui": "link:../ui",
     "escape-string-regexp": "^4.0.0",
     "escape-string-regexp": "^4.0.0",
     "express": "^4.16.1",
     "express": "^4.16.1",
-    "mongoose": "^6.5.0",
+    "mongoose": "^6.11.3",
     "swr": "^2.0.3"
     "swr": "^2.0.3"
   },
   },
   "devDependencies": {
   "devDependencies": {

+ 1 - 1
packages/remark-lsx/vite.client.config.ts

@@ -6,7 +6,7 @@ import dts from 'vite-plugin-dts';
 export default defineConfig({
 export default defineConfig({
   plugins: [
   plugins: [
     react(),
     react(),
-    dts(),
+    dts({ copyDtsFiles: true }),
   ],
   ],
   build: {
   build: {
     outDir: 'dist/client',
     outDir: 'dist/client',

+ 1 - 1
packages/remark-lsx/vite.server.config.ts

@@ -4,7 +4,7 @@ import dts from 'vite-plugin-dts';
 // https://vitejs.dev/config/
 // https://vitejs.dev/config/
 export default defineConfig({
 export default defineConfig({
   plugins: [
   plugins: [
-    dts(),
+    dts({ copyDtsFiles: true }),
   ],
   ],
   build: {
   build: {
     outDir: 'dist/server',
     outDir: 'dist/server',

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/slack",
   "name": "@growi/slack",
-  "version": "6.1.7-RC.0",
+  "version": "6.1.8-RC.0",
   "license": "MIT",
   "license": "MIT",
   "main": "dist/index.js",
   "main": "dist/index.js",
   "module": "dist/index.mjs",
   "module": "dist/index.mjs",

+ 1 - 1
packages/slack/vite.config.ts

@@ -7,7 +7,7 @@ import dts from 'vite-plugin-dts';
 // https://vitejs.dev/config/
 // https://vitejs.dev/config/
 export default defineConfig({
 export default defineConfig({
   plugins: [
   plugins: [
-    dts(),
+    dts({ copyDtsFiles: true }),
   ],
   ],
   build: {
   build: {
     outDir: 'dist',
     outDir: 'dist',

+ 1 - 1
packages/ui/package.json

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

+ 1 - 1
packages/ui/vite.config.ts

@@ -9,7 +9,7 @@ import dts from 'vite-plugin-dts';
 export default defineConfig({
 export default defineConfig({
   plugins: [
   plugins: [
     react(),
     react(),
-    dts(),
+    dts({ copyDtsFiles: true }),
   ],
   ],
   build: {
   build: {
     outDir: 'dist',
     outDir: 'dist',

Разница между файлами не показана из-за своего большого размера
+ 402 - 217
yarn.lock


Некоторые файлы не были показаны из-за большого количества измененных файлов