2
0
Эх сурвалжийг харах

Merge remote-tracking branch 'origin/master' into fix/126404-manager-authority-modifying

WNomunomu 2 жил өмнө
parent
commit
450fbfe1b3
61 өөрчлөгдсөн 592 нэмэгдсэн , 810 устгасан
  1. 7 0
      .github/release-drafter-dev-6.2.x.yml
  2. 7 0
      .github/release-drafter-master.yml
  3. 2 1
      .github/workflows/auto-labeling.yml
  4. 2 0
      .github/workflows/ci-app-prod.yml
  5. 3 3
      .github/workflows/ci-app.yml
  6. 3 3
      .github/workflows/ci-slackbot-proxy.yml
  7. 11 2
      .github/workflows/draft-release.yml
  8. 1 1
      .github/workflows/release-slackbot-proxy.yml
  9. 2 2
      .github/workflows/release.yml
  10. 3 3
      .github/workflows/reusable-app-prod.yml
  11. 1 1
      .github/workflows/reusable-app-reg-suit.yml
  12. 52 1
      CHANGELOG.md
  13. 2 1
      apps/app/package.json
  14. 2 4
      apps/app/src/client/services/page-operation.ts
  15. 4 0
      apps/app/src/client/services/renderer/renderer.tsx
  16. 1 1
      apps/app/src/components/Bookmarks/BookmarkFolderTree.module.scss
  17. 1 1
      apps/app/src/components/Bookmarks/BookmarkItem.tsx
  18. 14 23
      apps/app/src/components/Comments.tsx
  19. 6 8
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  20. 4 11
      apps/app/src/components/Page/RevisionLoader.tsx
  21. 0 1
      apps/app/src/components/Page/RevisionRenderer.tsx
  22. 7 19
      apps/app/src/components/PageComment.tsx
  23. 6 1
      apps/app/src/components/PageComment/CommentControl.tsx
  24. 16 0
      apps/app/src/components/PageComment/CommentEditor.tsx
  25. 2 2
      apps/app/src/components/PageEditor.tsx
  26. 4 0
      apps/app/src/components/PageList/PageListItemS.module.scss
  27. 18 4
      apps/app/src/components/PageList/PageListItemS.tsx
  28. 16 0
      apps/app/src/components/ReactMarkdownComponents/LightBox.tsx
  29. 4 5
      apps/app/src/components/SavePageControls.tsx
  30. 4 2
      apps/app/src/components/SearchPage/SearchPageBase.tsx
  31. 50 77
      apps/app/src/components/SearchPage/SearchResultContent.tsx
  32. 2 2
      apps/app/src/components/Sidebar/SidebarNav.tsx
  33. 3 2
      apps/app/src/features/questionnaire/server/service/questionnaire.ts
  34. 5 2
      apps/app/src/pages/[[...path]].page.tsx
  35. 5 1
      apps/app/src/pages/share/[[...path]].page.tsx
  36. 4 11
      apps/app/src/pages/utils/commons.ts
  37. 38 1
      apps/app/src/server/models/page.ts
  38. 11 9
      apps/app/src/server/routes/next.ts
  39. 9 7
      apps/app/src/stores/context.tsx
  40. 5 3
      apps/app/src/stores/search.tsx
  41. 4 21
      apps/app/test/cypress/e2e/20-basic-features/20-basic-features--access-to-page.cy.ts
  42. 135 0
      apps/app/test/cypress/e2e/20-basic-features/20-basic-features--comments.cy.ts
  43. 0 61
      apps/app/test/cypress/e2e/20-basic-features/20-basic-features--username-mention.cy.ts
  44. 15 0
      apps/app/test/cypress/support/commands.ts
  45. 1 0
      apps/app/test/cypress/support/index.ts
  46. 1 1
      apps/app/test/integration/service/questionnaire.test.ts
  47. 1 1
      apps/slackbot-proxy/package.json
  48. 2 2
      package.json
  49. 1 1
      packages/core/package.json
  50. 1 1
      packages/hackmd/package.json
  51. 1 1
      packages/presentation/package.json
  52. 1 1
      packages/preset-templates/package.json
  53. 1 1
      packages/preset-themes/package.json
  54. 1 1
      packages/remark-attachment-refs/package.json
  55. 1 1
      packages/remark-drawio/package.json
  56. 1 1
      packages/remark-growi-directive/package.json
  57. 1 1
      packages/remark-lsx/package.json
  58. 1 1
      packages/slack/package.json
  59. 1 1
      packages/ui/package.json
  60. 9 9
      packages/ui/src/components/PagePath/PageListMeta.tsx
  61. 76 489
      yarn.lock

+ 7 - 0
.github/release-drafter-dev-6.2.x.yml

@@ -0,0 +1,7 @@
+_extends: growi:.github/release-drafter.yml
+
+prerelease: true
+
+# Filter previous releases to consider only those with the tags starts with 'v6.2'
+include-pre-releases: true
+tag-prefix: v6.2

+ 7 - 0
.github/release-drafter-master.yml

@@ -0,0 +1,7 @@
+_extends: growi:.github/release-drafter.yml
+
+prerelease: true
+
+# Filter previous releases to consider only those with the master branch
+include-pre-releases: true
+filter-by-commitish: true

+ 2 - 1
.github/workflows/pr-to-master.yml → .github/workflows/auto-labeling.yml

@@ -1,9 +1,10 @@
-name: PR to master
+name: Auto-labeling
 
 on:
   pull_request:
     branches:
       - master
