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

Merge pull request #7421 from weseek/master

Release v6.0.8
Yuki Takei 3 лет назад
Родитель
Сommit
ad5c998190
56 измененных файлов с 454 добавлено и 295 удалено
  1. 1 1
      lerna.json
  2. 1 1
      package.json
  3. 11 11
      packages/app/package.json
  4. 1 2
      packages/app/src/client/services/page-operation.ts
  5. 1 1
      packages/app/src/client/services/side-effects/hackmd-draft-updated.ts
  6. 1 1
      packages/app/src/client/services/side-effects/page-updated.ts
  7. 3 4
      packages/app/src/components/InfiniteScroll.tsx
  8. 4 2
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  9. 1 1
      packages/app/src/components/Page/PageContentsUtilities.tsx
  10. 2 2
      packages/app/src/components/Page/PageView.tsx
  11. 28 3
      packages/app/src/components/PageAccessoriesModal.tsx
  12. 1 2
      packages/app/src/components/PageAlert/OldRevisionAlert.tsx
  13. 1 1
      packages/app/src/components/PageAlert/PageAlerts.tsx
  14. 2 2
      packages/app/src/components/PageAttachment.tsx
  15. 1 1
      packages/app/src/components/PageDeleteModal.tsx
  16. 32 15
      packages/app/src/components/PageEditor.tsx
  17. 9 8
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  18. 2 2
      packages/app/src/components/PageEditor/ConflictDiffModal.tsx
  19. 2 2
      packages/app/src/components/PageEditorByHackmd.tsx
  20. 24 68
      packages/app/src/components/PageHistory.tsx
  21. 128 49
      packages/app/src/components/PageHistory/PageRevisionTable.tsx
  22. 3 3
      packages/app/src/components/PageHistory/Revision.tsx
  23. 1 1
      packages/app/src/components/PagePathNav.tsx
  24. 2 1
      packages/app/src/components/RevisionComparer/RevisionComparer.tsx
  25. 2 2
      packages/app/src/components/SavePageControls.tsx
  26. 1 1
      packages/app/src/components/ShareLink/ShareLink.tsx
  27. 1 1
      packages/app/src/components/ShareLink/ShareLinkForm.tsx
  28. 1 1
      packages/app/src/components/ShareLink/ShareLinkPageView.tsx
  29. 2 4
      packages/app/src/components/Sidebar/PageTree.tsx
  30. 1 1
      packages/app/src/components/Sidebar/RecentChanges.tsx
  31. 13 18
      packages/app/src/components/TemplateModal.tsx
  32. 24 7
      packages/app/src/pages/[[...path]].page.tsx
  33. 2 1
      packages/app/src/pages/share/[[...path]].page.tsx
  34. 2 1
      packages/app/src/pages/trash.page.tsx
  35. 5 1
      packages/app/src/server/routes/apiv3/pages.js
  36. 21 10
      packages/app/src/server/routes/apiv3/revisions.js
  37. 5 2
      packages/app/src/server/routes/attachment.js
  38. 1 1
      packages/app/src/server/routes/login.js
  39. 5 5
      packages/app/src/server/service/page.ts
  40. 2 14
      packages/app/src/stores/context.tsx
  41. 29 0
      packages/app/src/stores/modal.tsx
  42. 39 21
      packages/app/src/stores/page.tsx
  43. 5 3
      packages/app/src/stores/ui.tsx
  44. 8 0
      packages/app/src/styles/theme/_apply-colors.scss
  45. 1 1
      packages/codemirror-textlint/package.json
  46. 1 1
      packages/core/package.json
  47. 6 0
      packages/core/src/interfaces/revision.ts
  48. 1 1
      packages/hackmd/package.json
  49. 2 2
      packages/presentation/package.json
  50. 1 1
      packages/preset-themes/package.json
  51. 1 1
      packages/remark-drawio/package.json
  52. 1 1
      packages/remark-growi-directive/package.json
  53. 4 4
      packages/remark-lsx/package.json
  54. 1 1
      packages/slack/package.json
  55. 2 2
      packages/slackbot-proxy/package.json
  56. 2 2
      packages/ui/package.json

+ 1 - 1
lerna.json

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

+ 1 - 1
package.json

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

+ 11 - 11
packages/app/package.json

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

+ 1 - 2
packages/app/src/client/services/page-operation.ts

@@ -4,9 +4,8 @@ import { SubscriptionStatusType, Nullable } from '@growi/core';
 import urljoin from 'url-join';
 
 import { OptionsToSave } from '~/interfaces/page-operation';
-import { useCurrentPageId } from '~/stores/context';
 import { useEditingMarkdown, useIsEnabledUnsavedWarning, usePageTagsForEditors } from '~/stores/editor';