+      - dev/*.*.*
     # Only following types are handled by the action, but one can default to all as well
     types: [opened, reopened, edited, synchronize]
 

+ 2 - 0
.github/workflows/ci-app-prod.yml

@@ -4,6 +4,7 @@ on:
   push:
     branches:
       - master
+      - dev/6.*.x
     paths:
       - .github/workflows/ci-app-prod.yml
       - .github/workflows/reusable-app-prod.yml
@@ -18,6 +19,7 @@ on:
   pull_request:
     branches:
       - master
+      - dev/6.*.x
     types: [opened, reopened, synchronize]
     paths:
       - .github/workflows/ci-app-prod.yml

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

@@ -60,7 +60,7 @@ jobs:
 
       - name: Install dependencies
         run: |
-          yarn global add turbo
+          yarn global add turbo@1.10.9
           yarn --frozen-lockfile
 
       - name: Lint
@@ -131,7 +131,7 @@ jobs:
 
       - name: Install dependencies
         run: |
-          yarn global add turbo
+          yarn global add turbo@1.10.9
           yarn --frozen-lockfile
 
       - name: Test
@@ -213,7 +213,7 @@ jobs:
 
       - name: Install dependencies
         run: |
-          yarn global add turbo
+          yarn global add turbo@1.10.9
           yarn --frozen-lockfile
 
       - name: turbo run dev:ci

+ 3 - 3
.github/workflows/ci-slackbot-proxy.yml

@@ -62,7 +62,7 @@ jobs:
 
     - name: Install dependencies
       run: |
-        yarn global add turbo
+        yarn global add turbo@1.10.9
         yarn --frozen-lockfile
 
     - name: Lint
@@ -136,7 +136,7 @@ jobs:
 
     - name: Install dependencies
       run: |
-        yarn global add turbo
+        yarn global add turbo@1.10.9
         yarn --frozen-lockfile
 
     - name: yarn dev:ci
@@ -200,7 +200,7 @@ jobs:
 
     - name: Install turbo
       run: |
-        yarn global add turbo
+        yarn global add turbo@1.10.9
 
     - name: Prune repositories
       run: |

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

@@ -4,6 +4,8 @@ on:
   push:
     branches:
       - master
+      - dev/*.*.*
+
 
 concurrency:
   group: ${{ github.workflow }}-${{ github.ref }}
@@ -27,10 +29,17 @@ jobs:
         uses: myrotvorets/info-from-package-json-action@1.2.0
         id: package-json
 
-      # Drafts your next Release notes as Pull Requests are merged into "master"
+      - name: Determine config file
+        id: determine-config-name
+        run: |
+          BRANCH_NAME="${{ github.ref_name }}"
+          BRANCH_NAME_REPLACED=${BRANCH_NAME/\//-}
+          echo "value=release-drafter-$BRANCH_NAME_REPLACED.yml" >> $GITHUB_OUTPUT
+
       - uses: release-drafter/release-drafter@v5
         id: release-drafter
         with:
+          config-name: ${{ steps.determine-config-name.outputs.value }}
           name: v${{ steps.package-json.outputs.packageVersion }}
           tag: v${{ steps.package-json.outputs.packageVersion }}
           version: ${{ steps.package-json.outputs.packageVersion }}
@@ -60,7 +69,7 @@ jobs:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           GIT_PR_RELEASE_BRANCH_PRODUCTION: release/current
-          GIT_PR_RELEASE_BRANCH_STAGING: master
+          GIT_PR_RELEASE_BRANCH_STAGING: ${{ github.ref_name }}
           GIT_PR_RELEASE_TEMPLATE: .github/git-pr-release-template.erb
           GIT_PR_RELEASE_TITLE: Release v${{ steps.release-version.outputs.RELEASE_VERSION }}
           GIT_PR_RELEASE_BODY: ${{ needs.update-release-draft.outputs.RELEASE_DRAFT_BODY }}

+ 1 - 1
.github/workflows/release-slackbot-proxy.yml

@@ -108,7 +108,7 @@ jobs:
 
     - name: Install dependencies
       run: |
-        yarn global add turbo
+        yarn global add turbo@1.10.9
         yarn --frozen-lockfile
 
     - name: Bump versions for next RC

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

@@ -30,7 +30,7 @@ jobs:
 
     - name: Install dependencies
       run: |
-        yarn global add turbo
+        yarn global add turbo@1.10.9
         yarn --frozen-lockfile
 
     - name: Bump versions
@@ -93,7 +93,7 @@ jobs:
 
     - name: Install dependencies
       run: |
-        yarn global add turbo
+        yarn global add turbo@1.10.9
         yarn --frozen-lockfile
 
     - name: Bump versions for next RC

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

@@ -36,7 +36,7 @@ jobs:
 
     - name: Install turbo
       run: |
-        yarn global add turbo
+        yarn global add turbo@1.10.9
 
     - name: Prune repositories
       run: |
@@ -147,7 +147,7 @@ jobs:
 
     - name: Install turbo
       run: |
-        yarn global add turbo
+        yarn global add turbo@1.10.9
 
     - name: Prune repositories
       run: |
@@ -238,7 +238,7 @@ jobs:
 
     - name: Install turbo
       run: |
-        yarn global add turbo
+        yarn global add turbo@1.10.9
 
     - name: Prune repositories
       run: |

+ 1 - 1
.github/workflows/reusable-app-reg-suit.yml

@@ -62,7 +62,7 @@ jobs:
 
     - name: Install turbo
       run: |
-        yarn global add turbo
+        yarn global add turbo@1.10.9
 
     - name: Prune repositories
       run: |

+ 52 - 1
CHANGELOG.md

@@ -1,9 +1,60 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.1.7...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.1.10...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v6.1.10](https://github.com/weseek/growi/compare/v6.1.9...v6.1.10) - 2023-08-01
+
+### 🐛 Bug Fixes
+
+- fix: Unsaved comments remain in the editor when transitioning to a new page  (#7912) @miya
+- fix: SWR settings for searching (#7940) @yuki-takei
+- fix: Auto-scroll search result content (#7935) @yuki-takei
+
+## [v6.1.9](https://github.com/weseek/growi/compare/v6.1.8...v6.1.9) - 2023-07-31
+
+### 💎 Features
+
+- feat: LightBox for enlargement of image (#7899) @WNomunomu
+
+### 🚀 Improvement
+
+- imprv: Do not use loadConfigs in skipSSR (#7929) @jam411
+- imprv: Improve default behavior of skipSSR (#7927) @jam411
+- imprv: Improved Design for Bookmarks Sidebar (#7886) @soumaeda
+
+### 🐛 Bug Fixes
+
+- fix: Page creation and update process (#7925) @yuki-takei
+- fix: Sidebar doesn't show the link to the administration panel when logged in as an Admin (#7914) @miya
+- fix: Improve page data mutation after renaming and deleting by GrowiContextualSubNavigation (#7926) @yuki-takei
+- fix: Revert dynamic import. (#7923) @TatsuyaIse
+- fix: Questionnaire wikiType (#7907) @TatsuyaIse
+
+## [v6.1.8](https://github.com/weseek/growi/compare/v6.1.7...v6.1.8) - 2023-07-24
+
+### 💎 Features
+
+- feat: Add plugin badge to TemplateModal's list of templates (#7897) @TatsuyaIse
+
+### 🚀 Improvement
+
+- imprv: Replace isAdmin with usersAdminHooks (#7840) @WNomunomu
+- imprv: Show alert for trashed pages only when the page is not empty (#7903) @TatsuyaIse
+- imprv: Template name truncation (#7898) @TatsuyaIse
+
+### 🐛 Bug Fixes
+
+- fix: Cancel a comment will cancel all comments (#7804) @mudana-grune
+
+### 🧰 Maintenance
+
+- ci(deps-dev): bump vite from 4.3.8 to 4.4.5 (#7901) @dependabot
+- ci(deps): bump semver from 5.7.1 to 5.7.2 (#7867) @dependabot
+- ci(deps): bump mongoose from 6.11.1 to 6.11.3 (#7891) @dependabot
+- ci(deps): bump word-wrap from 1.2.3 to 1.2.4 (#7892) @dependabot
+
 ## [v6.1.7](https://github.com/weseek/growi/compare/v6.1.6...v6.1.7) - 2023-07-19
 
 ### 💎 Features

+ 2 - 1
apps/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "6.1.8-RC.0",
+  "version": "6.1.11-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -235,6 +235,7 @@
     "eslint-plugin-jest": "^26.5.3",
     "eslint-plugin-regex": "^1.8.0",
     "font-awesome": "^4.7.0",
+    "fslightbox-react": "^1.7.6",
     "handsontable": "=6.2.2",
     "i18next-hmr": "^1.11.0",
     "jest": "^29.5.0",

+ 2 - 4
apps/app/src/client/services/page-operation.ts

@@ -1,6 +1,6 @@
 import { useCallback } from 'react';
 
-import { SubscriptionStatusType, Nullable } from '@growi/core';
+import { SubscriptionStatusType, type Nullable } from '@growi/core';
 import urljoin from 'url-join';
 
 import { OptionsToSave } from '~/interfaces/page-operation';
@@ -153,10 +153,8 @@ export const useSaveOrUpdate = (): SaveOrUpdateFunction => {
     // markdown = pageEditor.getMarkdown();
     // }
 
-    const isNoRevisionPage = pageId != null && revisionId == null;
-
     let res;
-    if (pageId == null || isNoRevisionPage) {
+    if (pageId == null || revisionId == null) {
       res = await createPage(path, markdown, options);
     }
     else {

+ 4 - 0
apps/app/src/client/services/renderer/renderer.tsx

@@ -16,6 +16,7 @@ import type { Pluggable } from 'unified';
 
 import { DrawioViewerWithEditButton } from '~/components/ReactMarkdownComponents/DrawioViewerWithEditButton';
 import { Header } from '~/components/ReactMarkdownComponents/Header';
+import { LightBox } from '~/components/ReactMarkdownComponents/LightBox';
 import { RichAttachment } from '~/components/ReactMarkdownComponents/RichAttachment';
 import { TableWithEditButton } from '~/components/ReactMarkdownComponents/TableWithEditButton';
 import * as mermaid from '~/features/mermaid';
@@ -113,6 +114,7 @@ export const generateViewOptions = (
     components.table = TableWithEditButton;
     components.mermaid = mermaid.MermaidViewer;
     components.attachment = RichAttachment;
+    components.img = LightBox;
   }
 
   if (config.isEnabledXssPrevention) {
@@ -218,6 +220,7 @@ export const generateSimpleViewOptions = (
     components.drawio = drawio.DrawioViewer;
     components.mermaid = mermaid.MermaidViewer;
     components.attachment = RichAttachment;
+    components.img = LightBox;
   }
 
   if (config.isEnabledXssPrevention) {
@@ -295,6 +298,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
     components.drawio = drawio.DrawioViewer;
     components.mermaid = mermaid.MermaidViewer;
     components.attachment = RichAttachment;
+    components.img = LightBox;
   }
 
   if (config.isEnabledXssPrevention) {

+ 1 - 1
apps/app/src/components/Bookmarks/BookmarkFolderTree.module.scss

@@ -64,7 +64,7 @@ $grw-bookmark-item-padding-left: 35px;
 
     .grw-bookmark-item-list{
       min-width: 30px;
-      height: 35px;
+      height: 50px;
 
       .picture {
         width: 16px;

+ 1 - 1
apps/app/src/components/Bookmarks/BookmarkItem.tsx

@@ -161,7 +161,7 @@ export const BookmarkItem = (props: Props): JSX.Element => {
               validationTarget={ValidationTarget.PAGE}
             />
           )
-          : <PageListItemS page={bookmarkedPage} pageTitle={pageTitle} />}
+          : <PageListItemS page={bookmarkedPage} pageTitle={pageTitle} isNarrowView />}
 
         <div className='grw-foldertree-control'>
           <PageItemControl

+ 14 - 23
apps/app/src/components/Comments.tsx

@@ -1,9 +1,10 @@
-import React, { useEffect, useRef } from 'react';
+import React, { useEffect, useMemo, useRef } from 'react';
 
 import { type IRevisionHasId, pagePathUtils } from '@growi/core';
 import dynamic from 'next/dynamic';
+import { debounce } from 'throttle-debounce';
 
-import { ROOT_ELEM_ID as PageCommentRootElemId, type PageCommentProps } from '~/components/PageComment';
+import { type PageCommentProps } from '~/components/PageComment';
 import { useSWRxPageComment } from '~/stores/comment';
 import { useIsTrashPage, useSWRMUTxPageInfo } from '~/stores/page';
 
@@ -38,30 +39,21 @@ export const Comments = (props: CommentsProps): JSX.Element => {
 
   const pageCommentParentRef = useRef<HTMLDivElement>(null);
 
+  const onLoadedDebounced = useMemo(() => debounce(500, () => onLoaded?.()), [onLoaded]);
+
   useEffect(() => {
     const parent = pageCommentParentRef.current;
     if (parent == null) return;
 
-    const observerCallback = (mutationRecords: MutationRecord[]) => {
-      mutationRecords.forEach((record: MutationRecord) => {
-        const target = record.target as HTMLElement;
-
-        for (const child of Array.from(target.children)) {
-          const childId = (child as HTMLElement).id;
-          if (childId === PageCommentRootElemId) {
-            onLoaded?.();
-            break;
-          }
-        }
-
-      });
-    };
-
-    const observer = new MutationObserver(observerCallback);
-    observer.observe(parent, { childList: true });
-    return () => {
-      observer.disconnect();
-    };
+    const observer = new MutationObserver(() => {
+      onLoadedDebounced();
+    });
+    observer.observe(parent, { childList: true, subtree: true });
+
+    // no cleanup function -- 2023.07.31 Yuki Takei
+    // see: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe
+    // > You can call observe() multiple times on the same MutationObserver
+    // > to watch for changes to different parts of the DOM tree and/or different types of changes.
   }, [onLoaded]);
 
   const isTopPagePath = isTopPage(pagePath);
@@ -86,7 +78,6 @@ export const Comments = (props: CommentsProps): JSX.Element => {
             currentUser={currentUser}
             isReadOnly={false}
             titleAlign="left"
-            hideIfEmpty={false}
           />
         </div>
         {!isDeleted && (

+ 6 - 8
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -27,6 +27,7 @@ import {
 import {
   useSWRMUTxCurrentPage, useSWRxTagsInfo, useCurrentPageId, useIsNotFound, useTemplateTagData, useSWRxPageInfo,
 } from '~/stores/page';
+import { mutatePageTree } from '~/stores/page-listing';
 import {
   EditorMode, useDrawerMode, useEditorMode, useIsAbleToShowPageManagement, useIsAbleToShowTagLabel,
   useIsAbleToChangeEditorMode, useIsAbleToShowPageAuthors,
@@ -283,12 +284,6 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
     return;
   }, [mutatePageTagsForEditors]);
 
-  const reload = useCallback(() => {
-    if (currentPathname != null) {
-      router.push(currentPathname);
-    }
-  }, [currentPathname, router]);
-
   const duplicateItemClickedHandler = useCallback(async(page: IPageForPageDuplicateModal) => {
     const duplicatedHandler: OnDuplicatedFunction = (fromPath, toPath) => {
       router.push(toPath);
@@ -298,10 +293,12 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
   const renameItemClickedHandler = useCallback(async(page: IPageToRenameWithMeta<IPageInfoForEntity>) => {
     const renamedHandler: OnRenamedFunction = () => {
-      reload();
+      mutateCurrentPage();
+      mutatePageInfo();
+      mutatePageTree();
     };
     openRenameModal(page, { onRenamed: renamedHandler });
-  }, [openRenameModal, reload]);
+  }, [mutateCurrentPage, mutatePageInfo, openRenameModal]);
 
   const deleteItemClickedHandler = useCallback((pageWithMeta: IPageWithMeta) => {
     const deletedHandler: OnDeletedFunction = (pathOrPathsToDelete, isRecursively, isCompletely) => {
@@ -321,6 +318,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
       mutateCurrentPage();
       mutatePageInfo();
+      mutatePageTree();
     };
     openDeleteModal([pageWithMeta], { onDeleted: deletedHandler });
   }, [currentPathname, mutateCurrentPage, openDeleteModal, router, mutatePageInfo]);

+ 4 - 11
apps/app/src/components/Page/RevisionLoader.tsx

@@ -20,11 +20,6 @@ export type RevisionLoaderProps = {
 
 const logger = loggerFactory('growi:Page:RevisionLoader');
 
-// Always render '#revision-loader' for MutationObserver of SearchResultContent
-const RevisionLoaderRoot = (props: React.HTMLAttributes<HTMLDivElement>): JSX.Element => (
-  <div id={ROOT_ELEM_ID} {...props}>{props.children}</div>
-);
-
 /**
  * Load data from server and render RevisionBody component
  */
@@ -76,11 +71,9 @@ export const RevisionLoader = (props: RevisionLoaderProps): JSX.Element => {
   }
 
   return (
-    <RevisionLoaderRoot>
-      <RevisionRenderer
-        rendererOptions={rendererOptions}
-        markdown={markdown}
-      />
-    </RevisionLoaderRoot>
+    <RevisionRenderer
+      rendererOptions={rendererOptions}
+      markdown={markdown}
+    />
   );
 };

+ 0 - 1
apps/app/src/components/Page/RevisionRenderer.tsx

@@ -8,7 +8,6 @@ import loggerFactory from '~/utils/logger';
 
 import 'katex/dist/katex.min.css';
 
-
 const logger = loggerFactory('components:Page:RevisionRenderer');
 
 type Props = {

+ 7 - 19
apps/app/src/components/PageComment.tsx

@@ -24,13 +24,6 @@ import { ReplyComments } from './PageComment/ReplyComments';
 
 import styles from './PageComment.module.scss';
 
-export const ROOT_ELEM_ID = 'page-comments' as const;
-
-// Always render '#page-comments' for MutationObserver of SearchResultContent
-const PageCommentRoot = (props: React.HTMLAttributes<HTMLDivElement>): JSX.Element => (
-  <div id={ROOT_ELEM_ID} {...props}>{props.children}</div>
-);
-
 
 export type PageCommentProps = {
   rendererOptions?: RendererOptions,
@@ -40,14 +33,13 @@ export type PageCommentProps = {
   currentUser: any,
   isReadOnly: boolean,
   titleAlign?: 'center' | 'left' | 'right',
-  hideIfEmpty?: boolean,
 }
 
 export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps): JSX.Element => {
 
   const {
     rendererOptions: rendererOptionsByProps,
-    pageId, pagePath, revision, currentUser, isReadOnly, titleAlign, hideIfEmpty,
+    pageId, pagePath, revision, currentUser, isReadOnly, titleAlign,
   } = props;
 
   const { data: comments, mutate } = useSWRxPageComment(pageId);
@@ -117,8 +109,8 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
     mutatePageInfo();
   }, [removeShowEditorId, mutate, mutatePageInfo]);
 