-import { useSWRMUTxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
+import { useCurrentPageId, useSWRMUTxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import loggerFactory from '~/utils/logger';
 

+ 1 - 1
packages/app/src/client/services/side-effects/hackmd-draft-updated.ts

@@ -1,8 +1,8 @@
 import { useCallback, useEffect } from 'react';
 
 import { SocketEventName } from '~/interfaces/websocket';
-import { useCurrentPageId } from '~/stores/context';
 import { useIsHackmdDraftUpdatingInRealtime } from '~/stores/hackmd';
+import { useCurrentPageId } from '~/stores/page';
 import { useGlobalSocket } from '~/stores/websocket';
 
 export const useHackmdDraftUpdatedEffect = (): void => {

+ 1 - 1
packages/app/src/client/services/side-effects/page-updated.ts

@@ -1,7 +1,7 @@
 import { useCallback, useEffect } from 'react';
 
 import { SocketEventName } from '~/interfaces/websocket';
-import { useCurrentPageId } from '~/stores/context';
+import { useCurrentPageId } from '~/stores/page';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import { useGlobalSocket } from '~/stores/websocket';
 

+ 3 - 4
packages/app/src/components/Sidebar/InfiniteScroll.tsx → packages/app/src/components/InfiniteScroll.tsx

@@ -5,11 +5,11 @@ import React, {
 import type { SWRInfiniteResponse } from 'swr/infinite';
 
 type Props<T> = {
-  swrInifiniteResponse : SWRInfiniteResponse<T>
+  swrInifiniteResponse: SWRInfiniteResponse<T>
   children: React.ReactNode,
   loadingIndicator?: React.ReactNode
   endingIndicator?: React.ReactNode
-  isReachingEnd?: boolean,
+  isReachingEnd?: boolean
   offset?: number
 }
 
@@ -59,8 +59,7 @@ const InfiniteScroll = <E, >(props: Props<E>): React.ReactElement<Props<E>> => {
 
   return (
     <>
-      { children }
-
+      {children}
       <div style={{ position: 'relative' }}>
         <div ref={ref} style={{ position: 'absolute', top: offset }}></div>
         {isReachingEnd

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

@@ -16,7 +16,7 @@ import {
 } from '~/interfaces/page';
 import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import {
-  useCurrentPageId, useCurrentPathname, useIsNotFound,
+  useCurrentPathname,
   useCurrentUser, useIsGuestUser, useIsSharedUser, useShareLinkId, useTemplateTagData, useIsContainerFluid, useIsIdenticalPath,
 } from '~/stores/context';
 import { usePageTagsForEditors } from '~/stores/editor';
@@ -24,7 +24,9 @@ import {
   usePageAccessoriesModal, PageAccessoriesModalContents, IPageForPageDuplicateModal,
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal, usePagePresentationModal,
 } from '~/stores/modal';
-import { useSWRMUTxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
+import {
+  useSWRMUTxCurrentPage, useSWRxTagsInfo, useCurrentPageId, useIsNotFound,
+} from '~/stores/page';
 import {
   EditorMode, useDrawerMode, useEditorMode, useIsAbleToShowPageManagement, useIsAbleToShowTagLabel,
   useIsAbleToChangeEditorMode, useIsAbleToShowPageAuthors,

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

@@ -4,7 +4,7 @@ import { useUpdateStateAfterSave } from '~/client/services/page-operation';
 import { useDrawioModalLauncherForView } from '~/client/services/side-effects/drawio-modal-launcher-for-view';
 import { useHandsontableModalLauncherForView } from '~/client/services/side-effects/handsontable-modal-launcher-for-view';
 import { toastSuccess, toastError } from '~/client/util/toastr';
-import { useCurrentPageId } from '~/stores/context';
+import { useCurrentPageId } from '~/stores/page';
 
 
 export const PageContentsUtilities = (): null => {

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

@@ -9,9 +9,9 @@ import dynamic from 'next/dynamic';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import { generateSSRViewOptions } from '~/services/renderer/renderer';
 import {
-  useIsForbidden, useIsIdenticalPath, useIsNotCreatable, useIsNotFound,
+  useIsForbidden, useIsIdenticalPath, useIsNotCreatable,
 } from '~/stores/context';
-import { useSWRxCurrentPage } from '~/stores/page';
+import { useSWRxCurrentPage, useIsNotFound } from '~/stores/page';
 import { useViewOptions } from '~/stores/renderer';
 import { useIsMobile } from '~/stores/ui';
 import { registerGrowiFacade } from '~/utils/growi-facade';

+ 28 - 3
packages/app/src/components/PageAccessoriesModal.tsx

@@ -17,7 +17,7 @@ import AttachmentIcon from './Icons/AttachmentIcon';
 import HistoryIcon from './Icons/HistoryIcon';
 import ShareLinkIcon from './Icons/ShareLinkIcon';
 import PageAttachment from './PageAttachment';
-import { PageHistory } from './PageHistory';
+import { PageHistory, getQueryParam } from './PageHistory';
 import ShareLink from './ShareLink/ShareLink';
 
 import styles from './PageAccessoriesModal.module.scss';
@@ -27,6 +27,9 @@ const PageAccessoriesModal = (): JSX.Element => {
   const { t } = useTranslation();
 
   const [activeTab, setActiveTab] = useState<PageAccessoriesModalContents>(PageAccessoriesModalContents.PageHistory);
+  const [sourceRevisionId, setSourceRevisionId] = useState<string>();
+  const [targetRevisionId, setTargetRevisionId] = useState<string>();
+
   const [isWindowExpanded, setIsWindowExpanded] = useState(false);
 
   const { data: isSharedUser } = useIsSharedUser();
@@ -48,6 +51,28 @@ const PageAccessoriesModal = (): JSX.Element => {
     }, false);
   }, [mutate, 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]);
+
   const navTabMapping = useMemo(() => {
     const isOpened = status == null ? false : status.isOpened;
     return {
@@ -57,7 +82,7 @@ const PageAccessoriesModal = (): JSX.Element => {
           if (!isOpened) {
             return <></>;
           }
-          return <PageHistory onClose={close}/>;
+          return <PageHistory onClose={close} sourceRevisionId={sourceRevisionId} targetRevisionId={targetRevisionId}/>;
         },
         i18n: t('History'),
         index: 0,
@@ -87,7 +112,7 @@ const PageAccessoriesModal = (): JSX.Element => {
         isLinkEnabled: () => !isGuestUser && !isSharedUser && !isLinkSharingDisabled,
       },
     };
-  }, [status, t, close, isGuestUser, isSharedUser, isLinkSharingDisabled]);
+  }, [status, t, close, sourceRevisionId, targetRevisionId, isGuestUser, isSharedUser, isLinkSharingDisabled]);
 
   const buttons = useMemo(() => (
     <div className="d-flex flex-nowrap">

+ 1 - 2
packages/app/src/components/PageAlert/OldRevisionAlert.tsx

@@ -4,8 +4,7 @@ import { pathUtils } from '@growi/core';
 import Link from 'next/link';
 import { useTranslation } from 'react-i18next';
 
-import { useIsLatestRevision } from '~/stores/context';
-import { useSWRxCurrentPage } from '~/stores/page';
+import { useSWRxCurrentPage, useIsLatestRevision } from '~/stores/page';
 
 export const OldRevisionAlert = (): JSX.Element => {
 

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

@@ -2,7 +2,7 @@ import React from 'react';
 
 import dynamic from 'next/dynamic';
 
-import { useIsNotFound } from '~/stores/context';
+import { useIsNotFound } from '~/stores/page';
 
 import { OldRevisionAlert } from './OldRevisionAlert';
 import { PageGrantAlert } from './PageGrantAlert';

+ 2 - 2
packages/app/src/components/PageAttachment.tsx

@@ -5,8 +5,8 @@ import React, {
 import { HasObjectId, IAttachment } from '@growi/core';
 
 import { useSWRxAttachments } from '~/stores/attachment';
-import { useCurrentPageId, useIsGuestUser } from '~/stores/context';
-import { useSWRxCurrentPage } from '~/stores/page';
+import { useIsGuestUser } from '~/stores/context';
+import { useSWRxCurrentPage, useCurrentPageId } from '~/stores/page';
 
 import { DeleteAttachmentModal } from './PageAttachment/DeleteAttachmentModal';
 import { PageAttachmentList } from './PageAttachment/PageAttachmentList';

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

@@ -229,7 +229,7 @@ const PageDeleteModal: FC = () => {
       return pages.map(page => (
         <p key={page.data._id} className="mb-1">
           <code>{ page.data.path }</code>
-          { !page.meta?.isDeletable && <span className="ml-3 text-danger"><strong>(CAN NOT TO DELETE)</strong></span> }
+          { page.meta?.isDeletable != null && !page.meta.isDeletable && <span className="ml-3 text-danger"><strong>(CAN NOT TO DELETE)</strong></span> }
         </p>
       ));
     }

+ 32 - 15
packages/app/src/components/PageEditor.tsx

@@ -16,13 +16,13 @@ import { throttle, debounce } from 'throttle-debounce';
 
 import { useUpdateStateAfterSave, useSaveOrUpdate } from '~/client/services/page-operation';
 import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
-import { toastError, toastSuccess, toastWarning } from '~/client/util/toastr';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { IEditorMethods } from '~/interfaces/editor-methods';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { SocketEventName } from '~/interfaces/websocket';
 import {
-  useCurrentPathname, useCurrentPageId, useIsEnabledAttachTitleHeader, useTemplateBodyData,
-  useIsEditable, useIsUploadableFile, useIsUploadableImage, useIsNotFound, useIsIndentSizeForced,
+  useCurrentPathname, useIsEnabledAttachTitleHeader, useTemplateBodyData,
+  useIsEditable, useIsUploadableFile, useIsUploadableImage, useIsIndentSizeForced,
 } from '~/stores/context';
 import {
   useCurrentIndentSize, useIsSlackEnabled, useIsTextlintEnabled, usePageTagsForEditors,
@@ -32,9 +32,15 @@ import {
 } from '~/stores/editor';
 import { useConflictDiffModal } from '~/stores/modal';
 import {
-  useCurrentPagePath, useSWRMUTxCurrentPage, useSWRxCurrentPage, useSWRxTagsInfo,
+  useCurrentPagePath, useSWRMUTxCurrentPage, useSWRxCurrentPage, useSWRxTagsInfo, useCurrentPageId, useIsNotFound, useIsLatestRevision,
 } from '~/stores/page';
 import { mutatePageTree } from '~/stores/page-listing';
+import {
+  useRemoteRevisionId,
+  useRemoteRevisionBody,
+  useRemoteRevisionLastUpdatedAt,
+  useRemoteRevisionLastUpdateUser,
+} from '~/stores/remote-latest-page';
 import { usePreviewOptions } from '~/stores/renderer';
 import {
   EditorMode,
@@ -71,8 +77,8 @@ const PageEditor = React.memo((): JSX.Element => {
   const { t } = useTranslation();
   const router = useRouter();
 
-  const { data: isNotFound } = useIsNotFound();
-  const { data: pageId } = useCurrentPageId();
+  const { data: isNotFound, mutate: mutateIsNotFound } = useIsNotFound();
+  const { data: pageId, mutate: mutateCurrentPageId } = useCurrentPageId();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPathname } = useCurrentPathname();
   const { data: currentPage } = useSWRxCurrentPage();
@@ -92,6 +98,11 @@ const PageEditor = React.memo((): JSX.Element => {
   const { data: isUploadableFile } = useIsUploadableFile();
   const { data: isUploadableImage } = useIsUploadableImage();
   const { data: conflictDiffModalStatus, close: closeConflictDiffModal } = useConflictDiffModal();
+  const { mutate: mutateIsLatestRevision } = useIsLatestRevision();
+  const { mutate: mutateRemotePageId } = useRemoteRevisionId();
+  const { mutate: mutateRemoteRevisionId } = useRemoteRevisionBody();
+  const { mutate: mutateRemoteRevisionLastUpdatedAt } = useRemoteRevisionLastUpdatedAt();
+  const { mutate: mutateRemoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
 
   const { data: rendererOptions, mutate: mutateRendererOptions } = usePreviewOptions();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
@@ -218,18 +229,18 @@ const PageEditor = React.memo((): JSX.Element => {
       logger.error('failed to save', error);
       toastError(error);
       if (error.code === 'conflict') {
-        toastWarning('(TBD) resolve conflict');
-        // pageContainer.setState({
-        //   remoteRevisionId: error.data.revisionId,
-        //   remoteRevisionBody: error.data.revisionBody,
-        //   remoteRevisionUpdateAt: error.data.createdAt,
-        //   lastUpdateUser: error.data.user,
-        // });
+        mutateRemotePageId(error.data.revisionId);
+        mutateRemoteRevisionId(error.data.revisionBody);
+        mutateRemoteRevisionLastUpdatedAt(error.data.createdAt);
+        mutateRemoteRevisionLastUpdateUser(error.data.user);
       }
       return null;
     }
 
-  }, [currentPathname, optionsToSave, grantData, isSlackEnabled, saveOrUpdate, pageId, currentPagePath, currentRevisionId]);
+  }, [
+    currentPathname, optionsToSave, grantData, isSlackEnabled, saveOrUpdate, pageId,
+    currentPagePath, currentRevisionId, mutateRemotePageId, mutateRemoteRevisionId, mutateRemoteRevisionLastUpdatedAt, mutateRemoteRevisionLastUpdateUser,
+  ]);
 
   const saveAndReturnToViewHandler = useCallback(async(opts: {slackChannels: string, overwriteScopesOfDescendants?: boolean}) => {
     if (editorMode !== EditorMode.Editor) {
@@ -300,6 +311,9 @@ const PageEditor = React.memo((): JSX.Element => {
       if (pageId != null) {
         formData.append('page_id', pageId);
       }
+      if (pageId == null && markdownToSave.current != null) {
+        formData.append('page_body', markdownToSave.current);
+      }
 
       res = await apiPostForm('/attachments.add', formData);
       const attachment = res.attachment;
@@ -318,6 +332,9 @@ const PageEditor = React.memo((): JSX.Element => {
         logger.info('Page is created', res.page._id);
         globalEmitter.emit('resetInitializedHackMdStatus');
         mutateGrant(res.page.grant);
+        mutateIsLatestRevision(true);
+        await mutateCurrentPageId(res.page._id);
+        await mutateCurrentPage();
       }
     }
     catch (e) {
@@ -327,7 +344,7 @@ const PageEditor = React.memo((): JSX.Element => {
     finally {
       editorRef.current.terminateUploadingState();
     }
-  }, [currentPagePath, mutateGrant, pageId]);
+  }, [currentPagePath, mutateCurrentPage, mutateCurrentPageId, mutateGrant, mutateIsLatestRevision, mutateIsNotFound, pageId]);
 
 
   const scrollPreviewByEditorLine = useCallback((line: number) => {

+ 9 - 8
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -11,10 +11,9 @@ import { throttle, debounce } from 'throttle-debounce';
 import urljoin from 'url-join';
 
 import InterceptorManager from '~/services/interceptor-manager';
-import { useHandsontableModal, useDrawioModal } from '~/stores/modal';
+import { useHandsontableModal, useDrawioModal, useTemplateModal } from '~/stores/modal';
 import loggerFactory from '~/utils/logger';
 
-import { TemplateModal } from '../TemplateModal';
 import { UncontrolledCodeMirror } from '../UncontrolledCodeMirror';
 
 import AbstractEditor from './AbstractEditor';
@@ -873,7 +872,8 @@ class CodeMirrorEditor extends AbstractEditor {
   }
 
   showTemplateModal() {
-    this.setState({ isTemplateModalOpened: true });
+    const onSubmit = templateText => this.setValue(templateText);
+    this.props.onClickTemplateBtn(onSubmit);
   }
 
   // fold draw.io section (``` drawio ~ ```)
@@ -1158,11 +1158,6 @@ class CodeMirrorEditor extends AbstractEditor {
           ref={this.linkEditModal}
           onSave={(linkText) => { return markdownLinkUtil.replaceFocusedMarkdownLinkWithEditor(this.getCodeMirror(), linkText) }}
         />
-        <TemplateModal
-          isOpen={this.state.isTemplateModalOpened}
-          onClose={() => this.setState({ isTemplateModalOpened: false })}
-          onSubmit={templateText => this.setValue(templateText) }
-        />
       </div>
     );
   }
@@ -1187,6 +1182,7 @@ const CodeMirrorEditorMemoized = memo(CodeMirrorEditor);
 const CodeMirrorEditorFc = React.forwardRef((props, ref) => {
   const { open: openDrawioModal } = useDrawioModal();
   const { open: openHandsontableModal } = useHandsontableModal();
+  const { open: openTemplateModal } = useTemplateModal();
 
   const openDrawioModalHandler = useCallback((drawioMxFile, onSave) => {
     openDrawioModal(drawioMxFile, onSave);
@@ -1196,11 +1192,16 @@ const CodeMirrorEditorFc = React.forwardRef((props, ref) => {
     openHandsontableModal(markdownTable, editor, autoFormatMarkdownTable);
   }, [openHandsontableModal]);
 
+  const openTemplateModalHandler = useCallback((onSubmit) => {
+    openTemplateModal(onSubmit);
+  }, [openTemplateModal]);
+
   return (
     <CodeMirrorEditorMemoized
       ref={ref}
       onClickDrawioBtn={openDrawioModalHandler}
       onClickTableBtn={openTableModalHandler}
+      onClickTemplateBtn={openTemplateModalHandler}
       {...props}
     />
   );

+ 2 - 2
packages/app/src/components/PageEditor/ConflictDiffModal.tsx

@@ -13,8 +13,8 @@ import {
 import { useSaveOrUpdate } from '~/client/services/page-operation';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { OptionsToSave } from '~/interfaces/page-operation';
-import { useCurrentPageId, useCurrentPathname, useCurrentUser } from '~/stores/context';
-import { useCurrentPagePath, useSWRxCurrentPage } from '~/stores/page';
+import { useCurrentPathname, useCurrentUser } from '~/stores/context';
+import { useCurrentPagePath, useSWRxCurrentPage, useCurrentPageId } from '~/stores/page';
 import {
   useRemoteRevisionBody, useRemoteRevisionId, useRemoteRevisionLastUpdatedAt, useRemoteRevisionLastUpdateUser, useSetRemoteLatestPageData,
 } from '~/stores/remote-latest-page';

+ 2 - 2
packages/app/src/components/PageEditorByHackmd.tsx

@@ -16,7 +16,7 @@ import { apiPost } from '~/client/util/apiv1-client';
 import { IResHackmdIntegrated, IResHackmdDiscard } from '~/interfaces/hackmd';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import {
-  useCurrentPageId, useCurrentPathname, useHackmdUri, useIsNotFound,
+  useCurrentPathname, useHackmdUri,
 } from '~/stores/context';
 import {
   useIsSlackEnabled, usePageTagsForEditors, useIsEnabledUnsavedWarning,
@@ -25,7 +25,7 @@ import {
   usePageIdOnHackmd, useHasDraftOnHackmd, useRevisionIdHackmdSynced, useIsHackmdDraftUpdatingInRealtime,
 } from '~/stores/hackmd';
 import {
-  useCurrentPagePath, useSWRMUTxCurrentPage, useSWRxCurrentPage, useSWRxTagsInfo,
+  useCurrentPagePath, useSWRMUTxCurrentPage, useSWRxCurrentPage, useSWRxTagsInfo, useCurrentPageId, useIsNotFound,
 } from '~/stores/page';
 import { mutatePageTree } from '~/stores/page-listing';
 import { useRemoteRevisionId } from '~/stores/remote-latest-page';

+ 24 - 68
packages/app/src/components/PageHistory.tsx

@@ -1,85 +1,41 @@
-import React, { useState, useEffect } from 'react';
+import React from 'react';
 
-import { IRevisionHasPageId } from '@growi/core';
-
-import { useCurrentPageId } from '~/stores/context';
-import { useSWRxPageRevisions, useCurrentPagePath } from '~/stores/page';
+import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 
 import { PageRevisionTable } from './PageHistory/PageRevisionTable';
-import PaginationWrapper from './PaginationWrapper';
-import { RevisionComparer } from './RevisionComparer/RevisionComparer';
 
 const logger = loggerFactory('growi:PageHistory');
 
-export const PageHistory: React.FC<{ onClose: () => void }> = ({ onClose }) => {
+type PageHistoryProps = {
+  sourceRevisionId?: string,
+  targetRevisionId?: string
+  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');
+};
 
-  const [activePage, setActivePage] = useState(1);
+export const PageHistory: React.FC<PageHistoryProps> = (props: PageHistoryProps) => {
+  const { sourceRevisionId, targetRevisionId, onClose } = props;
 
   const { data: currentPageId } = useCurrentPageId();
   const { data: currentPagePath } = useCurrentPagePath();
 
-  const { data: revisionsData, mutate: mutatePageRevisions } = useSWRxPageRevisions(activePage, 10, currentPageId);
-
-  const [sourceRevision, setSourceRevision] = useState<IRevisionHasPageId>();
-  const [targetRevision, setTargetRevision] = useState<IRevisionHasPageId>();
-
-  useEffect(() => {
-    if (revisionsData != null) {
-      setSourceRevision(revisionsData.revisions[0]);
-      setTargetRevision(revisionsData.revisions[0]);
-    }
-  }, [revisionsData]);
-
-  useEffect(() => {
-    mutatePageRevisions();
-  });
-
-  const pagingLimit = 10;
-
-  if (revisionsData == null || sourceRevision == null || targetRevision == null || currentPageId == null || currentPagePath == null) {
-    return (
-      <div className="text-muted text-center">
-        <i className="fa fa-2x fa-spinner fa-pulse mt-3"></i>
-      </div>
-    );
-  }
-
-  const pager = () => {
-    return (
-      <PaginationWrapper
-        activePage={activePage}
-        changePage={setActivePage}
-        totalItemsCount={revisionsData.totalCounts}
-        pagingLimit={pagingLimit}
-        align="center"
-      />
-    );
-  };
-
   return (
     <div className="revision-history" data-testid="page-history">
-      <PageRevisionTable
-        revisions={revisionsData.revisions}
-        pagingLimit={pagingLimit}
-        sourceRevision={sourceRevision}
-        targetRevision={targetRevision}
-        currentPageId={currentPageId}
-        currentPagePath={currentPagePath}
-        onChangeSourceInvoked={setSourceRevision}
-        onChangeTargetInvoked={setTargetRevision}
-        onClose={onClose}
-      />
-      <div className="my-3">
-        {pager()}
-      </div>
-      <RevisionComparer
-        sourceRevision={sourceRevision}
-        targetRevision={targetRevision}
-        currentPageId={currentPageId}
-        currentPagePath={currentPagePath}
-        onClose={onClose}
-      />
+      {currentPageId != null && currentPagePath != null && (
+        <PageRevisionTable
+          sourceRevisionId={sourceRevisionId}
+          targetRevisionId={targetRevisionId}
+          currentPageId={currentPageId}
+          currentPagePath={currentPagePath}
+          onClose={onClose}
+        />
+      )}
     </div>
   );
 };

+ 128 - 49
packages/app/src/components/PageHistory/PageRevisionTable.tsx

@@ -1,37 +1,110 @@
-import React from 'react';
+import React, {
+  useEffect, useRef, useState,
+} from 'react';
 
-import { IRevisionHasId } from '@growi/core';
+import { IRevisionHasId, IRevisionHasPageId } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 
+import { useSWRxInfinitePageRevisions } from '~/stores/page';
+
+import { RevisionComparer } from '../RevisionComparer/RevisionComparer';
+
 import { Revision } from './Revision';
 
 import styles from './PageRevisionTable.module.scss';
 
-type PageRevisionTAble = {
-  revisions: IRevisionHasId[],
-  pagingLimit: number,
-  sourceRevision: IRevisionHasId,
-  targetRevision: IRevisionHasId,
-  currentPageId: string,
-  currentPagePath: string,
-  onChangeSourceInvoked: React.Dispatch<React.SetStateAction<IRevisionHasId | undefined>>,
-  onChangeTargetInvoked: React.Dispatch<React.SetStateAction<IRevisionHasId | undefined>>,
+type PageRevisionTableProps = {
+  sourceRevisionId?: string
+  targetRevisionId?: string
   onClose: () => void,
+  currentPageId: string
+  currentPagePath: string
 }
 
-export const PageRevisionTable = (props: PageRevisionTAble): JSX.Element => {
+export const PageRevisionTable = (props: PageRevisionTableProps): JSX.Element => {
   const { t } = useTranslation();
 
+  const REVISIONS_PER_PAGE = 10;
+
   const {
-    revisions, pagingLimit, sourceRevision, targetRevision, currentPageId, currentPagePath,
-    onChangeSourceInvoked, onChangeTargetInvoked, onClose,
+    sourceRevisionId, targetRevisionId, onClose, currentPageId, currentPagePath,
   } = props;
 
-  const revisionCount = revisions.length;
-  const latestRevision = revisions[0];
-  const oldestRevision = revisions[revisions.length - 1];
+  // Load all data if source revision id and target revision id not null
+  const revisionPerPage = (sourceRevisionId != null && targetRevisionId != null) ? 0 : REVISIONS_PER_PAGE;
+  const swrInifiniteResponse = useSWRxInfinitePageRevisions(currentPageId, revisionPerPage);
+
 
-  const renderRow = (revision: IRevisionHasId, previousRevision: IRevisionHasId, latestRevision: IRevisionHasId,
+  const {
+    data, size, error, setSize, isValidating,
+  } = swrInifiniteResponse;
+
+  const revisions = data && data[0].revisions;
+  const oldestRevision = revisions && revisions[revisions.length - 1];
+
+  // First load
+  const isLoadingInitialData = !data && !error;
+  const isLoadingMore = isLoadingInitialData
+    || (isValidating && data != null && typeof data[size - 1] === 'undefined');
+  const isReachingEnd = (revisionPerPage === 0) || !!(data != null && data[data.length - 1]?.revisions.length < REVISIONS_PER_PAGE);
+
+  const [sourceRevision, setSourceRevision] = useState<IRevisionHasPageId>();
+  const [targetRevision, setTargetRevision] = useState<IRevisionHasPageId>();
+
+  const tbodyRef = useRef<HTMLTableSectionElement>(null);
+
+
+  useEffect(() => {
+    if (revisions != null) {
+      if (sourceRevisionId != null && targetRevisionId != null) {
+        const sourceRevision = revisions.filter(revision => revision._id === sourceRevisionId)[0];
+        const targetRevision = revisions.filter(revision => revision._id === targetRevisionId)[0];
+        setSourceRevision(sourceRevision);
+        setTargetRevision(targetRevision);
+      }
+      else {
+        const latestRevision = revisions != null ? revisions[0] : null;
+        if (latestRevision != null) {
+          setSourceRevision(latestRevision);
+          setTargetRevision(latestRevision);
+        }
+      }
+    }
+  }, [revisions, sourceRevisionId, targetRevisionId]);
+
+  useEffect(() => {
+    // Apply ref to tbody
+    const tbody = tbodyRef.current;
+    const handleScroll = () => {
+      const offset = 30; // Threshold before scroll actually reaching the end
+      if (tbody) {
+        // Scroll end
+        const isEnd = tbody.scrollTop + tbody.clientHeight + offset >= tbody.scrollHeight;
+        if (isEnd && !isLoadingMore && !isReachingEnd) {
+          setSize(size + 1);
+        }
+      }
+    };
+    if (tbody) {
+      tbody.addEventListener('scroll', handleScroll);
+    }
+    return () => {
+      if (tbody) {
+        tbody.removeEventListener('scroll', handleScroll);
+      }
+    };
+  }, [isLoadingMore, isReachingEnd, setSize, size]);
+
+
+  const onChangeSourceInvoked: React.Dispatch<React.SetStateAction<IRevisionHasId | undefined>> = (revision: IRevisionHasPageId) => {
+    setSourceRevision(revision);
+  };
+  const onChangeTargetInvoked: React.Dispatch<React.SetStateAction<IRevisionHasId | undefined>> = (revision: IRevisionHasPageId) => {
+    setTargetRevision(revision);
+  };
+
+
+  const renderRow = (revision: IRevisionHasPageId, previousRevision: IRevisionHasPageId, latestRevision: IRevisionHasPageId,
       isOldestRevision: boolean, hasDiff: boolean) => {
 
     const revisionId = revision._id;
@@ -52,10 +125,10 @@ export const PageRevisionTable = (props: PageRevisionTAble): JSX.Element => {
           <div className="d-lg-flex">
             <Revision
               revision={revision}
-              currentPageId={currentPageId}
-              currentPagePath={currentPagePath}
               isLatestRevision={revision === latestRevision}
               hasDiff={hasDiff}
+              currentPageId={currentPageId}
+              currentPagePath={currentPagePath}
               key={`revision-history-rev-${revisionId}`}
               onClose={onClose}
             />
@@ -118,36 +191,42 @@ export const PageRevisionTable = (props: PageRevisionTAble): JSX.Element => {
     );
   };
 
-  const revisionList = revisions.map((revision, idx) => {
-    // Returns null because the last revision is for the bottom diff display
-    if (idx === pagingLimit) {
-      return null;
-    }
-
-    // if it is the first revision, show full text as diff text
-    const previousRevision = (idx + 1 < revisionCount) ? revisions[idx + 1] : revision;
-
-    const isOldestRevision = revision === oldestRevision;
-
-    // set 'true' if undefined for backward compatibility
-    const hasDiff = revision.hasDiffToPrev !== false;
-
-    return renderRow(revision, previousRevision, latestRevision, isOldestRevision, hasDiff);
-  });
-
   return (
-    <table className={`${styles['revision-history-table']} table revision-history-table`}>
-      <thead>
-        <tr className="d-flex">
-          <th className="col">{t('page_history.revision')}</th>
-          <th className="col-1">{t('page_history.comparing_source')}</th>
-          <th className="col-2">{t('page_history.comparing_target')}</th>
-        </tr>
-      </thead>
-      <tbody className="overflow-auto d-block">
-        {revisionList}
-      </tbody>
-    </table>
+    <>
+      <table className={`${styles['revision-history-table']} table revision-history-table`}>
+        <thead>
+          <tr className="d-flex">
+            <th className="col">{t('page_history.revision')}</th>
+            <th className="col-1">{t('page_history.comparing_source')}</th>
+            <th className="col-2">{t('page_history.comparing_target')}</th>
+          </tr>
+        </thead>
+        <tbody className="overflow-auto d-block" ref={tbodyRef}>
+          {revisions != null && data != null && data.map(apiResult => apiResult.revisions).flat()
+            .map((revision, idx) => {
+              const previousRevision = (idx + 1 < revisions?.length) ? revisions[idx + 1] : revision;
+
+              const isOldestRevision = revision === oldestRevision;
+              const latestRevision = revisions[0];
+
+              // set 'true' if undefined for backward compatibility
+              const hasDiff = revision.hasDiffToPrev !== false;
+              return renderRow(revision, previousRevision, latestRevision, isOldestRevision, hasDiff);
+            })
+          }
+        </tbody>
+      </table>
+
+      {sourceRevision != null && targetRevision != null && (
+        <RevisionComparer
+          sourceRevision={sourceRevision}
+          targetRevision={targetRevision}
+          currentPageId={currentPageId}
+          currentPagePath={currentPagePath}
+          onClose={onClose}
+        />)
+      }
+    </>
   );
 
 };

+ 3 - 3
packages/app/src/components/PageHistory/Revision.tsx

@@ -13,10 +13,10 @@ import styles from './Revision.module.scss';
 
 type RevisionProps = {
   revision: IRevisionHasId,
-  currentPageId: string,
-  currentPagePath: string,
   isLatestRevision: boolean,
   hasDiff: boolean,
+  currentPageId: string
+  currentPagePath: string
   onClose: () => void,
 }
 
@@ -24,7 +24,7 @@ export const Revision = (props: RevisionProps): JSX.Element => {
   const { t } = useTranslation();
 
   const {
-    revision, currentPageId, currentPagePath, isLatestRevision, hasDiff, onClose,
+    revision, isLatestRevision, hasDiff, onClose, currentPageId, currentPagePath,
   } = props;
 
   const { returnPathForURL } = pathUtils;

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

@@ -3,7 +3,7 @@ import React, { FC } from 'react';
 import { DevidedPagePath, pagePathUtils } from '@growi/core';
 import dynamic from 'next/dynamic';
 
-import { useIsNotFound } from '~/stores/context';
+import { useIsNotFound } from '~/stores/page';
 
 import LinkedPagePath from '../models/linked-page-path';
 

+ 2 - 1
packages/app/src/components/RevisionComparer/RevisionComparer.tsx

@@ -32,11 +32,12 @@ export const RevisionComparer = (props: RevisionComparerProps): JSX.Element => {
   const { t } = useTranslation(['translation', 'commons']);
 
   const {
-    sourceRevision, targetRevision, currentPageId, currentPagePath, onClose,
+    sourceRevision, targetRevision, onClose, currentPageId, currentPagePath,
   } = props;
 
   const [dropdownOpen, setDropdownOpen] = useState(false);
 
+
   const toggleDropdown = () => {
     setDropdownOpen(!dropdownOpen);
   };

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

@@ -11,9 +11,9 @@ import {
 
 import { IPageGrantData } from '~/interfaces/page';
 import {
-  useIsEditable, useCurrentPageId, useIsAclEnabled,
+  useIsEditable, useIsAclEnabled,
 } from '~/stores/context';
-import { useCurrentPagePath } from '~/stores/page';
+import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
 import { useSelectedGrant } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 

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

@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
 
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Delete } from '~/client/util/apiv3-client';
-import { useCurrentPageId } from '~/stores/context';
+import { useCurrentPageId } from '~/stores/page';
 import { useSWRxSharelink } from '~/stores/share-link';
 
 import { ShareLinkForm } from './ShareLinkForm';

+ 1 - 1
packages/app/src/components/ShareLink/ShareLinkForm.tsx

@@ -8,7 +8,7 @@ import { useTranslation } from 'next-i18next';
 
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Post } from '~/client/util/apiv3-client';
-import { useCurrentPageId } from '~/stores/context';
+import { useCurrentPageId } from '~/stores/page';
 
 
 const ExpirationType = {

+ 1 - 1
packages/app/src/components/ShareLink/ShareLinkPageView.tsx

@@ -6,7 +6,7 @@ import dynamic from 'next/dynamic';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { IShareLinkHasId } from '~/interfaces/share-link';
 import { generateSSRViewOptions } from '~/services/renderer/renderer';
-import { useIsNotFound } from '~/stores/context';
+import { useIsNotFound } from '~/stores/page';
 import { useViewOptions } from '~/stores/renderer';
 import { registerGrowiFacade } from '~/utils/growi-facade';
 import loggerFactory from '~/utils/logger';

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

@@ -2,10 +2,8 @@ import React, { FC, memo } from 'react';
 
 import { useTranslation } from 'next-i18next';
 
-import {
-  useCurrentPageId, useTargetAndAncestors, useIsGuestUser,
-} from '~/stores/context';
-import { useCurrentPagePath } from '~/stores/page';
+import { useTargetAndAncestors, useIsGuestUser } from '~/stores/context';
+import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
 import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
 
 import ItemsTree from './PageTree/ItemsTree';

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

@@ -14,8 +14,8 @@ import { useSWRINFxRecentlyUpdated } from '~/stores/page-listing';
 import loggerFactory from '~/utils/logger';
 
 import FormattedDistanceDate from '../FormattedDistanceDate';
+import InfiniteScroll from '../InfiniteScroll';
 
-import InfiniteScroll from './InfiniteScroll';
 import { SidebarHeaderReloadButton } from './SidebarHeaderReloadButton';
 import RecentChangesContentSkeleton from './Skeleton/RecentChangesContentSkeleton';
 

+ 13 - 18
packages/app/src/components/TemplateModal.tsx

@@ -9,6 +9,7 @@ import {
   ModalFooter,
 } from 'reactstrap';
 
+import { useTemplateModal } from '~/stores/modal';
 import { usePreviewOptions } from '~/stores/renderer';
 import { useTemplates } from '~/stores/template';
 
@@ -40,17 +41,10 @@ const TemplateRadioButton = ({ template, onChange, isSelected }: TemplateRadioBu
   );
 };
 
-
-type Props = {
-  isOpen: boolean,
-  onClose: () => void,
-  onSubmit?: (markdown: string) => void,
-}
-
-export const TemplateModal = (props: Props): JSX.Element => {
+export const TemplateModal = (): JSX.Element => {
   const { t } = useTranslation();
 
-  const { isOpen, onClose, onSubmit } = props;
+  const { data: templateModalStatus, close } = useTemplateModal();
 
   const { data: rendererOptions } = usePreviewOptions();
   const { data: templates } = useTemplates();
@@ -58,22 +52,23 @@ export const TemplateModal = (props: Props): JSX.Element => {
   const [selectedTemplate, setSelectedTemplate] = useState<ITemplate>();
 
   const submitHandler = useCallback((template?: ITemplate) => {
-    if (onSubmit == null || template == null) {
-      onClose();
+    if (templateModalStatus == null) { return }
+    if (templateModalStatus.onSubmit == null || template == null) {
+      close();
       return;
     }
 
-    onSubmit(template.markdown);
-    onClose();
-  }, [onClose, onSubmit]);
+    templateModalStatus.onSubmit(template.markdown);
+    close();
+  }, [close, templateModalStatus]);
 
-  if (templates == null) {
+  if (templates == null || templateModalStatus == null) {
     return <></>;
   }
 
   return (
-    <Modal className="link-edit-modal" isOpen={isOpen} toggle={onClose} size="lg" autoFocus={false}>
-      <ModalHeader tag="h4" toggle={onClose} className="bg-primary text-light">
+    <Modal className="link-edit-modal" isOpen={templateModalStatus.isOpened} toggle={close} size="lg" autoFocus={false}>
+      <ModalHeader tag="h4" toggle={close} className="bg-primary text-light">
         Template
       </ModalHeader>
 
@@ -105,7 +100,7 @@ export const TemplateModal = (props: Props): JSX.Element => {
 
       </ModalBody>
       <ModalFooter>
-        <button type="button" className="btn btn-sm btn-outline-secondary mx-1" onClick={onClose}>
+        <button type="button" className="btn btn-sm btn-outline-secondary mx-1" onClick={close}>
           {t('Cancel')}
         </button>
         <button type="submit" className="btn btn-sm btn-primary mx-1" onClick={() => submitHandler(selectedTemplate)} disabled={selectedTemplate == null}>

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

@@ -30,19 +30,20 @@ import type { PageModel, PageDocument } from '~/server/models/page';
 import type { PageRedirectModel } from '~/server/models/page-redirect';
 import {
   useCurrentUser,
-  useIsLatestRevision,
-  useIsForbidden, useIsNotFound, useIsSharedUser,
+  useIsForbidden, useIsSharedUser,
   useIsEnabledStaleNotification, useIsIdenticalPath,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useDisableLinkSharing,
   useDrawioUri, useHackmdUri, useDefaultIndentSize, useIsIndentSizeForced,
   useIsAclEnabled, useIsSearchPage, useTemplateTagData, useTemplateBodyData, useIsEnabledAttachTitleHeader,
-  useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
+  useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPathname,
   useIsSlackConfigured, useRendererConfig,
   useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useIsContainerFluid, useIsNotCreatable,
 } from '~/stores/context';
 import { useEditingMarkdown } from '~/stores/editor';
 import { useHasDraftOnHackmd, usePageIdOnHackmd, useRevisionIdHackmdSynced } from '~/stores/hackmd';
-import { useSWRxCurrentPage, useSWRxIsGrantNormalized } from '~/stores/page';
+import {
+  useSWRxCurrentPage, useSWRxIsGrantNormalized, useCurrentPageId, useIsNotFound, useIsLatestRevision,
+} from '~/stores/page';
 import { useRedirectFrom } from '~/stores/page-redirect';
 import { useRemoteRevisionId } from '~/stores/remote-latest-page';
 import { useSelectedGrant } from '~/stores/ui';
@@ -73,6 +74,7 @@ const GrowiSubNavigationSwitcher = dynamic<GrowiSubNavigationSwitcherProps>(() =
   .then(mod => mod.GrowiSubNavigationSwitcher), { ssr: false });
 const DrawioModal = dynamic(() => import('../components/PageEditor/DrawioModal').then(mod => mod.DrawioModal), { ssr: false });
 const HandsontableModal = dynamic(() => import('../components/PageEditor/HandsontableModal').then(mod => mod.HandsontableModal), { ssr: false });
+const TemplateModal = dynamic(() => import('../components/TemplateModal').then(mod => mod.TemplateModal), { ssr: false });
 const PageStatusAlert = dynamic(() => import('../components/PageStatusAlert').then(mod => mod.PageStatusAlert), { ssr: false });
 
 const logger = loggerFactory('growi:pages:all');
@@ -189,11 +191,9 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   useCsrfToken(props.csrfToken);
 
   // page
-  useIsLatestRevision(props.isLatestRevision);
   useIsContainerFluid(props.isContainerFluid);
   // useOwnerOfCurrentPage(props.pageUser != null ? JSON.parse(props.pageUser) : null);
   useIsForbidden(props.isForbidden);
-  useIsNotFound(props.isNotFound);
   useIsNotCreatable(props.isNotCreatable);
   useRedirectFrom(props.redirectFrom ?? null);
   useIsSharedUser(false); // this page cann't be routed for '/share'
@@ -233,14 +233,18 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   const pagePath = pageWithMeta?.data.path ?? props.currentPathname;
   const revisionBody = pageWithMeta?.data.revision?.body;
 
-  useCurrentPageId(pageId ?? null);
   usePageIdOnHackmd(pageWithMeta?.data.pageIdOnHackmd);
   useHasDraftOnHackmd(pageWithMeta?.data.hasDraftOnHackmd ?? false);
   useCurrentPathname(props.currentPathname);
 
   useSWRxCurrentPage(pageWithMeta?.data ?? null); // store initial data
 
+  const { mutate: mutateIsNotFound } = useIsNotFound();
+
+  const { mutate: mutateCurrentPageId } = useCurrentPageId();
+
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
+  const { mutate: mutateIsLatestRevision } = useIsLatestRevision();
 
   const { data: grantData } = useSWRxIsGrantNormalized(pageId);
   const { mutate: mutateSelectedGrant } = useSelectedGrant();
@@ -285,6 +289,18 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
     mutateRevisionIdHackmdSynced(pageWithMeta?.data.revisionHackmdSynced);
   }, [mutateRemoteRevisionId, mutateRevisionIdHackmdSynced, pageWithMeta?.data.revision?._id, pageWithMeta?.data.revisionHackmdSynced]);
 
+  useEffect(() => {
+    mutateCurrentPageId(pageId ?? null);
+  }, [mutateCurrentPageId, pageId]);
+
+  useEffect(() => {
+    mutateIsNotFound(props.isNotFound);
+  }, [mutateIsNotFound, props.isNotFound]);
+
+  useEffect(() => {
+    mutateIsLatestRevision(props.isLatestRevision);
+  }, [mutateIsLatestRevision, props.isLatestRevision]);
+
   const title = generateCustomTitleForPage(props, pagePath);
 
   return (
@@ -353,6 +369,7 @@ Page.getLayout = function getLayout(page: React.ReactElement<Props>) {
       <DescendantsPageListModal />
       <DrawioModal />
       <HandsontableModal />
+      <TemplateModal />
     </>
   );
 };

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

@@ -19,9 +19,10 @@ import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { IShareLinkHasId } from '~/interfaces/share-link';
 import type { PageDocument } from '~/server/models/page';
 import {
-  useCurrentUser, useCurrentPageId, useRendererConfig, useIsSearchPage, useCurrentPathname, useIsNotFound,
+  useCurrentUser, useRendererConfig, useIsSearchPage, useCurrentPathname,
   useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault, useDrawioUri, useIsContainerFluid,
 } from '~/stores/context';
+import { useCurrentPageId, useIsNotFound } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 
 import type { NextPageWithLayout } from '../_app.page';

+ 2 - 1
packages/app/src/pages/trash.page.tsx

@@ -9,11 +9,12 @@ import Head from 'next/head';
 import { GrowiSubNavigation } from '~/components/Navbar/GrowiSubNavigation';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
+import { useCurrentPageId } from '~/stores/page';
 import { useDrawerMode } from '~/stores/ui';
 
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import {
-  useCurrentUser, useCurrentPageId, useCurrentPathname,
+  useCurrentUser, useCurrentPathname,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
   useIsSearchScopeChildrenAsDefault, useIsSearchPage, useShowPageLimitationXL, useIsGuestUser, useRendererConfig,
 } from '../stores/context';

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

@@ -871,8 +871,12 @@ module.exports = (crowi) => {
     }
 
     // run delete
+    const activityParameters = {
+      ip: req.ip,
+      endpoint: req.originalUrl,
+    };
     const options = { isCompletely, isRecursively };
-    crowi.pageService.deleteMultiplePages(pagesCanBeDeleted, req.user, options);
+    crowi.pageService.deleteMultiplePages(pagesCanBeDeleted, req.user, options, activityParameters);
 
     return res.apiv3({ paths: pagesCanBeDeleted.map(p => p.path), isRecursively, isCompletely });
   });

+ 21 - 10
packages/app/src/server/routes/apiv3/revisions.js

@@ -72,7 +72,7 @@ module.exports = (crowi) => {
   const validator = {
     retrieveRevisions: [
       query('pageId').isMongoId().withMessage('pageId is required'),
-      query('page').isInt({ min: 0 }).withMessage('page must be int'),
+      query('offset').if(value => value != null).isInt({ min: 0 }).withMessage('offset must be int'),
       query('limit').if(value => value != null).isInt({ max: 100 }).withMessage('You should set less than 100 or not to set limit.'),
 
     ],
@@ -114,8 +114,7 @@ module.exports = (crowi) => {
     const pageId = req.query.pageId;
     const limit = req.query.limit || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS') || 10;
     const { isSharedPage } = req;
-
-    const selectedPage = parseInt(req.query.page) || 1;
+    const offset = req.query.offset || 0;
 
     // check whether accessible
     if (!isSharedPage && !(await Page.isAccessiblePageByViewer(pageId, req.user))) {
@@ -124,15 +123,21 @@ module.exports = (crowi) => {
 
     try {
       const page = await Page.findOne({ _id: pageId });
+      const queryOpts = {
+        offset,
+        sort: { createdAt: -1 },
+        populate: 'author',
+        pagination: false,
+      };
+
+      if (limit > 0) {
+        queryOpts.limit = limit;
+        queryOpts.pagination = true;
+      }
 
       const paginateResult = await Revision.paginate(
         { pageId: page._id },
-        {
-          page: selectedPage,
-          limit,
-          sort: { createdAt: -1 },
-          populate: 'author',
-        },
+        queryOpts,
       );
 
       paginateResult.docs.forEach((doc) => {
@@ -141,7 +146,13 @@ module.exports = (crowi) => {
         }
       });
 
-      return res.apiv3(paginateResult);
+      const result = {
+        revisions: paginateResult.docs,
+        totalCount: paginateResult.totalDocs,
+        offset: paginateResult.offset,
+      };
+
+      return res.apiv3(result);
     }
     catch (err) {
       const msg = 'Error occurred in getting revisions by poge id';

+ 5 - 2
packages/app/src/server/routes/attachment.js

@@ -1,4 +1,3 @@
-
 import { SupportedAction } from '~/interfaces/activity';
 import { AttachmentType } from '~/server/interfaces/attachment';
 import loggerFactory from '~/utils/logger';
@@ -452,6 +451,7 @@ module.exports = function(crowi, app) {
   api.add = async function(req, res) {
     let pageId = req.body.page_id || null;
     const pagePath = req.body.path || null;
+    const pageBody = req.body.page_body || null;
     let pageCreated = false;
 
     // check params
@@ -468,7 +468,10 @@ module.exports = function(crowi, app) {
     if (pageId == null) {
       logger.debug('Create page before file upload');
 
-      page = await crowi.pageService.create(pagePath, `# ${pagePath}`, req.user, { grant: Page.GRANT_OWNER });
+      const isAclEnabled = crowi.aclService.isAclEnabled();
+      const grant = isAclEnabled ? Page.GRANT_OWNER : Page.GRANT_PUBLIC;
+
+      page = await crowi.pageService.create(pagePath, pageBody ?? '', req.user, { grant });
       pageCreated = true;
       pageId = page._id;
     }

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

@@ -28,8 +28,8 @@ module.exports = function(crowi, app) {
         subject: `[${appTitle}:admin] A New User Created and Waiting for Activation`,
         template: path.join(crowi.localeDir, 'en_US/admin/userWaitingActivation.txt'),
         vars: {
+          adminUser: admin,
           createdUser: userData,
-          admin,
           url: appService.getSiteUrl(),
           appTitle,
         },

+ 5 - 5
packages/app/src/server/service/page.ts

@@ -1360,7 +1360,7 @@ class PageService {
   /*
    * Delete
    */
-  async deletePage(page, user, options = {}, isRecursively = false, activityParameters?) {
+  async deletePage(page, user, options = {}, isRecursively = false, activityParameters) {
     /*
      * Common Operation
      */
@@ -1719,7 +1719,7 @@ class PageService {
     return;
   }
 
-  async deleteCompletely(page, user, options = {}, isRecursively = false, preventEmitting = false, activityParameters?) {
+  async deleteCompletely(page, user, options = {}, isRecursively = false, preventEmitting = false, activityParameters) {
     /*
      * Common Operation
      */
@@ -1936,7 +1936,7 @@ class PageService {
   }
 
   // no need to separate Main Sub since it is devided into single page operations
-  async deleteMultiplePages(pagesToDelete, user, options): Promise<void> {
+  async deleteMultiplePages(pagesToDelete, user, options, activityParameters): Promise<void> {
     const { isRecursively, isCompletely } = options;
 
     if (pagesToDelete.length > LIMIT_FOR_MULTIPLE_PAGE_OP) {
@@ -1948,12 +1948,12 @@ class PageService {
 
     if (isCompletely) {
       for await (const page of pages) {
-        await this.deleteCompletely(page, user, {}, isRecursively);
+        await this.deleteCompletely(page, user, {}, isRecursively, false, activityParameters);
       }
     }
     else {
       for await (const page of pages) {
-        await this.deletePage(page, user, {}, isRecursively);
+        await this.deletePage(page, user, {}, isRecursively, activityParameters);
       }
     }
   }

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

@@ -1,5 +1,5 @@
-import type { ColorScheme, IUser, IUserHasId } from '@growi/core';
-import { Key, SWRResponse } from 'swr';
+import type { ColorScheme, IUserHasId } from '@growi/core';
+import { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
 import { SupportedActionType } from '~/interfaces/activity';
@@ -44,10 +44,6 @@ export const useCurrentPathname = (initialData?: string): SWRResponse<string, Er
   return useContextSWR('currentPathname', initialData);
 };
 
-export const useCurrentPageId = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
-  return useStaticSWR<Nullable<string>, Error>('currentPageId', initialData);
-};
-
 export const useIsIdenticalPath = (initialData?: boolean): SWRResponse<boolean, Error> => {
   return useContextSWR<boolean, Error>('isIdenticalPath', initialData, { fallbackData: false });
 };
@@ -60,10 +56,6 @@ export const useIsNotCreatable = (initialData?: boolean): SWRResponse<boolean, E
   return useContextSWR<boolean, Error>('isNotCreatable', initialData, { fallbackData: false });
 };
 
-export const useIsNotFound = (initialData?: boolean): SWRResponse<boolean, Error> => {
-  return useContextSWR<boolean, Error>('isNotFound', initialData, { fallbackData: false });
-};
-
 export const useTemplateTagData = (initialData?: string[]): SWRResponse<string[], Error> => {
   return useContextSWR<string[], Error>('templateTagData', initialData);
 };
@@ -161,10 +153,6 @@ export const useIsEnabledStaleNotification = (initialData?: boolean): SWRRespons
   return useContextSWR('isEnabledStaleNotification', initialData);
 };
 
-export const useIsLatestRevision = (initialData?: boolean): SWRResponse<boolean, any> => {
-  return useContextSWR('isLatestRevision', initialData);
-};
-
 export const useEditorConfig = (initialData?: EditorConfig): SWRResponse<EditorConfig, Error> => {
   return useContextSWR<EditorConfig, Error>('editorConfig', initialData);
 };

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

@@ -579,3 +579,32 @@ export const useConflictDiffModal = (): SWRResponse<ConflictDiffModalStatus, Err
     },
   });
 };
+
+
+/*
+ * TemplateModal
+ */
+type TemplateModalStatus = {
+  isOpened: boolean,
+  onSubmit?: (templateText: string) => void
+}
+
+type TemplateModalUtils = {
+  open(onSubmit: (templateText: string) => void): void,
+  close(): void,
+}
+
+export const useTemplateModal = (): SWRResponse<TemplateModalStatus, Error> & TemplateModalUtils => {
+
+  const initialStatus: TemplateModalStatus = { isOpened: false };
+  const swrResponse = useStaticSWR<TemplateModalStatus, Error>('templateModal', undefined, { fallbackData: initialStatus });
+
+  return Object.assign(swrResponse, {
+    open: (onSubmit: (templateText: string) => void) => {
+      swrResponse.mutate({ isOpened: true, onSubmit });
+    },
+    close: () => {
+      swrResponse.mutate({ isOpened: false });
+    },
+  });
+};

+ 39 - 21
packages/app/src/stores/page.tsx

@@ -1,11 +1,12 @@
-import { useEffect, useMemo, useCallback } from 'react';
+import { useEffect, useMemo } from 'react';
 
 import type {
-  IPageInfoForEntity, IPagePopulatedToShowRevision, Nullable,
+  IPageInfoForEntity, IPagePopulatedToShowRevision, Nullable, SWRInfinitePageRevisionsResponse,
 } from '@growi/core';
 import { Ref, isClient, pagePathUtils } from '@growi/core';
 import useSWR, { mutate, SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
+import useSWRInfinite, { SWRInfiniteResponse } from 'swr/infinite';
 import useSWRMutation, { SWRMutationResponse } from 'swr/mutation';
 
 import { apiGet } from '~/client/util/apiv1-client';
@@ -14,17 +15,28 @@ import {
   IPageInfo, IPageInfoForOperation,
 } from '~/interfaces/page';
 import { IRecordApplicableGrant, IResIsGrantNormalized } from '~/interfaces/page-grant';
-import { IRevision, IRevisionHasId, IRevisionsForPagination } from '~/interfaces/revision';
+import { IRevision, IRevisionHasId } from '~/interfaces/revision';
 
 import { IPageTagsInfo } from '../interfaces/tag';
 
-import {
-  useCurrentPageId, useCurrentPathname, useShareLinkId, useIsGuestUser, useIsNotFound,
-} from './context';
+import { useCurrentPathname, useShareLinkId, useIsGuestUser } from './context';
+import { useStaticSWR } from './use-static-swr';
 
 const { isPermalink: _isPermalink } = pagePathUtils;
 
 
+export const useCurrentPageId = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
+  return useStaticSWR<Nullable<string>, Error>('currentPageId', initialData);
+};
+
+export const useIsLatestRevision = (initialData?: boolean): SWRResponse<boolean, any> => {
+  return useStaticSWR('isLatestRevision', initialData);
+};
+
+export const useIsNotFound = (initialData?: boolean): SWRResponse<boolean, Error> => {
+  return useStaticSWR<boolean, Error>('isNotFound', initialData, { fallbackData: false });
+};
+
 export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision|null): SWRResponse<IPagePopulatedToShowRevision|null> => {
   const key = 'currentPage';
 
@@ -143,22 +155,28 @@ export const useSWRxPageRevision = (pageId: string, revisionId: Ref<IRevision>):
   );
 };
 
-export const useSWRxPageRevisions = (
-    page: number, // page number of pagination
-    limit: number, // max number of pages in one paginate
-    pageId: string | null | undefined,
-): SWRResponse<IRevisionsForPagination, Error> => {
+/*
+ * SWR Infinite for page revision list
+ */
 
-  return useSWRImmutable(
-    ['/revisions/list', pageId, page, limit],
-    ([endpoint, pageId, page, limit]) => {
-      return apiv3Get(endpoint, { pageId, page, limit }).then((response) => {
-        const revisions = {
-          revisions: response.data.docs,
-          totalCounts: response.data.totalDocs,
-        };
-        return revisions;
-      });
+export const useSWRxInfinitePageRevisions = (
+    pageId: string,
+    limit: number,
+): SWRInfiniteResponse<SWRInfinitePageRevisionsResponse, Error> => {
+  return useSWRInfinite(
+    (pageIndex, previousRevisionData) => {
+      if (previousRevisionData != null && previousRevisionData.revisions.length === 0) return null;
+
+      if (pageIndex === 0 || previousRevisionData == null) {
+        return ['/revisions/list', pageId, undefined, limit];
+      }
+      const offset = previousRevisionData.offset + limit;
+      return ['/revisions/list', pageId, offset, limit];
+    },
+    ([endpoint, pageId, offset, limit]) => apiv3Get<SWRInfinitePageRevisionsResponse>(endpoint, { pageId, offset, limit }).then(response => response.data),
+    {
+      revalidateFirstPage: true,
+      revalidateAll: false,
     },
   );
 };

+ 5 - 3
packages/app/src/stores/ui.tsx

@@ -19,13 +19,15 @@ import type { IPageGrantData } from '~/interfaces/page';
 import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import { SidebarContentsType } from '~/interfaces/ui';
 import type { UpdateDescCountData } from '~/interfaces/websocket';
+import {
+  useIsNotFound, useCurrentPagePath, useIsTrashPage, useCurrentPageId,
+} from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 
 import {
-  useCurrentPageId, useIsEditable,
-  useIsSharedUser, useIsIdenticalPath, useCurrentUser, useShareLinkId, useIsNotFound,
+  useIsEditable,
+  useIsSharedUser, useIsIdenticalPath, useCurrentUser, useShareLinkId,
 } from './context';
-import { useCurrentPagePath, useIsTrashPage } from './page';
 import { useStaticSWR } from './use-static-swr';
 
 const { isTrashTopPage, isUsersTopPage } = pagePathUtils;

+ 8 - 0
packages/app/src/styles/theme/_apply-colors.scss

@@ -350,6 +350,14 @@ ul.pagination {
   }
 }
 
+.grw-page-accessories-modal,.grw-descendants-page-list-modal {
+  .modal-header {
+    .close {
+      color: #{hsl.contrast(var(--bgcolor-global))};
+    }
+  }
+}
+
 .grw-custom-nav-tab {
   .nav-item {
     &:hover,

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

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

+ 1 - 1
packages/core/package.json

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

+ 6 - 0
packages/core/src/interfaces/revision.ts

@@ -32,3 +32,9 @@ export type IRevisionOnConflict = {
 export type HasRevisionShortbody = {
   revisionShortBody?: string,
 }
+
+export type SWRInfinitePageRevisionsResponse = {
+  revisions: IRevisionHasPageId[],
+  totalCount: number,
+  offset: number,
+}

+ 1 - 1
packages/hackmd/package.json

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

+ 2 - 2
packages/presentation/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/presentation",
-  "version": "6.0.7",
+  "version": "6.0.8-RC.0",
   "description": "GROWI plugin for presentation",
   "license": "MIT",
   "keywords": ["growi", "growi-plugin"],
@@ -15,7 +15,7 @@
     "test": ""
   },
   "dependencies": {
-    "@growi/core": "^6.0.7"
+    "@growi/core": "^6.0.8-RC.0"
   },
   "devDependencies": {
     "@marp-team/marp-core": "^3.4.2",

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

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

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

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

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

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

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

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

+ 1 - 1
packages/slack/package.json

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

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

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

+ 2 - 2
packages/ui/package.json

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