-  if (hideIfEmpty && comments?.length === 0) {
-    return <PageCommentRoot />;
+  if (comments?.length === 0) {
+    return <></>;
   }
 
   let commentTitleClasses = 'border-bottom py-3 mb-3';
@@ -127,12 +119,7 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
   const rendererOptions = rendererOptionsByProps ?? rendererOptionsForCurrentPage;
 
   if (commentsFromOldest == null || commentsExceptReply == null || rendererOptions == null) {
-    if (hideIfEmpty) {
-      return <PageCommentRoot />;
-    }
-    return (
-      <></>
-    );
+    return <></>;
   }
 
   const revisionId = getIdForRef(revision);
@@ -169,7 +156,7 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
   );
 
   return (
-    <PageCommentRoot className={`${styles['page-comment-styles']} page-comments-row comment-list`}>
+    <div className={`${styles['page-comment-styles']} page-comments-row comment-list`}>
       <div className="container-lg">
         <div className="page-comments">
           <h2 className={commentTitleClasses}><i className="icon-fw icon-bubbles"></i>Comments</h2>
@@ -191,6 +178,7 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
                       <NotAvailableForGuest>
                         <NotAvailableForReadOnlyUser>
                           <Button
+                            data-testid="comment-reply-button"
                             outline
                             color="secondary"
                             size="sm"
@@ -230,7 +218,7 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
           confirmToDelete={onDeleteComment}
         />
       )}
-    </PageCommentRoot>
+    </div>
   );
 });
 

+ 6 - 1
apps/app/src/components/PageComment/CommentControl.tsx

@@ -16,7 +16,12 @@ export const CommentControl = (props: CommentControlProps): JSX.Element => {
       <button type="button" className="btn btn-link p-2" onClick={onClickEditBtn}>
         <i className="ti ti-pencil"></i>
       </button>
-      <button type="button" className="btn btn-link p-2 mr-2" onClick={onClickDeleteBtn}>
+      <button
+        data-testid="comment-delete-button"
+        type="button"
+        className="btn btn-link p-2 mr-2"
+        onClick={onClickDeleteBtn}
+      >
         <i className="ti ti-close"></i>
       </button>
     </div>

+ 16 - 0
apps/app/src/components/PageComment/CommentEditor.tsx

@@ -2,6 +2,7 @@ import React, {
   useCallback, useState, useRef, useEffect,
 } from 'react';
 
+import { useRouter } from 'next/router';
 import { UserPicture } from '@growi/ui/dist/components/User/UserPicture';
 import dynamic from 'next/dynamic';
 import {
@@ -85,6 +86,20 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
 
   const editorRef = useRef<IEditorMethods>(null);
 
+  const router = useRouter();
+
+  // UnControlled CodeMirror value is not reset on page transition, so explicitly set the value to the initial value
+  const onRouterChangeComplete = useCallback(() => {
+    editorRef.current?.setValue('');
+  }, []);
+
+  useEffect(() => {
+    router.events.on('routeChangeComplete', onRouterChangeComplete);
+    return () => {
+      router.events.off('routeChangeComplete', onRouterChangeComplete);
+    };
+  }, [onRouterChangeComplete, router.events]);
+
   const handleSelect = useCallback((activeTab: string) => {
     setActiveTab(activeTab);
   }, []);
@@ -279,6 +294,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     );
     const submitButton = (
       <Button
+        data-testid="comment-submit-button"
         outline
         color="primary"
         className="btn btn-outline-primary rounded-pill"

+ 2 - 2
apps/app/src/components/PageEditor.tsx

@@ -112,7 +112,7 @@ const PageEditor = React.memo((): JSX.Element => {
 
   // TODO: remove workaround
   // for https://redmine.weseek.co.jp/issues/125923
-  const [createdPageRevisionIdWithAttachment, setCreatedPageRevisionIdWithAttachment] = useState('');
+  const [createdPageRevisionIdWithAttachment, setCreatedPageRevisionIdWithAttachment] = useState();
 
   // TODO: remove workaround
   // for https://redmine.weseek.co.jp/issues/125923
@@ -158,7 +158,7 @@ const PageEditor = React.memo((): JSX.Element => {
   // TODO: remove workaround
   // for https://redmine.weseek.co.jp/issues/125923
   useEffect(() => {
-    setCreatedPageRevisionIdWithAttachment('');
+    setCreatedPageRevisionIdWithAttachment(undefined);
   }, [router]);
 
   useEffect(() => {

+ 4 - 0
apps/app/src/components/PageList/PageListItemS.module.scss

@@ -0,0 +1,4 @@
+.page-title {
+  flex: 1;
+  line-height: 1.2;
+}

+ 18 - 4
apps/app/src/components/PageList/PageListItemS.tsx

@@ -4,20 +4,26 @@ import { PageListMeta } from '@growi/ui/dist/components/PagePath/PageListMeta';
 import { PagePathLabel } from '@growi/ui/dist/components/PagePath/PagePathLabel';
 import { UserPicture } from '@growi/ui/dist/components/User/UserPicture';
 import Link from 'next/link';
+import Clamp from 'react-multiline-clamp';
 
 import { IPageHasId } from '~/interfaces/page';
 
+import styles from './PageListItemS.module.scss';
 
 type PageListItemSProps = {
   page: IPageHasId,
   noLink?: boolean,
-  pageTitle?: string,
+  pageTitle?: string
+  isNarrowView?: boolean,
 }
 
 export const PageListItemS = (props: PageListItemSProps): JSX.Element => {
 
   const {
-    page, noLink = false, pageTitle,
+    page,
+    noLink = false,
+    pageTitle,
+    isNarrowView = false,
   } = props;
 
   const path = pageTitle != null ? pageTitle : page.path;
@@ -30,9 +36,17 @@ export const PageListItemS = (props: PageListItemSProps): JSX.Element => {
   return (
     <>
       <UserPicture user={page.lastUpdateUser} noLink={noLink} />
-      {pagePathElement}
+      {isNarrowView ? (
+        <Clamp lines={2}>
+          <div className={`mx-2 ${styles['page-title']} ${noLink ? 'text-break' : ''}`}>
+            {pagePathElement}
+          </div>
+        </Clamp>
+      ) : (
+        pagePathElement
+      )}
       <span className="ml-2">
-        <PageListMeta page={page} />
+        <PageListMeta page={page} shouldSpaceOutIcon />
       </span>
     </>
   );

+ 16 - 0
apps/app/src/components/ReactMarkdownComponents/LightBox.tsx

@@ -0,0 +1,16 @@
+import React, { useState } from 'react';
+
+import FsLightbox from 'fslightbox-react';
+
+export const LightBox = (props) => {
+  const [toggler, setToggler] = useState(false);
+  return (
+    <>
+      <img src={props.src} alt={props.alt} onClick={() => setToggler(!toggler)}/>
+      <FsLightbox
+        toggler={toggler}
+        sources={[props.src]}
+      />
+    </>
+  );
+};

+ 4 - 5
apps/app/src/components/SavePageControls.tsx

@@ -14,7 +14,7 @@ import {
   useIsEditable, useIsAclEnabled,
 } from '~/stores/context';
 import { useWaitingSaveProcessing } from '~/stores/editor';
-import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
+import { useSWRxCurrentPage } from '~/stores/page';
 import { useSelectedGrant } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 
@@ -38,11 +38,10 @@ export type SavePageControlsProps = {
 export const SavePageControls = (props: SavePageControlsProps): JSX.Element | null => {
   const { slackChannels } = props;
   const { t } = useTranslation();
-  const { data: currentPagePath } = useCurrentPagePath();
+  const { data: currentPage } = useSWRxCurrentPage();
   const { data: isEditable } = useIsEditable();
   const { data: isAclEnabled } = useIsAclEnabled();
   const { data: grantData, mutate: mutateGrant } = useSelectedGrant();
-  const { data: pageId } = useCurrentPageId();
   const { data: _isWaitingSaveProcessing } = useWaitingSaveProcessing();
 
   const isWaitingSaveProcessing = _isWaitingSaveProcessing === true; // ignore undefined
@@ -72,8 +71,8 @@ export const SavePageControls = (props: SavePageControlsProps): JSX.Element | nu
 
   const { grant, grantedGroup } = grantData;
 
-  const isRootPage = isTopPage(currentPagePath ?? '');
-  const labelSubmitButton = pageId == null ? t('Create') : t('Update');
+  const isRootPage = isTopPage(currentPage?.path ?? '');
+  const labelSubmitButton = (currentPage != null && !currentPage.isEmpty) ? t('Update') : t('Create');
   const labelOverwriteScopes = t('page_edit.overwrite_scopes', { operation: labelSubmitButton });
 
   return (

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

@@ -17,6 +17,10 @@ import { mutatePageTree } from '~/stores/page-listing';
 
 import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 
+// Do not import with next/dynamic
+// see: https://github.com/weseek/growi/pull/7923
+import { SearchResultList } from './SearchResultList';
+
 import styles from './SearchPageBase.module.scss';
 
 // https://regex101.com/r/brrkBu/1
@@ -41,8 +45,6 @@ type Props = {
   searchPager: React.ReactNode,
 }
 
-
-const SearchResultList = dynamic(() => import('./SearchResultList').then(mod => mod.SearchResultList), { ssr: false });
 const SearchResultContent = dynamic(() => import('./SearchResultContent').then(mod => mod.SearchResultContent), {
   ssr: false,
   loading: () => <></>,

+ 50 - 77
apps/app/src/components/SearchPage/SearchResultContent.tsx

@@ -7,7 +7,7 @@ import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import { animateScroll } from 'react-scroll';
 import { DropdownItem } from 'reactstrap';
-
+import { debounce } from 'throttle-debounce';
 
 import { exportAsMarkdown, updateContentWidth } from '~/client/services/page-operation';
 import { toastSuccess } from '~/client/util/toastr';
@@ -25,8 +25,8 @@ import { mutateSearching } from '~/stores/search';
 import type { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import type { GrowiSubNavigationProps } from '../Navbar/GrowiSubNavigation';
 import type { SubNavButtonsProps } from '../Navbar/SubNavButtons';
-import { ROOT_ELEM_ID as RevisionLoaderRoomElemId, type RevisionLoaderProps } from '../Page/RevisionLoader';
-import { ROOT_ELEM_ID as PageCommentRootElemId, type PageCommentProps } from '../PageComment';
+import { type RevisionLoaderProps } from '../Page/RevisionLoader';
+import { type PageCommentProps } from '../PageComment';
 import type { PageContentFooterProps } from '../PageContentFooter';
 
 import styles from './SearchResultContent.module.scss';
@@ -61,7 +61,7 @@ const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
 };
 
 const SCROLL_OFFSET_TOP = 30;
-const MUTATION_OBSERVER_CONFIG = { childList: true }; // omit 'subtree: true'
+const MUTATION_OBSERVER_CONFIG = { childList: true, subtree: true }; // omit 'subtree: true'
 
 type Props ={
   pageWithMeta : IPageWithSearchMeta,
@@ -70,72 +70,41 @@ type Props ={
   forceHideMenuItems?: ForceHideMenuItems,
 }
 
-const scrollToFirstHighlightedKeyword = (scrollElement: HTMLElement): boolean => {
+const scrollToFirstHighlightedKeyword = (scrollElement: HTMLElement): void => {
   // use querySelector to intentionally get the first element found
   const toElem = scrollElement.querySelector('.highlighted-keyword') as HTMLElement | null;
   if (toElem == null) {
-    return false;
+    return;
   }
 
-  animateScroll.scrollTo(toElem.offsetTop - SCROLL_OFFSET_TOP, {
+  const distance = toElem.getBoundingClientRect().top - scrollElement.getBoundingClientRect().top - SCROLL_OFFSET_TOP;
+  animateScroll.scrollMore(distance, {
     containerId: scrollElement.id,
     duration: 200,
   });
-  return true;
 };
+const scrollToFirstHighlightedKeywordDebounced = debounce(500, scrollToFirstHighlightedKeyword);
 
 export const SearchResultContent: FC<Props> = (props: Props) => {
 
   const scrollElementRef = useRef<HTMLDivElement|null>(null);
 
-  const [isRevisionLoaded, setRevisionLoaded] = useState(false);
-  const [isPageCommentLoaded, setPageCommentLoaded] = useState(false);
-
   // ***************************  Auto Scroll  ***************************
   useEffect(() => {
     const scrollElement = scrollElementRef.current;
-    if (scrollElement == null) return;
 
-    const observerCallback = (mutationRecords:MutationRecord[]) => {
-      mutationRecords.forEach((record:MutationRecord) => {
-        const target = record.target as HTMLElement;
-
-        // turn on boolean if loaded
-        Array.from(target.children).forEach((child) => {
-          const childId = (child as HTMLElement).id;
-          if (childId === RevisionLoaderRoomElemId) {
-            setRevisionLoaded(true);
-          }
-          else if (childId === PageCommentRootElemId) {
-            setPageCommentLoaded(true);
-          }
-        });
-      });
-    };
+    if (scrollElement == null) return;
 
-    const observer = new MutationObserver(observerCallback);
+    const observer = new MutationObserver(() => {
+      scrollToFirstHighlightedKeywordDebounced(scrollElement);
+    });
     observer.observe(scrollElement, MUTATION_OBSERVER_CONFIG);
-    return () => {
-      observer.disconnect();
-    };
-  }, []);
-
-  useEffect(() => {
-    if (!isRevisionLoaded || !isPageCommentLoaded) {
-      return;
-    }
-    if (scrollElementRef.current == null) {
-      return;
-    }
 
-    const scrollElement = scrollElementRef.current;
-    const isScrollProcessed = scrollToFirstHighlightedKeyword(scrollElement);
-    // retry after 1000ms if highlighted element is absense
-    if (!isScrollProcessed) {
-      setTimeout(() => scrollToFirstHighlightedKeyword(scrollElement), 1000);
-    }
-
-  }, [isPageCommentLoaded, isRevisionLoaded]);
+    // no cleanup function -- 2023.07.31 Yuki Takei
+    // see: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe
+    // > You can call observe() multiple times on the same MutationObserver
+    // > to watch for changes to different parts of the DOM tree and/or different types of changes.
+  });
   // *******************************  end  *******************************
 
   const {
@@ -234,40 +203,44 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
   }, [page, isExpandContentWidth, showPageControlDropdown, forceHideMenuItems, isContainerFluid,
       duplicateItemClickedHandler, renameItemClickedHandler, deleteItemClickedHandler, switchContentWidthHandler]);
 
-  // return if page or growiRenderer is null
-  if (page == null || rendererOptions == null) return <></>;
+  const isRenderable = page != null && rendererOptions != null;
 
   return (
     <div key={page._id} data-testid="search-result-content" className={`search-result-content ${styles['search-result-content']} d-flex flex-column`}>
       <div className="grw-page-path-text-muted-container">
-        <GrowiSubNavigation
-          pagePath={page.path}
-          pageId={page._id}
-          rightComponent={RightComponent}
-          isCompactMode
-          additionalClasses={['px-4']}
-        />
+        { isRenderable && (
+          <GrowiSubNavigation
+            pagePath={page.path}
+            pageId={page._id}
+            rightComponent={RightComponent}
+            isCompactMode
+            additionalClasses={['px-4']}
+          />
+        ) }
       </div>
       <div id="search-result-content-body-container" className="search-result-content-body-container" ref={scrollElementRef}>
-        {/* RevisionLoader will render '#revision-loader' after loaded */}
-        <RevisionLoader
-          rendererOptions={rendererOptions}
-          pageId={page._id}
-          revisionId={page.revision}
-        />
-        {/* PageComment will render '#page-comment' after loaded */}
-        <PageComment
-          rendererOptions={rendererOptions}
-          pageId={page._id}
-          pagePath={page.path}
-          revision={page.revision}
-          currentUser={currentUser}
-          isReadOnly
-          hideIfEmpty
-        />
-        <PageContentFooter
-          page={page}
-        />
+        { isRenderable && (
+          <RevisionLoader
+            rendererOptions={rendererOptions}
+            pageId={page._id}
+            revisionId={page.revision}
+          />
+        )}
+        { isRenderable && (
+          <PageComment
+            rendererOptions={rendererOptions}
+            pageId={page._id}
+            pagePath={page.path}
+            revision={page.revision}
+            currentUser={currentUser}
+            isReadOnly
+          />
+        )}
+        { isRenderable && (
+          <PageContentFooter
+            page={page}
+          />
+        )}
       </div>
     </div>
   );

+ 2 - 2
apps/app/src/components/Sidebar/SidebarNav.tsx

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

+ 3 - 2
apps/app/src/features/questionnaire/server/service/questionnaire.ts

@@ -3,6 +3,7 @@ import * as os from 'node:os';
 
 import { IUserHasId } from '~/interfaces/user';
 import { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
+import { aclService } from '~/server/service/acl';
 
 import {
   GrowiWikiType, GrowiExternalAuthProviderType, IGrowiInfo, GrowiServiceType, GrowiAttachmentType, GrowiDeploymentType,
@@ -33,8 +34,8 @@ class QuestionnaireService {
     const currentUsersCount = await User.countDocuments();
     const currentActiveUsersCount = await User.countActiveUsers();
 
-    const wikiMode = this.crowi.configManager.getConfig('crowi', 'security:wikiMode');
-    const wikiType = wikiMode === 'private' ? GrowiWikiType.closed : GrowiWikiType.open;
+    const isGuestAllowedToRead = aclService.isGuestAllowedToRead();
+    const wikiType = isGuestAllowedToRead ? GrowiWikiType.open : GrowiWikiType.closed;
 
     const activeExternalAccountTypes: GrowiExternalAuthProviderType[] = Object.values(GrowiExternalAuthProviderType).filter((type) => {
       return this.crowi.configManager.getConfig('crowi', `security:passport-${type}:isEnabled`);

+ 5 - 2
apps/app/src/pages/[[...path]].page.tsx

@@ -169,6 +169,7 @@ type Props = CommonProps & {
   isIndentSizeForced: boolean,
   disableLinkSharing: boolean,
   skipSSR: boolean,
+  ssrMaxRevisionBodyLength: number,
 
   grantData?: IPageGrantData,
 
@@ -437,7 +438,7 @@ async function injectPageData(context: GetServerSidePropsContext, props: Props):
 
   const Page = crowi.model('Page') as PageModel;
   const PageRedirect = mongooseModel('PageRedirect') as PageRedirectModel;
-  const { pageService } = crowi;
+  const { pageService, configManager } = crowi;
 
   let currentPathname = props.currentPathname;
 
@@ -476,7 +477,8 @@ async function injectPageData(context: GetServerSidePropsContext, props: Props):
   if (page != null) {
     page.initLatestRevisionField(revisionId);
     props.isLatestRevision = page.isLatestRevision();
-    props.skipSSR = await skipSSR(page);
+    const ssrMaxRevisionBodyLength = configManager.getConfig('crowi', 'app:ssrMaxRevisionBodyLength');
+    props.skipSSR = await skipSSR(page, ssrMaxRevisionBodyLength);
     await page.populateDataToShowRevision(props.skipSSR); // shouldExcludeBody = skipSSR
   }
 
@@ -602,6 +604,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
     highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
   };
 
+  props.ssrMaxRevisionBodyLength = configManager.getConfig('crowi', 'app:ssrMaxRevisionBodyLength');
 }
 
 /**

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

@@ -44,6 +44,7 @@ type Props = CommonProps & {
   drawioUri: string | null,
   rendererConfig: RendererConfig,
   skipSSR: boolean,
+  ssrMaxRevisionBodyLength: number,
 };
 
 type IShareLinkRelatedPage = IPagePopulatedToShowRevision & PageDocument;
@@ -178,6 +179,8 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
     tagWhitelist: crowi.configManager.getConfig('markdown', 'markdown:rehypeSanitize:tagNames'),
     highlightJsStyleBorder: configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
   };
+
+  props.ssrMaxRevisionBodyLength = configManager.getConfig('crowi', 'app:ssrMaxRevisionBodyLength');
 }
 
 async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
@@ -234,7 +237,8 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
     }
     else {
       props.isNotFound = false;
-      props.skipSSR = await skipSSR(shareLink.relatedPage);
+      const ssrMaxRevisionBodyLength = crowi.configManager.getConfig('crowi', 'app:ssrMaxRevisionBodyLength');
+      props.skipSSR = await skipSSR(shareLink.relatedPage, ssrMaxRevisionBodyLength);
       props.shareLinkRelatedPage = await shareLink.relatedPage.populateDataToShowRevision(props.skipSSR); // shouldExcludeBody = skipSSR
       props.isExpired = shareLink.isExpired();
       props.shareLink = shareLink.toObject();

+ 4 - 11
apps/app/src/pages/utils/commons.ts

@@ -172,23 +172,16 @@ export const useInitSidebarConfig = (sidebarConfig: ISidebarConfig, userUISettin
   useCurrentProductNavWidth(userUISettings?.currentProductNavWidth);
 };
 
-
-export const skipSSR = async(page: PageDocument): Promise<boolean> => {
+export const skipSSR = async(page: PageDocument, ssrMaxRevisionBodyLength: number): Promise<boolean> => {
   if (!isServer()) {
     throw new Error('This method is not available on the client-side');
   }
 
-  // page document only stores the bodyLength of the latest revision
-  if (!page.isLatestRevision() || page.latestRevisionBodyLength == null) {
-    return true;
-  }
+  const latestRevisionBodyLength = await page.getLatestRevisionBodyLength();
 
-  const { configManager } = await import('~/server/service/config-manager');
-  await configManager.loadConfigs();
-  const ssrMaxRevisionBodyLength = configManager.getConfig('crowi', 'app:ssrMaxRevisionBodyLength');
-  if (ssrMaxRevisionBodyLength < page.latestRevisionBodyLength) {
+  if (latestRevisionBodyLength == null) {
     return true;
   }
 
-  return false;
+  return ssrMaxRevisionBodyLength < latestRevisionBodyLength;
 };

+ 38 - 1
apps/app/src/server/models/page.ts

@@ -1,8 +1,11 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
 
+import assert from 'assert';
 import nodePath from 'path';
 
-import { HasObjectId, pagePathUtils, pathUtils } from '@growi/core';
+import {
+  HasObjectId, isPopulated, pagePathUtils, pathUtils,
+} from '@growi/core';
 import { collectAncestorPaths } from '@growi/core/dist/utils/page-path-utils/collect-ancestor-paths';
 import escapeStringRegexp from 'escape-string-regexp';
 import mongoose, {
@@ -40,6 +43,8 @@ const STATUS_DELETED = 'deleted';
 
 export interface PageDocument extends IPage, Document {
   [x:string]: any // for obsolete methods
+  getLatestRevisionBodyLength(): Promise<number | null | undefined>
+  calculateAndUpdateLatestRevisionBodyLength(this: PageDocument): Promise<void>
 }
 
 
@@ -959,6 +964,38 @@ schema.statics.findNonEmptyClosestAncestor = async function(path: string): Promi
   return ancestors[0];
 };
 
+/*
+ * get latest revision body length
+ */
+schema.methods.getLatestRevisionBodyLength = async function(this: PageDocument): Promise<number | null | undefined> {
+  if (!this.isLatestRevision() || this.revision == null) {
+    return null;
+  }
+
+  if (this.latestRevisionBodyLength == null) {
+    await this.calculateAndUpdateLatestRevisionBodyLength();
+  }
+
+  return this.latestRevisionBodyLength;
+};
+
+/*
+ * calculate and update latestRevisionBodyLength
+ */
+schema.methods.calculateAndUpdateLatestRevisionBodyLength = async function(this: PageDocument): Promise<void> {
+  if (!this.isLatestRevision() || this.revision == null) {
+    logger.error('revision field is required.');
+    return;
+  }
+
+  // eslint-disable-next-line rulesdir/no-populate
+  const populatedPageDocument = await this.populate<PageDocument>('revision', 'body');
+
+  assert(isPopulated(populatedPageDocument.revision));
+
+  this.latestRevisionBodyLength = populatedPageDocument.revision.body.length;
+  await this.save();
+};
 
 export type PageCreateOptions = {
   format?: string

+ 11 - 9
apps/app/src/server/routes/next.ts

@@ -1,23 +1,25 @@
-import {
-  Request, Response,
-} from 'express';
+import type { IncomingMessage } from 'http';
+
+import type { NextServer, RequestHandler } from 'next/dist/server/next';
 
 type Crowi = {
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  nextApp: any,
+  nextApp: NextServer,
 }
 
-type CrowiReq = Request & {
+type CrowiReq = IncomingMessage & {
   crowi: Crowi,
 }
 
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-const delegator = (crowi: Crowi) => {
+type NextDelegatorResult = {
+  delegateToNext: RequestHandler,
+};
+
+const delegator = (crowi: Crowi): NextDelegatorResult => {
 
   const { nextApp } = crowi;
   const handle = nextApp.getRequestHandler();
 
-  const delegateToNext = (req: CrowiReq, res: Response): void => {
+  const delegateToNext: RequestHandler = (req: CrowiReq, res): Promise<void> => {
     req.crowi = crowi;
     return handle(req, res);
   };

+ 9 - 7
apps/app/src/stores/context.tsx

@@ -1,5 +1,5 @@
 import type { ColorScheme, IUserHasId } from '@growi/core';
-import { SWRResponse } from 'swr';
+import useSWR, { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
 import { SupportedActionType } from '~/interfaces/activity';
@@ -232,14 +232,16 @@ 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,
+  return useSWR(
+    isLoading ? null : ['isAdminUser', currentUser?._id, currentUser?.admin],
+    ([, , isAdmin]) => isAdmin ?? false,
     {
-      fallbackData: isAdminUser,
+      fallbackData: currentUser?.admin ?? false,
       keepPreviousData: true,
+      // disable all revalidation but revalidateIfStale
+      revalidateOnMount: false,
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
     },
   );
 };

+ 5 - 3
apps/app/src/stores/search.tsx

@@ -1,5 +1,4 @@
-import { mutate, SWRResponse } from 'swr';
-import useSWRImmutable from 'swr/immutable';
+import useSWR, { mutate, SWRResponse } from 'swr';
 
 import { apiGet } from '~/client/util/apiv1-client';
 import { IFormattedSearchResult, SORT_AXIS, SORT_ORDER } from '~/interfaces/search';
@@ -67,7 +66,7 @@ export const useSWRxSearch = (
 
   const isKeywordValid = keyword != null && keyword.length > 0;
 
-  const swrResult = useSWRImmutable(
+  const swrResult = useSWR(
     isKeywordValid ? ['/search', keyword, fixedConfigurations] : null,
     ([endpoint, , fixedConfigurations]) => {
       const {
@@ -86,6 +85,9 @@ export const useSWRxSearch = (
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       ).then(result => result as IFormattedSearchResult);
     },
+    {
+      revalidateOnFocus: false,
+    },
   );
 
   return {

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

@@ -11,21 +11,6 @@ function openEditor() {
   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', () => {
   const ssPrefix = 'access-to-page-';
 
@@ -107,7 +92,7 @@ context('Access to page', () => {
     openEditor();
 
     // check edited contents after save
-    appendTextToEditorUntilContains(body1);
+    cy.appendTextToEditorUntilContains(body1);
     cy.get('.page-editor-preview-body').should('contain.text', body1);
     cy.getByTestid('page-editor').should('be.visible');
     cy.getByTestid('save-page-btn').click();
@@ -124,7 +109,7 @@ context('Access to page', () => {
     openEditor();
 
     // check editing contents with shortcut key
-    appendTextToEditorUntilContains(body2);
+    cy.appendTextToEditorUntilContains(body2);
     cy.get('.page-editor-preview-body').should('contain.text', body1+body2);
     cy.get('.CodeMirror').click().type(savePageShortcutKey);
     cy.get('.CodeMirror-code').should('contain.text', body1+body2);
@@ -295,8 +280,7 @@ context('Access to Template Editing Mode', () => {
       cy.screenshot(`${ssPrefix}-open-template-page-for-children-in-editor-mode`);
     });
 
-    cy.get('.CodeMirror textarea').type(templateBody1, { force: true });
-    cy.get('.CodeMirror-code').should('contain.text', templateBody1);
+    cy.appendTextToEditorUntilContains(templateBody1);
     cy.get('.page-editor-preview-body').should('contain.text', templateBody1);
     cy.getByTestid('page-editor').should('be.visible');
     cy.getByTestid('save-page-btn').click();
@@ -330,8 +314,7 @@ context('Access to Template Editing Mode', () => {
       cy.screenshot(`${ssPrefix}-open-template-page-for-descendants-in-editor-mode`);
     })
 
-    cy.get('.CodeMirror textarea').type(templateBody2, { force: true });
-    cy.get('.CodeMirror-code').should('contain.text', templateBody2);
+    cy.appendTextToEditorUntilContains(templateBody2);
     cy.get('.page-editor-preview-body').should('contain.text', templateBody2);
     cy.getByTestid('page-editor').should('be.visible');
     cy.getByTestid('save-page-btn').click();

+ 135 - 0
apps/app/test/cypress/e2e/20-basic-features/20-basic-features--comments.cy.ts

@@ -0,0 +1,135 @@
+context('Comment', () => {
+  const ssPrefix = 'comments-';
+  let commentCount = 0;
+
+  beforeEach(() => {
+    // login
+    cy.fixture("user-admin.json").then(user => {
+      cy.login(user.username, user.password);
+    });
+
+    // visit page
+    cy.visit('/comment');
+    cy.collapseSidebar(true, true);
+  })
+
+  it('Create comment page', () => {
+    // save page
+    cy.get('#grw-page-editor-mode-manager').as('pageEditorModeManager').should('be.visible');
+    cy.waitUntil(() => {
+      // do
+      cy.get('@pageEditorModeManager').within(() => {
+        cy.get('button:nth-child(2)').click();
+      });
+      // until
+      return cy.get('.layout-root').then($elem => $elem.hasClass('editing'));
+    });
+    cy.get('.CodeMirror').should('be.visible');
+
+    cy.getByTestid('page-editor').should('be.visible');
+    cy.getByTestid('save-page-btn').click();
+  })
+
+  it('Successfully add comments', () => {
+    const commetText = 'add comment';
+
+    cy.getByTestid('page-comment-button').click();
+
+    // Open comment editor
+    cy.waitUntil(() => {
+      // do
+      cy.getByTestid('open-comment-editor-button').click();
+      // wait until
+      return cy.get('.comment-write').then($elem => $elem.is(':visible'));
+    });
+
+    cy.get('.CodeMirror').type(commetText);
+    cy.getByTestid("comment-submit-button").eq(0).click();
+
+    // Check update comment count
+    commentCount += 1
+    cy.getByTestid('page-comment-button').contains(commentCount);
+    cy.screenshot(`${ssPrefix}1-add-comments`);
+  });
+
+  it('Successfully reply comments', () => {
+    const commetText = 'reply comment';
+
+    cy.getByTestid('page-comment-button').click();
+
+    // Open reply comment editor
+    cy.waitUntil(() => {
+      // do
+      cy.getByTestid('comment-reply-button').eq(0).click();
+      // wait until
+      return cy.get('.comment-write').then($elem => $elem.is(':visible'));
+    });
+
+    cy.get('.CodeMirror').type(commetText);
+    cy.getByTestid("comment-submit-button").eq(0).click();
+
+    // Check update comment count
+    commentCount += 1
+    cy.getByTestid('page-comment-button').contains(commentCount);
+    cy.screenshot(`${ssPrefix}2-reply-comments`);
+  });
+
+  it('Successfully delete comments', () => {
+
+    cy.getByTestid('page-comment-button').click();
+
+    cy.get('.page-comments').should('be.visible');
+    cy.getByTestid('comment-delete-button').eq(0).click({force: true});
+    cy.get('.modal-content').then($elem => $elem.is(':visible'));
+    cy.get('.modal-footer > button:nth-child(3)').click();
+
+    // Check update comment count
+    commentCount -= 2
+    cy.getByTestid('page-comment-button').contains(commentCount);
+    cy.screenshot(`${ssPrefix}3-delete-comments`);
+  });
+
+  // Mention username in comment
+  it('Successfully mention username in comment', () => {
+    const username = '@adm';
+
+    cy.getByTestid('page-comment-button').click();
+
+    // Open comment editor
+    cy.waitUntil(() => {
+      // do
+      cy.getByTestid('open-comment-editor-button').click();
+      // wait until
+      return cy.get('.comment-write').then($elem => $elem.is(':visible'));
+    });
+
+    cy.appendTextToEditorUntilContains(username);
+
+    cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}4-mention-username-found`) });
+    // Click on mentioned username
+    cy.get('.CodeMirror-hints > li').first().click();
+    cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}5-mention-username-mentioned`) });
+  });
+
+  it('Username not found when mention username in comment', () => {
+    const username = '@user';
+
+    cy.getByTestid('page-comment-button').click();
+
+    // Open comment editor
+    cy.waitUntil(() => {
+      // do
+      cy.getByTestid('open-comment-editor-button').click();
+      // wait until
+      return cy.get('.comment-write').then($elem => $elem.is(':visible'));
+    });
+
+    cy.appendTextToEditorUntilContains(username);
+
+    cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}6-mention-username-not-found`) });
+    // Click on username not found hint
+    cy.get('.CodeMirror-hints > li').first().click();
+    cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}7-mention-no-username-mentioned`) });
+  });
+
+})

+ 0 - 61
apps/app/test/cypress/e2e/20-basic-features/20-basic-features--username-mention.cy.ts

@@ -1,61 +0,0 @@
-context('Mention username in comment', () => {
-  const ssPrefix = 'mention-username-';
-
-  beforeEach(() => {
-    // login
-    cy.fixture("user-admin.json").then(user => {
-      cy.login(user.username, user.password);
-    });
-
-    // Visit /Sandbox
-    cy.visit('/Sandbox');
-    cy.waitUntilSkeletonDisappear();
-
-    cy.collapseSidebar(true, true);
-
-    // Go to comment page
-    cy.getByTestid('page-comment-button').click();
-
-    // Open comment editor
-    cy.waitUntil(() => {
-      // do
-      cy.getByTestid('open-comment-editor-button').click();
-      // wait until
-      return cy.get('.comment-write').then($elem => $elem.is(':visible'));
-    });
-
-  });
-
-  it('Successfully mention username in comment', () => {
-    const username = '@adm';
-
-    cy.waitUntil(() => {
-      // do
-      cy.get('.CodeMirror').type(username);
-      // wait until
-      return cy.get('.CodeMirror-hints').then($elem => $elem.is(':visible'));
-    });
-
-    cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}1-username-found`) });
-    // Click on mentioned username
-    cy.get('.CodeMirror-hints > li').first().click();
-    cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}2-username-mentioned`) });
-  });
-
-  it('Username not found when mention username in comment', () => {
-    const username = '@user';
-
-    cy.waitUntil(() => {
-      // do
-      cy.get('.CodeMirror').type(username);
-      // wait until
-      return cy.get('.CodeMirror-hints').then($elem => $elem.is(':visible'));
-    });
-
-    cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}3-username-not-found`) });
-    // Click on username not found hint
-    cy.get('.CodeMirror-hints > li').first().click();
-    cy.get('#comments-container').within(() => { cy.screenshot(`${ssPrefix}4-no-username-mentioned`) });
-  });
-
-});

+ 15 - 0
apps/app/test/cypress/support/commands.ts

@@ -101,3 +101,18 @@ Cypress.Commands.add('collapseSidebar', (isCollapsed: boolean, waitUntilSaving =
   });
 
 });
+
+Cypress.Commands.add('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);
+      });
+  });
+});

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

@@ -40,6 +40,7 @@ declare global {
        collapseSidebar(isCollapsed: boolean, waitUntilSaving?: boolean): Chainable<void>,
        waitUntilSkeletonDisappear(): Chainable<void>,
        waitUntilSpinnerDisappear(): Chainable<void>,
+       appendTextToEditorUntilContains(inputText: string): Chainable<void>
     }
   }
 }

+ 1 - 1
apps/app/test/integration/service/questionnaire.test.ts

@@ -53,7 +53,7 @@ describe('QuestionnaireService', () => {
         deploymentType: 'growi-docker-compose',
         type: 'on-premise',
         version: crowi.version,
-        wikiType: 'open',
+        wikiType: 'closed',
       });
     });
 

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

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

+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "6.1.8-RC.0",
+  "version": "6.1.11-RC.0",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -83,7 +83,7 @@
     "reg-notify-github-plugin": "^0.11.1",
     "reg-notify-slack-plugin": "^0.11.0",
     "reg-publish-s3-plugin": "^0.11.0",
-    "reg-suit": "^0.12.1",
+    "reg-suit": "^0.12.2",
     "shx": "^0.3.4",
     "stylelint": "^14.2.0",
     "stylelint-config-recess-order": "^3.0.0",

+ 1 - 1
packages/core/package.json

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

+ 1 - 1
packages/hackmd/package.json

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

+ 1 - 1
packages/presentation/package.json

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

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

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

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

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

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

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

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

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

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

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

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

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-lsx",
-  "version": "6.1.8-RC.0",
+  "version": "6.1.11-RC.0",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/slack/package.json

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

+ 1 - 1
packages/ui/package.json

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

+ 9 - 9
packages/ui/src/components/PagePath/PageListMeta.tsx

@@ -1,4 +1,4 @@
-import React, { FC } from 'react';
+import { FC } from 'react';
 
 import assert from 'assert';
 
@@ -45,7 +45,7 @@ const SeenUsersCount = (props: SeenUsersCountProps): JSX.Element => {
   const strengthClass = `strength-${strengthLevel}`; // strength-{0, 1, 2, 3, 4}
 
   return (
-    <span className={`seen-users-count ${shouldSpaceOutIcon ? 'mr-3' : ''} ${strengthClass}`}>
+    <span className={`seen-users-count ${shouldSpaceOutIcon ? 'mr-2' : ''} ${strengthClass}`}>
       <i className="footstamp-icon"><FootstampIcon /></i>
       {count}
     </span>
@@ -69,40 +69,40 @@ export const PageListMeta: FC<PageListMetaProps> = (props: PageListMetaProps) =>
   // top check
   let topLabel;
   if (isTopPage(page.path)) {
-    topLabel = <span className={`badge badge-info ${shouldSpaceOutIcon ? 'mr-3' : ''} top-label`}>TOP</span>;
+    topLabel = <span className={`badge badge-info ${shouldSpaceOutIcon ? 'mr-2' : ''} top-label`}>TOP</span>;
   }
 
   // template check
   let templateLabel;
   if (checkTemplatePath(page.path)) {
-    templateLabel = <span className={`badge badge-info ${shouldSpaceOutIcon ? 'mr-3' : ''}`}>TMPL</span>;
+    templateLabel = <span className={`badge badge-info ${shouldSpaceOutIcon ? 'mr-2' : ''}`}>TMPL</span>;
   }
 
   let commentCount;
   if (page.commentCount > 0) {
-    commentCount = <span className={`${shouldSpaceOutIcon ? 'mr-3' : ''}`}><i className="icon-bubble" />{page.commentCount}</span>;
+    commentCount = <span className={`${shouldSpaceOutIcon ? 'mr-2' : ''}`}><i className="icon-bubble" />{page.commentCount}</span>;
   }
 
   let likerCount;
   if (props.likerCount != null && props.likerCount > 0) {
-    likerCount = <span className={`${shouldSpaceOutIcon ? 'mr-3' : ''}`}><i className="fa fa-heart-o" />{props.likerCount}</span>;
+    likerCount = <span className={`${shouldSpaceOutIcon ? 'mr-2' : ''}`}><i className="fa fa-heart-o" />{props.likerCount}</span>;
   }
 
   let locked;
   if (page.grant !== 1) {
-    locked = <span className={`${shouldSpaceOutIcon ? 'mr-3' : ''}`}><i className="icon-lock" /></span>;
+    locked = <span className={`${shouldSpaceOutIcon ? 'mr-2' : ''}`}><i className="icon-lock" /></span>;
   }
 
   let bookmarkCount;
   if (props.bookmarkCount != null && props.bookmarkCount > 0) {
-    bookmarkCount = <span className={`${shouldSpaceOutIcon ? 'mr-3' : ''}`}><i className="fa fa-bookmark-o" />{props.bookmarkCount}</span>;
+    bookmarkCount = <span className={`${shouldSpaceOutIcon ? 'mr-2' : ''}`}><i className="fa fa-bookmark-o" />{props.bookmarkCount}</span>;
   }
 
   return (
     <span className="page-list-meta">
       {topLabel}
       {templateLabel}
-      <SeenUsersCount count={page.seenUsers.length} basisViewersCount={basisViewersCount} />
+      <SeenUsersCount count={page.seenUsers.length} basisViewersCount={basisViewersCount} shouldSpaceOutIcon={shouldSpaceOutIcon} />
       {commentCount}
       {likerCount}
       {locked}

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 76 - 489
yarn.lock


Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно