Procházet zdrojové kódy

Merge branch 'master' into fix/114374-114406-disable-to-show-password-reset-info

jam411 před 3 roky
rodič
revize
0efa266819
72 změnil soubory, kde provedl 599 přidání a 592 odebrání
  1. 1 1
      packages/app/package.json
  2. 1 1
      packages/app/public/static/locales/en_US/commons.json
  3. 1 1
      packages/app/public/static/locales/ja_JP/commons.json
  4. 1 1
      packages/app/public/static/locales/zh_CN/commons.json
  5. 2 2
      packages/app/src/client/services/page-operation.ts
  6. 2 2
      packages/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts
  7. 2 2
      packages/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts
  8. 1 1
      packages/app/src/client/util/toastr.ts
  9. 9 8
      packages/app/src/components/Admin/App/ConfirmModal.tsx
  10. 3 3
      packages/app/src/components/Admin/Common/AdminNavigation.jsx
  11. 1 2
      packages/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx
  12. 1 2
      packages/app/src/components/Admin/UserGroup/UserGroupForm.tsx
  13. 1 2
      packages/app/src/components/Admin/UserGroup/UserGroupModal.tsx
  14. 1 2
      packages/app/src/components/Admin/UserGroup/UserGroupTable.tsx
  15. 1 1
      packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.tsx
  16. 13 46
      packages/app/src/components/DescendantsPageList.tsx
  17. 2 8
      packages/app/src/components/DescendantsPageListModal.tsx
  18. 0 11
      packages/app/src/components/Layout/BasicLayout.tsx
  19. 1 1
      packages/app/src/components/Me/BasicInfoSettings.tsx
  20. 2 2
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  21. 0 3
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  22. 1 2
      packages/app/src/components/NotAvailable.tsx
  23. 11 4
      packages/app/src/components/NotFoundPage.tsx
  24. 8 26
      packages/app/src/components/Page/PageContents.tsx
  25. 4 4
      packages/app/src/components/Page/PageView.tsx
  26. 1 3
      packages/app/src/components/Page/RevisionLoader.tsx
  27. 1 1
      packages/app/src/components/PageAlert/TrashPageAlert.tsx
  28. 10 10
      packages/app/src/components/PageEditor.tsx
  29. 5 3
      packages/app/src/components/PageEditor/EditorNavbarBottom.tsx
  30. 11 10
      packages/app/src/components/PageEditorByHackmd.tsx
  31. 3 2
      packages/app/src/components/PageStatusAlert.tsx
  32. 8 8
      packages/app/src/components/PrivateLegacyPages.tsx
  33. 2 4
      packages/app/src/components/PutbackPageModal.jsx
  34. 1 2
      packages/app/src/components/SearchPage.tsx
  35. 2 2
      packages/app/src/components/SearchPage/SearchControl.tsx
  36. 2 5
      packages/app/src/components/SearchPage/SearchPageBase.tsx
  37. 14 19
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  38. 15 19
      packages/app/src/components/SearchPage/SearchResultList.tsx
  39. 1 7
      packages/app/src/components/ShareLink/ShareLinkPageContents.tsx
  40. 17 21
      packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx
  41. 8 2
      packages/app/src/components/Sidebar/SidebarNav.tsx
  42. 2 1
      packages/app/src/components/StickyStretchableScroller.tsx
  43. 24 6
      packages/app/src/components/TrashPageList.tsx
  44. 21 8
      packages/app/src/pages/[[...path]].page.tsx
  45. 2 12
      packages/app/src/pages/_app.page.tsx
  46. 5 5
      packages/app/src/pages/trash.page.tsx
  47. 1 1
      packages/app/src/pages/utils/commons.ts
  48. 17 4
      packages/app/src/server/routes/apiv3/users.js
  49. 1 1
      packages/app/src/stores/activity.ts
  50. 1 1
      packages/app/src/stores/attachment.tsx
  51. 1 1
      packages/app/src/stores/comment.tsx
  52. 8 8
      packages/app/src/stores/context.tsx
  53. 4 4
      packages/app/src/stores/editor.tsx
  54. 2 2
      packages/app/src/stores/in-app-notification.ts
  55. 1 1
      packages/app/src/stores/maintenanceMode.tsx
  56. 1 2
      packages/app/src/stores/middlewares/user.ts
  57. 70 61
      packages/app/src/stores/page-listing.tsx
  58. 77 68
      packages/app/src/stores/page.tsx
  59. 49 44
      packages/app/src/stores/renderer.tsx
  60. 10 14
      packages/app/src/stores/search.tsx
  61. 4 1
      packages/app/src/stores/share-link.tsx
  62. 2 2
      packages/app/src/stores/tag.tsx
  63. 1 1
      packages/app/src/stores/template.tsx
  64. 27 21
      packages/app/src/stores/ui.tsx
  65. 2 2
      packages/app/src/stores/use-context-swr.tsx
  66. 0 25
      packages/app/src/stores/use-static-swr.tsx
  67. 11 11
      packages/app/src/stores/user-group.tsx
  68. 2 2
      packages/app/src/stores/user.tsx
  69. 3 1
      packages/app/src/stores/xss.ts
  70. 12 8
      packages/app/src/utils/swr-utils.ts
  71. 55 0
      packages/app/test/cypress/integration/20-basic-features/20-basic-features--access-to-page.spec.ts
  72. 12 18
      yarn.lock

+ 1 - 1
packages/app/package.json

@@ -185,7 +185,7 @@
     "string-width": "=4.2.2",
     "superjson": "^1.9.1",
     "swagger-jsdoc": "^6.1.0",
-    "swr": "^1.3.0",
+    "swr": "^2.0.2",
     "throttle-debounce": "^3.0.1",
     "toastr": "^2.1.2",
     "uglifycss": "^0.0.29",

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

@@ -100,7 +100,7 @@
 
   "g2g_data_transfer": {
     "tab": "Data transfer",
-    "data_transfer": "GROWI To GROWI Data Transfer",
+    "data_transfer": "Data Transfer",
     "transfer_data_to_this_growi": "Transfer data from another GROWI to this GROWI",
     "publish_transfer_key": "Publish transfer key",
     "transfer_key_limit": "Transfer keys are valid for 1 hour after issuance.",

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

@@ -100,7 +100,7 @@
 
   "g2g_data_transfer": {
     "tab": "データ移行",
-    "data_transfer": "別GROWIとのデータ移行",
+    "data_transfer": "データ移行",
     "transfer_data_to_this_growi": "別GROWIのデータをこのGROWIへ移行する",
     "publish_transfer_key": "移行キーを発行する",
     "transfer_key_limit": "※ 移行キーの有効期限は発行から1時間となります。",

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

@@ -100,7 +100,7 @@
 
   "g2g_data_transfer": {
     "tab": "数据迁移",
-    "data_transfer": "与另一个GROWI的数据转移",
+    "data_transfer": "数据迁移",
     "transfer_data_to_this_growi": "将数据从另一个GROWI迁移到这个GROWI上",
     "publish_transfer_key": "发布迁移密钥",
     "transfer_key_limit": "迁移密钥在签发后一小时内有效。",

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

@@ -6,7 +6,7 @@ import urljoin from 'url-join';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { useCurrentPageId } from '~/stores/context';
 import { useEditingMarkdown, useIsEnabledUnsavedWarning, usePageTagsForEditors } from '~/stores/editor';
-import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
+import { useSWRMUTxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import loggerFactory from '~/utils/logger';
 
@@ -179,7 +179,7 @@ export const useSaveOrUpdate = (): SaveOrUpdateFunction => {
 
 export const useUpdateStateAfterSave = (pageId: string|undefined|null): (() => Promise<void>) | undefined => {
   const { mutate: mutateCurrentPageId } = useCurrentPageId();
-  const { mutate: mutateCurrentPage } = useSWRxCurrentPage();
+  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
   const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
   const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
   const { sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);

+ 2 - 2
packages/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts

@@ -23,7 +23,7 @@ declare global {
 
 
 export const useDrawioModalLauncherForView = (opts?: {
-  onSaveSuccess?: (newMarkdown: string) => void,
+  onSaveSuccess?: () => void,
   onSaveError?: (error: any) => void,
 }): void => {
 
@@ -61,7 +61,7 @@ export const useDrawioModalLauncherForView = (opts?: {
         optionsToSave,
       );
 
-      opts?.onSaveSuccess?.(newMarkdown);
+      opts?.onSaveSuccess?.();
     }
     catch (error) {
       logger.error('failed to save', error);

+ 2 - 2
packages/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts

@@ -22,7 +22,7 @@ declare global {
 
 
 export const useHandsontableModalLauncherForView = (opts?: {
-  onSaveSuccess?: (newMarkdown: string) => void,
+  onSaveSuccess?: () => void,
   onSaveError?: (error: any) => void,
 }): void => {
 
@@ -60,7 +60,7 @@ export const useHandsontableModalLauncherForView = (opts?: {
         optionsToSave,
       );
 
-      opts?.onSaveSuccess?.(newMarkdown);
+      opts?.onSaveSuccess?.();
     }
     catch (error) {
       logger.error('failed to save', error);

+ 1 - 1
packages/app/src/client/util/toastr.ts

@@ -17,7 +17,7 @@ export const toastError = (err: string | Error | Error[], option: ToastOptions =
 
   for (const err of errs) {
     const message = (typeof err === 'string') ? err : err.message;
-    toast.error(message || err, option);
+    toast.error(message, option);
   }
 };
 

+ 9 - 8
packages/app/src/components/Admin/App/ConfirmModal.tsx

@@ -1,15 +1,15 @@
 import React, { FC } from 'react';
+
+import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
-import { useTranslation } from 'next-i18next';
-import { TFunctionResult } from 'i18next';
 
 type ConfirmModalProps = {
   isModalOpen: boolean
-  warningMessage: TFunctionResult
-  supplymentaryMessage: TFunctionResult | null
-  confirmButtonTitle: TFunctionResult
+  warningMessage: string
+  supplymentaryMessage: string | null
+  confirmButtonTitle: string
   onConfirm?: () => Promise<void>
   onCancel?: () => void
 };
@@ -43,13 +43,14 @@ export const ConfirmModal: FC<ConfirmModalProps> = (props: ConfirmModalProps) =>
               <br />
               <br />
               <span className="text-warning">
-                <i className="icon-exclamation icon-fw"></i>
-                {props.supplymentaryMessage}
+                <>
+                  <i className="icon-exclamation icon-fw"></i>
+                  {props.supplymentaryMessage}
+                </>
               </span>
             </>
           )
         }
-
       </ModalBody>
       <ModalFooter>
         <button

+ 3 - 3
packages/app/src/components/Admin/Common/AdminNavigation.jsx

@@ -29,15 +29,15 @@ const AdminNavigation = (props) => {
       case 'customize':                return <><i className="mr-1 icon-fw icon-wrench"></i>{          t('customize_settings.customize_settings') }</>;
       case 'importer':                 return <><i className="mr-1 icon-fw icon-cloud-upload"></i>{    t('importer_management.import_data') }</>;
       case 'export':                   return <><i className="mr-1 icon-fw icon-cloud-download"></i>{  t('export_management.export_archive_data') }</>;
+      case 'data-transfer':            return <><i className="mr-1 icon-fw icon-plane"></i>{           t('g2g_data_transfer.data_transfer', { ns: 'commons' })}</>;
       case 'notification':             return <><i className="mr-1 icon-fw icon-bell"></i>{            t('external_notification.external_notification')}</>;
       case 'slack-integration':        return <><i className="mr-1 icon-fw icon-shuffle"></i>{         t('slack_integration.slack_integration') }</>;
       case 'slack-integration-legacy': return <><i className="mr-1 icon-fw icon-shuffle"></i>{         t('slack_integration_legacy.slack_integration_legacy')}</>;
       case 'users':                    return <><i className="mr-1 icon-fw icon-user"></i>{            t('user_management.user_management') }</>;
       case 'user-groups':              return <><i className="mr-1 icon-fw icon-people"></i>{          t('user_group_management.user_group_management') }</>;
-      case 'search':                   return <><i className="mr-1 icon-fw icon-magnifier"></i>{       t('full_text_search_management.full_text_search_management') }</>;
       case 'audit-log':                return <><i className="mr-1 icon-fw icon-feed"></i>{            t('audit_log_management.audit_log')}</>;
-      case 'data-transfer':            return <><i className="mr-1 icon-fw icon-arrow-right"></i>{     t('g2g_data_transfer.data_transfer', { ns: 'commons' })}</>;
       case 'plugins':                  return <><i className="mr-1 icon-fw icon-puzzle"></i>{          t('plugins.plugins')}</>;
+      case 'search':                   return <><i className="mr-1 icon-fw icon-magnifier"></i>{       t('full_text_search_management.full_text_search_management') }</>;
       case 'cloud':                    return <><i className="mr-1 icon-fw icon-share-alt"></i>{       t('cloud_setting_management.to_cloud_settings')} </>;
       default:                         return <><i className="mr-1 icon-fw icon-home"></i>{            t('wiki_management_home_page') }</>;
       /* eslint-enable no-multi-spaces, max-len */
@@ -87,6 +87,7 @@ const AdminNavigation = (props) => {
         <MenuLink menu="customize"    isListGroupItems isActive={isActiveMenu('/customize')} />
         <MenuLink menu="importer"     isListGroupItems isActive={isActiveMenu('/importer')} />
         <MenuLink menu="export"       isListGroupItems isActive={isActiveMenu('/export')} />
+        <MenuLink menu="data-transfer" isListGroupItems isActive={isActiveMenu('/data-transfer')} />
         <MenuLink menu="notification" isListGroupItems isActive={isActiveMenu('/notification') || isActiveMenu('/global-notification')} />
         <MenuLink menu="slack-integration" isListGroupItems isActive={isActiveMenu('/slack-integration')} />
         <MenuLink menu="slack-integration-legacy" isListGroupItems isActive={isActiveMenu('/slack-integration-legacy')} />
@@ -94,7 +95,6 @@ const AdminNavigation = (props) => {
         <MenuLink menu="user-groups"  isListGroupItems isActive={isActiveMenu('/user-groups')} />
         <MenuLink menu="audit-log"    isListGroupItems isActive={isActiveMenu('/audit-log')} />
         <MenuLink menu="plugins"      isListGroupItems isActive={isActiveMenu('/plugins')} />
-        <MenuLink menu="data-transfer" isListGroupItems isActive={isActiveMenu('/data-transfer')} />
         <MenuLink menu="search"       isListGroupItems isActive={isActiveMenu('/search')} />
         {growiCloudUri != null && growiAppIdForGrowiCloud != null
           && (

+ 1 - 2
packages/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx

@@ -2,7 +2,6 @@ import React, {
   FC, useCallback, useState, useMemo,
 } from 'react';
 
-import { TFunctionResult } from 'i18next';
 import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
@@ -30,7 +29,7 @@ type AvailableOption = {
   actionForPages: string,
   iconClass: string,
   styleClass: string,
-  label: TFunctionResult,
+  label: string,
 };
 
 // actionName master constants

+ 1 - 2
packages/app/src/components/Admin/UserGroup/UserGroupForm.tsx

@@ -1,7 +1,6 @@
 import React, { FC, useCallback, useState } from 'react';
 
 import dateFnsFormat from 'date-fns/format';
-import { TFunctionResult } from 'i18next';
 import { useTranslation } from 'next-i18next';
 
 import { IUserGroupHasId } from '~/interfaces/user';
@@ -9,7 +8,7 @@ import { IUserGroupHasId } from '~/interfaces/user';
 type Props = {
   userGroup: IUserGroupHasId,
   selectableParentUserGroups?: IUserGroupHasId[],
-  submitButtonLabel: TFunctionResult;
+  submitButtonLabel: string;
   onSubmit?: (targetGroup: IUserGroupHasId, userGroupData: Partial<IUserGroupHasId>) => Promise<void> | void
 };
 

+ 1 - 2
packages/app/src/components/Admin/UserGroup/UserGroupModal.tsx

@@ -3,7 +3,6 @@ import React, {
 } from 'react';
 
 import { Ref } from '@growi/core';
-import { TFunctionResult } from 'i18next';
 import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
@@ -13,7 +12,7 @@ import { IUserGroup, IUserGroupHasId } from '~/interfaces/user';
 
 type Props = {
   userGroup?: IUserGroupHasId,
-  buttonLabel?: TFunctionResult,
+  buttonLabel?: string,
   onClickSubmit?: (userGroupData: Partial<IUserGroupHasId>) => Promise<IUserGroupHasId | void>
   isShow?: boolean
   onHide?: () => Promise<void> | void

+ 1 - 2
packages/app/src/components/Admin/UserGroup/UserGroupTable.tsx

@@ -4,12 +4,11 @@ import React, {
 
 import type { IUserGroupHasId, IUserGroupRelation, IUserHasId } from '@growi/core';
 import dateFnsFormat from 'date-fns/format';
-import { TFunctionResult } from 'i18next';
 import { useTranslation } from 'next-i18next';
 
 
 type Props = {
-  headerLabel?: TFunctionResult,
+  headerLabel?: string,
   userGroups: IUserGroupHasId[],
   userGroupRelations: IUserGroupRelation[],
   childUserGroups: IUserGroupHasId[],

+ 1 - 1
packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.tsx

@@ -13,7 +13,7 @@ type Props = {
 }
 
 export const UserGroupUserTable = (props: Props): JSX.Element => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
 
   const {
     userGroupRelations, onClickRemoveUserBtn, onClickPlusBtn,

+ 13 - 46
packages/app/src/components/DescendantsPageList.tsx

@@ -11,15 +11,14 @@ import {
 import { IPagingResult } from '~/interfaces/paging-result';
 import { OnDeletedFunction, OnPutBackedFunction } from '~/interfaces/ui';
 import {
-  useIsGuestUser, useIsSharedUser, useShowPageLimitationXL,
+  useIsGuestUser, useIsSharedUser,
 } from '~/stores/context';
-import { useIsTrashPage } from '~/stores/page';
 import {
-  usePageTreeTermManager, useDescendantsPageListForCurrentPathTermManager, useSWRxDescendantsPageListForCurrrentPath,
+  mutatePageTree,
   useSWRxPageInfoForList, useSWRxPageList,
 } from '~/stores/page-listing';
 
-import { ForceHideMenuItems, MenuItemType } from './Common/Dropdown/PageItemControl';
+import { ForceHideMenuItems } from './Common/Dropdown/PageItemControl';
 import PageList from './PageList/PageList';
 import PaginationWrapper from './PaginationWrapper';
 
@@ -37,7 +36,7 @@ const convertToIDataWithMeta = (page: IPageHasId): IDataWithMeta<IPageHasId> =>
   return { data: page };
 };
 
-export const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element => {
+const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element => {
 
   const { t } = useTranslation();
 
@@ -52,10 +51,6 @@ export const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element
 
   let pageWithMetas: IDataWithMeta<IPageHasId, IPageInfoForOperation>[] = [];
 
-  // for mutation
-  const { advance: advancePt } = usePageTreeTermManager();
-  const { advance: advanceDpl } = useDescendantsPageListForCurrentPathTermManager();
-
   // initial data
   if (pagingResult != null) {
     // convert without meta at first
@@ -74,23 +69,22 @@ export const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element
       toastSuccess(t('deleted_pages_completely', { path }));
     }
 
-    advancePt();
+    mutatePageTree();
 
     if (onPagesDeleted != null) {
       onPagesDeleted(...args);
     }
-  }, [advancePt, onPagesDeleted, t]);
+  }, [onPagesDeleted, t]);
 
   const pagePutBackedHandler: OnPutBackedFunction = useCallback((path) => {
     toastSuccess(t('page_has_been_reverted', { path }));
 
-    advancePt();
-    advanceDpl();
+    mutatePageTree();
 
     if (onPagePutBacked != null) {
       onPagePutBacked(path);
     }
-  }, [advanceDpl, advancePt, onPagePutBacked, t]);
+  }, [onPagePutBacked, t]);
 
   function setPageNumber(selectedPageNumber) {
     setActivePage(selectedPageNumber);
@@ -135,43 +129,18 @@ export const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element
 
 export type DescendantsPageListProps = {
   path: string,
+  limit?: number,
+  forceHideMenuItems?: ForceHideMenuItems,
 }
 
 export const DescendantsPageList = (props: DescendantsPageListProps): JSX.Element => {
-  const { path } = props;
+  const { path, limit, forceHideMenuItems } = props;
 
   const [activePage, setActivePage] = useState(1);
 
   const { data: isSharedUser } = useIsSharedUser();
 
-  const { data: pagingResult, error, mutate } = useSWRxPageList(isSharedUser ? null : path, activePage);
-
-  if (error != null) {
-    return (
-      <div className="my-5">
-        <div className="text-danger">{error.message}</div>
-      </div>
-    );
-  }
-
-  return (
-    <DescendantsPageListSubstance
-      pagingResult={pagingResult}
-      activePage={activePage}
-      setActivePage={setActivePage}
-      onPagesDeleted={() => mutate()}
-      onPagePutBacked={() => mutate()}
-    />
-  );
-};
-
-export const DescendantsPageListForCurrentPath = (): JSX.Element => {
-
-  const [activePage, setActivePage] = useState(1);
-
-  const { data: isTrashPage } = useIsTrashPage();
-  const { data: limit } = useShowPageLimitationXL();
-  const { data: pagingResult, error, mutate } = useSWRxDescendantsPageListForCurrrentPath(activePage, limit);
+  const { data: pagingResult, error, mutate } = useSWRxPageList(isSharedUser ? null : path, activePage, limit);
 
   if (error != null) {
     return (
@@ -181,8 +150,6 @@ export const DescendantsPageListForCurrentPath = (): JSX.Element => {
     );
   }
 
-  const forceHideMenuItems = isTrashPage ? [MenuItemType.RENAME] : undefined;
-
   return (
     <DescendantsPageListSubstance
       pagingResult={pagingResult}
@@ -190,7 +157,7 @@ export const DescendantsPageListForCurrentPath = (): JSX.Element => {
       setActivePage={setActivePage}
       forceHideMenuItems={forceHideMenuItems}
       onPagesDeleted={() => mutate()}
+      onPagePutBacked={() => mutate()}
     />
   );
-
 };

+ 2 - 8
packages/app/src/components/DescendantsPageListModal.tsx

@@ -20,15 +20,9 @@ import TimeLineIcon from './Icons/TimeLineIcon';
 
 import styles from './DescendantsPageListModal.module.scss';
 
-const DescendantsPageList = (props: DescendantsPageListProps): JSX.Element => {
-  const DescendantsPageList = dynamic<DescendantsPageListProps>(() => import('./DescendantsPageList').then(mod => mod.DescendantsPageList), { ssr: false });
-  return <DescendantsPageList {...props}/>;
-};
+const DescendantsPageList = dynamic<DescendantsPageListProps>(() => import('./DescendantsPageList').then(mod => mod.DescendantsPageList), { ssr: false });
 
-const PageTimeline = (): JSX.Element => {
-  const PageTimeline = dynamic(() => import('./PageTimeline').then(mod => mod.PageTimeline), { ssr: false });
-  return <PageTimeline />;
-};
+const PageTimeline = dynamic(() => import('./PageTimeline').then(mod => mod.PageTimeline), { ssr: false });
 
 export const DescendantsPageListModal = (): JSX.Element => {
   const { t } = useTranslation();

+ 0 - 11
packages/app/src/components/Layout/BasicLayout.tsx

@@ -4,7 +4,6 @@ import dynamic from 'next/dynamic';
 import { DndProvider } from 'react-dnd';
 import { HTML5Backend } from 'react-dnd-html5-backend';
 
-import { useEditorModeClassName } from '../../client/services/layout';
 import { GrowiNavbar } from '../Navbar/GrowiNavbar';
 import Sidebar from '../Sidebar';
 
@@ -68,13 +67,3 @@ export const BasicLayout = ({ children, className }: Props): JSX.Element => {
     </RawLayout>
   );
 };
-
-export const BasicLayoutWithEditorMode = ({ children }: Props): JSX.Element => {
-  const className = useEditorModeClassName();
-
-  return (
-    <BasicLayout className={className}>
-      {children}
-    </BasicLayout>
-  );
-};

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

@@ -120,7 +120,7 @@ export const BasicInfoSettings = (): JSX.Element => {
                     checked={personalSettingsInfo?.lang === locale}
                     onChange={() => changePersonalSettingsHandler({ lang: locale })}
                   />
-                  <label className="custom-control-label" htmlFor={`radioLang${locale}`}>{fixedT('meta.display_name')}</label>
+                  <label className="custom-control-label" htmlFor={`radioLang${locale}`}>{fixedT('meta.display_name') as string}</label>
                 </div>
               );
             })

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

@@ -24,7 +24,7 @@ import {
   usePageAccessoriesModal, PageAccessoriesModalContents, IPageForPageDuplicateModal,
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal, usePagePresentationModal,
 } from '~/stores/modal';
-import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
+import { useSWRMUTxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import {
   EditorMode, useDrawerMode, useEditorMode, useIsAbleToShowPageManagement, useIsAbleToShowTagLabel,
   useIsAbleToChangeEditorMode, useIsAbleToShowPageAuthors,
@@ -200,7 +200,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const router = useRouter();
 
   const { data: shareLinkId } = useShareLinkId();
-  const { mutate: mutateCurrentPage } = useSWRxCurrentPage();
+  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
 
   const { data: currentPathname } = useCurrentPathname();
   const isSharedPage = pagePathUtils.isSharedPage(currentPathname ?? '');

+ 0 - 3
packages/app/src/components/Navbar/GrowiNavbar.tsx

@@ -2,10 +2,8 @@ import React, {
   FC, memo, useMemo, useRef,
 } from 'react';
 
-import { isServer } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
-import Image from 'next/image';
 import Link from 'next/link';
 import { useRipple } from 'react-use-ripple';
 import { UncontrolledTooltip } from 'reactstrap';
@@ -17,7 +15,6 @@ import { usePageCreateModal } from '~/stores/modal';
 import { useCurrentPagePath } from '~/stores/page';
 import { useIsDeviceSmallerThanMd } from '~/stores/ui';
 
-import { HasChildren } from '../../interfaces/common';
 import GrowiLogo from '../Icons/GrowiLogo';
 
 import { GlobalSearchProps } from './GlobalSearch';

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

@@ -1,13 +1,12 @@
 import React from 'react';
 
-import { TFunction } from 'next-i18next';
 import { Disable } from 'react-disable';
 import { UncontrolledTooltip, UncontrolledTooltipProps } from 'reactstrap';
 
 type NotAvailableProps = {
   children: JSX.Element
   isDisabled: boolean
-  title: ReturnType<TFunction>
+  title: string
   classNamePrefix?: string
   placement?: UncontrolledTooltipProps['placement']
 }

+ 11 - 4
packages/app/src/components/NotFoundPage.tsx

@@ -3,19 +3,26 @@ import React, { useMemo } from 'react';
 import { useTranslation } from 'next-i18next';
 
 import CustomNavAndContents from './CustomNavigation/CustomNavAndContents';
-import { DescendantsPageListForCurrentPath } from './DescendantsPageList';
+import { DescendantsPageList } from './DescendantsPageList';
 import PageListIcon from './Icons/PageListIcon';
 import TimeLineIcon from './Icons/TimeLineIcon';
 import { PageTimeline } from './PageTimeline';
 
-const NotFoundPage = (): JSX.Element => {
+
+type NotFoundPageProps = {
+  path: string,
+}
+
+const NotFoundPage = (props: NotFoundPageProps): JSX.Element => {
   const { t } = useTranslation();
 
+  const { path } = props;
+
   const navTabMapping = useMemo(() => {
     return {
       pagelist: {
         Icon: PageListIcon,
-        Content: DescendantsPageListForCurrentPath,
+        Content: () => <DescendantsPageList path={path} />,
         i18n: t('page_list'),
         index: 0,
       },
@@ -26,7 +33,7 @@ const NotFoundPage = (): JSX.Element => {
         index: 1,
       },
     };
-  }, [t]);
+  }, [path, t]);
 
   return (
     <div className="d-edit-none">

+ 8 - 26
packages/app/src/components/Page/PageContents.tsx

@@ -1,17 +1,13 @@
 import React, { useEffect } from 'react';
 
-import { pagePathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
-import type { HtmlElementNode } from 'rehype-toc';
 
+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 { useCurrentPathname } from '~/stores/context';
-import { useEditingMarkdown } from '~/stores/editor';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useViewOptions } from '~/stores/renderer';
-import { useCurrentPageTocNode } from '~/stores/ui';
 import { registerGrowiFacade } from '~/utils/growi-facade';
 import loggerFactory from '~/utils/logger';
 
@@ -24,16 +20,10 @@ const logger = loggerFactory('growi:Page');
 export const PageContents = (): JSX.Element => {
   const { t } = useTranslation();
 
-  const { data: currentPathname } = useCurrentPathname();
-  const isSharedPage = pagePathUtils.isSharedPage(currentPathname ?? '');
+  const { data: currentPage } = useSWRxCurrentPage();
+  const updateStateAfterSave = useUpdateStateAfterSave(currentPage?._id);
 
-  const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
-  const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
-  const { mutate: mutateCurrentPageTocNode } = useCurrentPageTocNode();
-
-  const { data: rendererOptions, mutate: mutateRendererOptions } = useViewOptions((toc: HtmlElementNode) => {
-    mutateCurrentPageTocNode(toc);
-  });
+  const { data: rendererOptions, mutate: mutateRendererOptions } = useViewOptions();
 
   // register to facade
   useEffect(() => {
@@ -47,14 +37,10 @@ export const PageContents = (): JSX.Element => {
   }, [mutateRendererOptions]);
 
   useHandsontableModalLauncherForView({
-    onSaveSuccess: (newMarkdown) => {
+    onSaveSuccess: () => {
       toastSuccess(t('toaster.save_succeeded'));
 
-      // rerender
-      if (!isSharedPage) {
-        mutateCurrentPage();
-      }
-      mutateEditingMarkdown(newMarkdown);
+      updateStateAfterSave?.();
     },
     onSaveError: (error) => {
       toastError(error);
@@ -62,14 +48,10 @@ export const PageContents = (): JSX.Element => {
   });
 
   useDrawioModalLauncherForView({
-    onSaveSuccess: (newMarkdown) => {
+    onSaveSuccess: () => {
       toastSuccess(t('toaster.save_succeeded'));
 
-      // rerender
-      if (!isSharedPage) {
-        mutateCurrentPage();
-      }
-      mutateEditingMarkdown(newMarkdown);
+      updateStateAfterSave?.();
     },
     onSaveError: (error) => {
       toastError(error);

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

@@ -69,15 +69,15 @@ export const PageView = (props: Props): JSX.Element => {
       return <NotCreatablePage />;
     }
     if (isNotFound) {
-      return <NotFoundPage />;
+      return <NotFoundPage path={pagePath} />;
     }
-  }, [isForbidden, isIdenticalPathPage, isNotCreatable, isNotFound]);
+  }, [isForbidden, isIdenticalPathPage, isNotCreatable, isNotFound, pagePath]);
 
   const sideContents = !isNotFound && !isNotCreatable
     ? (
       <PageSideContents page={page} />
     )
-    : <></>;
+    : null;
 
   const footerContents = !isIdenticalPathPage && !isNotFound && page != null
     ? (
@@ -91,7 +91,7 @@ export const PageView = (props: Props): JSX.Element => {
         <PageContentFooter page={page} />
       </>
     )
-    : <></>;
+    : null;
 
   const isUsersHomePagePath = isUsersHomePage(pagePath);
 

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

@@ -84,9 +84,7 @@ export const RevisionLoader = (props: RevisionLoaderProps): JSX.Element => {
   /* ----- before load ----- */
   if (lazy && !isLoaded) {
     return (
-      <Waypoint onPositionChange={onWaypointChange} bottomOffset="-100px">
-        <div></div>
-      </Waypoint>
+      <Waypoint onPositionChange={onWaypointChange} bottomOffset="-100px" />
     );
   }
 

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

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

+ 10 - 10
packages/app/src/components/PageEditor.tsx

@@ -7,7 +7,7 @@ import nodePath from 'path';
 
 
 import {
-  IPageHasId, PageGrant, pathUtils,
+  IPageHasId, pathUtils,
 } from '@growi/core';
 import detectIndent from 'detect-indent';
 import { useTranslation } from 'next-i18next';
@@ -25,15 +25,16 @@ import {
   useIsEditable, useIsUploadableFile, useIsUploadableImage, useIsNotFound, useIsIndentSizeForced,
 } from '~/stores/context';
 import {
-  useCurrentIndentSize, useSWRxSlackChannels, useIsSlackEnabled, useIsTextlintEnabled, usePageTagsForEditors,
+  useCurrentIndentSize, useIsSlackEnabled, useIsTextlintEnabled, usePageTagsForEditors,
   useIsEnabledUnsavedWarning,
   useIsConflict,
   useEditingMarkdown,
 } from '~/stores/editor';
 import { useConflictDiffModal } from '~/stores/modal';
-import { useCurrentPagePath, useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
-import { usePageTreeTermManager } from '~/stores/page-listing';
-import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
+import {
+  useCurrentPagePath, useSWRMUTxCurrentPage, useSWRxCurrentPage, useSWRxTagsInfo,
+} from '~/stores/page';
+import { mutatePageTree } from '~/stores/page-listing';
 import { usePreviewOptions } from '~/stores/renderer';
 import {
   EditorMode,
@@ -74,7 +75,8 @@ const PageEditor = React.memo((): JSX.Element => {
   const { data: pageId } = useCurrentPageId();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPathname } = useCurrentPathname();
-  const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
+  const { data: currentPage } = useSWRxCurrentPage();
+  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
   const { data: grantData, mutate: mutateGrant } = useSelectedGrant();
   const { data: pageTags, sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);
   const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
@@ -90,7 +92,6 @@ const PageEditor = React.memo((): JSX.Element => {
   const { data: isUploadableFile } = useIsUploadableFile();
   const { data: isUploadableImage } = useIsUploadableImage();
   const { data: conflictDiffModalStatus, close: closeConflictDiffModal } = useConflictDiffModal();
-  const { advance: advancePt } = usePageTreeTermManager();
 
   const { data: rendererOptions, mutate: mutateRendererOptions } = usePreviewOptions();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
@@ -209,7 +210,7 @@ const PageEditor = React.memo((): JSX.Element => {
       );
 
       // to sync revision id with page tree: https://github.com/weseek/growi/pull/7227
-      advancePt();
+      mutatePageTree();
 
       return page;
     }
@@ -227,8 +228,7 @@ const PageEditor = React.memo((): JSX.Element => {
       return null;
     }
 
-  // eslint-disable-next-line max-len
-  }, [currentPathname, optionsToSave, grantData, isSlackEnabled, saveOrUpdate, pageId, currentPagePath, currentRevisionId, advancePt]);
+  }, [currentPathname, optionsToSave, grantData, isSlackEnabled, saveOrUpdate, pageId, currentPagePath, currentRevisionId]);
 
   const saveAndReturnToViewHandler = useCallback(async(opts: {slackChannels: string, overwriteScopesOfDescendants?: boolean}) => {
     if (editorMode !== EditorMode.Editor) {

+ 5 - 3
packages/app/src/components/PageEditor/EditorNavbarBottom.tsx

@@ -36,12 +36,14 @@ const EditorNavbarBottom = (): JSX.Element => {
 
   const [slackChannelsStr, setSlackChannelsStr] = useState<string>('');
 
+  // DO NOT dependent on slackChannelsData directly: https://github.com/weseek/growi/pull/7332
+  const slackChannelsDataString = slackChannelsData?.toString();
   useEffect(() => {
-    if (slackChannelsData != null) {
-      setSlackChannelsStr(slackChannelsData.toString());
+    if (editorMode === 'editor') {
+      setSlackChannelsStr(slackChannelsDataString ?? '');
       mutateIsSlackEnabled(false);
     }
-  }, [mutateIsSlackEnabled, slackChannelsData]);
+  }, [editorMode, mutateIsSlackEnabled, slackChannelsDataString]);
 
   const isSlackEnabledToggleHandler = (bool: boolean) => {
     mutateIsSlackEnabled(bool, false);

+ 11 - 10
packages/app/src/components/PageEditorByHackmd.tsx

@@ -24,8 +24,10 @@ import {
 import {
   usePageIdOnHackmd, useHasDraftOnHackmd, useRevisionIdHackmdSynced, useIsHackmdDraftUpdatingInRealtime,
 } from '~/stores/hackmd';
-import { useCurrentPagePath, useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
-import { usePageTreeTermManager } from '~/stores/page-listing';
+import {
+  useCurrentPagePath, useSWRMUTxCurrentPage, useSWRxCurrentPage, useSWRxTagsInfo,
+} from '~/stores/page';
+import { mutatePageTree } from '~/stores/page-listing';
 import { useRemoteRevisionId } from '~/stores/remote-latest-page';
 import {
   EditorMode,
@@ -64,12 +66,12 @@ export const PageEditorByHackmd = (): JSX.Element => {
   const { data: grantData } = useSelectedGrant();
   const { data: hackmdUri } = useHackmdUri();
   const saveOrUpdate = useSaveOrUpdate();
-  const { advance: advancePt } = usePageTreeTermManager();
 
   const { returnPathForURL } = pathUtils;
 
   // pageData
-  const { data: pageData, mutate: mutatePageData } = useSWRxCurrentPage();
+  const { data: pageData } = useSWRxCurrentPage();
+  const { trigger: mutatePageData } = useSWRMUTxCurrentPage();
   const revision = pageData?.revision;
 
   const [isInitialized, setIsInitialized] = useState(false);
@@ -131,7 +133,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
         mutateIsHackmdDraftUpdatingInRealtime(false);
 
         // to sync revision id with page tree: https://github.com/weseek/growi/pull/7227
-        advancePt();
+        mutatePageTree();
       }
       setIsInitialized(false);
       mutateEditorMode(EditorMode.View);
@@ -141,7 +143,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
       toastError(error.message);
     }
   // eslint-disable-next-line max-len
-  }, [editorMode, currentPathname, revision, revisionIdHackmdSynced, optionsToSave, saveOrUpdate, pageId, currentPagePath, isNotFound, mutateEditorMode, router, updateStateAfterSave, mutateIsHackmdDraftUpdatingInRealtime, advancePt]);
+  }, [editorMode, currentPathname, revision, revisionIdHackmdSynced, optionsToSave, saveOrUpdate, pageId, currentPagePath, isNotFound, mutateEditorMode, router, updateStateAfterSave, mutateIsHackmdDraftUpdatingInRealtime]);
 
   // set handler to save and reload Page
   useEffect(() => {
@@ -264,7 +266,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
       mutateTagsInfo();
 
       // to sync revision id with page tree: https://github.com/weseek/growi/pull/7227
-      advancePt();
+      mutatePageTree();
 
       mutateIsEnabledUnsavedWarning(false);
 
@@ -276,9 +278,8 @@ export const PageEditorByHackmd = (): JSX.Element => {
       logger.error('failed to save', error);
       toastError(error.message);
     }
-  }, [
-    currentPagePath, currentPathname, pageId, revisionIdHackmdSynced, optionsToSave,
-    saveOrUpdate, mutatePageData, updateStateAfterSave, mutateTagsInfo, advancePt, mutateIsEnabledUnsavedWarning, t]);
+  // eslint-disable-next-line max-len
+  }, [currentPagePath, currentPathname, pageId, revisionIdHackmdSynced, optionsToSave, saveOrUpdate, mutatePageData, updateStateAfterSave, mutateTagsInfo, mutateIsEnabledUnsavedWarning, t]);
 
   /**
    * onChange event of HackmdEditor handler

+ 3 - 2
packages/app/src/components/PageStatusAlert.tsx

@@ -9,7 +9,7 @@ import {
   useHasDraftOnHackmd, useIsHackmdDraftUpdatingInRealtime, useRevisionIdHackmdSynced,
 } from '~/stores/hackmd';
 import { useConflictDiffModal } from '~/stores/modal';
-import { useSWRxCurrentPage } from '~/stores/page';
+import { useSWRMUTxCurrentPage, useSWRxCurrentPage } from '~/stores/page';
 import { useRemoteRevisionId, useRemoteRevisionLastUpdateUser } from '~/stores/remote-latest-page';
 import { EditorMode, useEditorMode } from '~/stores/ui';
 
@@ -39,7 +39,8 @@ export const PageStatusAlert = (): JSX.Element => {
   const { data: remoteRevisionId } = useRemoteRevisionId();
   const { data: remoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
 
-  const { data: pageData, mutate: mutatePageData } = useSWRxCurrentPage();
+  const { data: pageData } = useSWRxCurrentPage();
+  const { trigger: mutatePageData } = useSWRMUTxCurrentPage();
   const revision = pageData?.revision;
 
   const refreshPage = useCallback(async() => {

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

@@ -18,7 +18,7 @@ import { useCurrentUser } from '~/stores/context';
 import {
   ILegacyPrivatePage, usePrivateLegacyPagesMigrationModal,
 } from '~/stores/modal';
-import { usePageTreeTermManager, useSWRxV5MigrationStatus } from '~/stores/page-listing';
+import { mutatePageTree, useSWRxV5MigrationStatus } from '~/stores/page-listing';
 import {
   useSWRxSearch,
 } from '~/stores/search';
@@ -213,13 +213,12 @@ const PrivateLegacyPages = (): JSX.Element => {
   });
 
   const { data: migrationStatus, mutate: mutateMigrationStatus } = useSWRxV5MigrationStatus();
-  const { advance: advancePt } = usePageTreeTermManager();
 
   const searchInvokedHandler = useCallback((_keyword: string) => {
     mutateMigrationStatus();
     setKeyword(_keyword);
     setOffset(0);
-  }, []);
+  }, [mutateMigrationStatus]);
 
   const { open: openModal, close: closeModal } = usePrivateLegacyPagesMigrationModal();
   const { data: socket } = useGlobalSocket();
@@ -245,7 +244,7 @@ const PrivateLegacyPages = (): JSX.Element => {
       socket?.off(SocketEventName.PageMigrationSuccess);
       socket?.off(SocketEventName.PageMigrationError);
     };
-  }, [socket]);
+  }, [socket, t]);
 
   const selectAllCheckboxChangedHandler = useCallback((isChecked: boolean) => {
     const instance = searchPageBaseRef.current;
@@ -315,10 +314,10 @@ const PrivateLegacyPages = (): JSX.Element => {
         closeModal();
         mutateMigrationStatus();
         mutate();
-        advancePt();
+        mutatePageTree();
       },
     );
-  }, [data, mutate, openModal, closeModal, mutateMigrationStatus]);
+  }, [data, openModal, t, closeModal, mutateMigrationStatus, mutate]);
 
   const pagingSizeChangedHandler = useCallback((pagingSize: number) => {
     setOffset(0);
@@ -381,7 +380,8 @@ const PrivateLegacyPages = (): JSX.Element => {
         {isAdmin && renderOpenModalButton()}
       </div>
     );
-  }, [convertMenuItemClickedHandler, deleteAllButtonClickedHandler, hitsCount, isControlEnabled, selectAllCheckboxChangedHandler, t]);
+  // eslint-disable-next-line max-len
+  }, [convertMenuItemClickedHandler, deleteAllButtonClickedHandler, hitsCount, isAdmin, isControlEnabled, renderOpenModalButton, selectAllCheckboxChangedHandler, t]);
 
   const searchControl = useMemo(() => {
     return (
@@ -455,7 +455,7 @@ const PrivateLegacyPages = (): JSX.Element => {
             toastSuccess(t('private_legacy_pages.by_path_modal.success'));
             setOpenConvertModal(false);
             mutate();
-            advancePt();
+            mutatePageTree();
           }
           catch (errs) {
             if (errs.length === 1) {

+ 2 - 4
packages/app/src/components/PutbackPageModal.jsx

@@ -7,9 +7,8 @@ import {
 } from 'reactstrap';
 
 import { apiPost } from '~/client/util/apiv1-client';
-import { PathAlreadyExistsError } from '~/server/models/errors';
 import { usePutBackPageModal } from '~/stores/modal';
-import { usePageInfoTermManager } from '~/stores/page';
+import { mutateAllPageInfo } from '~/stores/page';
 
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 
@@ -17,7 +16,6 @@ const PutBackPageModal = () => {
   const { t } = useTranslation();
 
   const { data: pageDataToRevert, close: closePutBackPageModal } = usePutBackPageModal();
-  const { advance: advancePi } = usePageInfoTermManager();
   const { isOpened, page } = pageDataToRevert;
   const { pageId, path } = page;
   const onPutBacked = pageDataToRevert.opts?.onPutBacked;
@@ -43,7 +41,7 @@ const PutBackPageModal = () => {
         page_id: pageId,
         recursively,
       });
-      advancePi();
+      mutateAllPageInfo();
 
       if (onPutBacked != null) {
         onPutBacked(response.page.path);

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

@@ -216,8 +216,7 @@ export const SearchPage = (): JSX.Element => {
         initialSearchConditions={initialSearchConditions}
         onSearchInvoked={searchInvokedHandler}
         allControl={allControl}
-      >
-      </SearchControl>
+      />
     );
   }, [allControl, initialSearchConditions, isSearchServiceReachable, searchInvokedHandler]);
 

+ 2 - 2
packages/app/src/components/SearchPage/SearchControl.tsx

@@ -1,5 +1,5 @@
 import React, {
-  FC, useCallback, useEffect, useState,
+  useCallback, useEffect, useState,
 } from 'react';
 
 import { useTranslation } from 'next-i18next';
@@ -23,7 +23,7 @@ type Props = {
   allControl: React.ReactNode,
 }
 
-const SearchControl: FC <Props> = React.memo((props: Props) => {
+const SearchControl = React.memo((props: Props): JSX.Element => {
 
   const {
     isSearchServiceReachable,

+ 2 - 5
packages/app/src/components/SearchPage/SearchPageBase.tsx

@@ -11,7 +11,7 @@ import { IFormattedSearchResult, IPageWithSearchMeta } from '~/interfaces/search
 import { OnDeletedFunction } from '~/interfaces/ui';
 import { useIsGuestUser, useIsSearchServiceConfigured, useIsSearchServiceReachable } from '~/stores/context';
 import { usePageDeleteModal } from '~/stores/modal';
-import { usePageTreeTermManager } from '~/stores/page-listing';
+import { mutatePageTree } from '~/stores/page-listing';
 
 import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 
@@ -228,9 +228,6 @@ export const usePageDeleteModalForBulkDeletion = (
 
   const { open: openDeleteModal } = usePageDeleteModal();
 
-  // for PageTree mutation
-  const { advance: advancePt } = usePageTreeTermManager();
-
   return () => {
     if (data == null) {
       return;
@@ -260,7 +257,7 @@ export const usePageDeleteModalForBulkDeletion = (
         else {
           toastSuccess(t('deleted_pages_completely', { path }));
         }
-        advancePt();
+        mutatePageTree();
 
         if (onDeleted != null) {
           onDeleted(...args);

+ 14 - 19
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -18,9 +18,9 @@ import { useCurrentUser, useIsContainerFluid } from '~/stores/context';
 import {
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal,
 } from '~/stores/modal';
-import { useDescendantsPageListForCurrentPathTermManager, usePageTreeTermManager } from '~/stores/page-listing';
+import { mutatePageList, mutatePageTree } from '~/stores/page-listing';
 import { useSearchResultOptions } from '~/stores/renderer';
-import { useFullTextSearchTermManager } from '~/stores/search';
+import { mutateSearching } from '~/stores/search';
 
 import { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import { GrowiSubNavigationProps } from '../Navbar/GrowiSubNavigation';
@@ -91,11 +91,6 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
   const [isRevisionLoaded, setRevisionLoaded] = useState(false);
   const [isPageCommentLoaded, setPageCommentLoaded] = useState(false);
 
-  // for mutation
-  const { advance: advancePt } = usePageTreeTermManager();
-  const { advance: advanceFts } = useFullTextSearchTermManager();
-  const { advance: advanceDpl } = useDescendantsPageListForCurrentPathTermManager();
-
   // ***************************  Auto Scroll  ***************************
   useEffect(() => {
     const scrollElement = scrollElementRef.current;
@@ -167,23 +162,23 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
     const duplicatedHandler: OnDuplicatedFunction = (fromPath, toPath) => {
       toastSuccess(t('duplicated_pages', { fromPath }));
 
-      advancePt();
-      advanceFts();
-      advanceDpl();
+      mutatePageTree();
+      mutateSearching();
+      mutatePageList();
     };
     openDuplicateModal(pageToDuplicate, { onDuplicated: duplicatedHandler });
-  }, [advanceDpl, advanceFts, advancePt, openDuplicateModal, t]);
+  }, [openDuplicateModal, t]);
 
   const renameItemClickedHandler = useCallback((pageToRename: IPageToRenameWithMeta) => {
     const renamedHandler: OnRenamedFunction = (path) => {
       toastSuccess(t('renamed_pages', { path }));
 
-      advancePt();
-      advanceFts();
-      advanceDpl();
+      mutatePageTree();
+      mutateSearching();
+      mutatePageList();
     };
     openRenameModal(pageToRename, { onRenamed: renamedHandler });
-  }, [advanceDpl, advanceFts, advancePt, openRenameModal, t]);
+  }, [openRenameModal, t]);
 
   const onDeletedHandler: OnDeletedFunction = useCallback((pathOrPathsToDelete, isRecursively, isCompletely) => {
     if (typeof pathOrPathsToDelete !== 'string') {
@@ -197,10 +192,10 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
     else {
       toastSuccess(t('deleted_pages', { path }));
     }
-    advancePt();
-    advanceFts();
-    advanceDpl();
-  }, [advanceDpl, advanceFts, advancePt, t]);
+    mutatePageTree();
+    mutateSearching();
+    mutatePageList();
+  }, [t]);
 
   const deleteItemClickedHandler = useCallback((pageToDelete: IPageToDeleteWithMeta) => {
     openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });

+ 15 - 19
packages/app/src/components/SearchPage/SearchResultList.tsx

@@ -11,10 +11,9 @@ import {
   IPageInfoForListing, IPageWithMeta, isIPageInfoForListing,
 } from '~/interfaces/page';
 import { IPageSearchMeta, IPageWithSearchMeta } from '~/interfaces/search';
-import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import { useIsGuestUser } from '~/stores/context';
-import { useSWRxPageInfoForList, usePageTreeTermManager } from '~/stores/page-listing';
-import { useFullTextSearchTermManager } from '~/stores/search';
+import { mutatePageTree, useSWRxPageInfoForList } from '~/stores/page-listing';
+import { mutateSearching } from '~/stores/search';
 
 import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import { PageListItemL } from '../PageList/PageListItemL';
@@ -44,10 +43,6 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
   const { data: isGuestUser } = useIsGuestUser();
   const { data: idToPageInfo } = useSWRxPageInfoForList(pageIdsWithNoSnippet, null, true, true);
 
-  // for mutation
-  const { advance: advancePt } = usePageTreeTermManager();
-  const { advance: advanceFts } = useFullTextSearchTermManager();
-
   const itemsRef = useRef<(ISelectable|null)[]>([]);
 
   // publish selectAll()
@@ -95,20 +90,21 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
   }
 
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
-  const duplicatedHandler : OnDuplicatedFunction = (fromPath, toPath) => {
+  const duplicatedHandler = useCallback((fromPath, toPath) => {
     toastSuccess(t('duplicated_pages', { fromPath }));
 
-    advancePt();
-    advanceFts();
-  };
+    mutatePageTree();
+    mutateSearching();
+  }, [t]);
 
-  const renamedHandler: OnRenamedFunction = (path) => {
+  const renamedHandler = useCallback((path) => {
     toastSuccess(t('renamed_pages', { path }));
 
-    advancePt();
-    advanceFts();
-  };
-  const deletedHandler: OnDeletedFunction = (pathOrPathsToDelete, isRecursively, isCompletely) => {
+    mutatePageTree();
+    mutateSearching();
+  }, [t]);
+
+  const deletedHandler = useCallback((pathOrPathsToDelete, isRecursively, isCompletely) => {
     if (typeof pathOrPathsToDelete !== 'string') {
       return;
     }
@@ -121,9 +117,9 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
     else {
       toastSuccess(t('deleted_pages', { path }));
     }
-    advancePt();
-    advanceFts();
-  };
+    mutatePageTree();
+    mutateSearching();
+  }, [t]);
 
   return (
     <ul data-testid="search-result-list" className="page-list-ul list-group list-group-flush">

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

@@ -1,10 +1,8 @@
 import React, { useEffect } from 'react';
 
 import type { IPagePopulatedToShowRevision } from '@growi/core';
-import type { HtmlElementNode } from 'rehype-toc';
 
 import { useViewOptions } from '~/stores/renderer';
-import { useCurrentPageTocNode } from '~/stores/ui';
 import { registerGrowiFacade } from '~/utils/growi-facade';
 import loggerFactory from '~/utils/logger';
 
@@ -21,11 +19,7 @@ export type ShareLinkPageContentsProps = {
 export const ShareLinkPageContents = (props: ShareLinkPageContentsProps): JSX.Element => {
   const { page } = props;
 
-  const { mutate: mutateCurrentPageTocNode } = useCurrentPageTocNode();
-
-  const { data: rendererOptions, mutate: mutateRendererOptions } = useViewOptions((toc: HtmlElementNode) => {
-    mutateCurrentPageTocNode(toc);
-  });
+  const { data: rendererOptions, mutate: mutateRendererOptions } = useViewOptions();
 
   // register to facade
   useEffect(() => {

+ 17 - 21
packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx

@@ -16,11 +16,11 @@ import { useIsEnabledAttachTitleHeader } from '~/stores/context';
 import {
   IPageForPageDuplicateModal, usePageDuplicateModal, usePageDeleteModal,
 } from '~/stores/modal';
-import { useCurrentPagePath, usePageInfoTermManager, useSWRxCurrentPage } from '~/stores/page';
+import { mutateAllPageInfo, useCurrentPagePath, useSWRMUTxCurrentPage } from '~/stores/page';
 import {
-  usePageTreeTermManager, useSWRxPageAncestorsChildren, useSWRxRootPage, useDescendantsPageListForCurrentPathTermManager,
+  useSWRxPageAncestorsChildren, useSWRxRootPage, mutatePageTree, mutatePageList,
 } from '~/stores/page-listing';
-import { useFullTextSearchTermManager } from '~/stores/search';
+import { mutateSearching } from '~/stores/search';
 import { usePageTreeDescCountMap, useSidebarScrollerRef } from '~/stores/ui';
 import { useGlobalSocket } from '~/stores/websocket';
 import loggerFactory from '~/utils/logger';
@@ -117,11 +117,7 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
   const { data: ptDescCountMap, update: updatePtDescCountMap } = usePageTreeDescCountMap();
 
   // for mutation
-  const { mutate: mutateCurrentPage } = useSWRxCurrentPage();
-  const { advance: advancePt } = usePageTreeTermManager();
-  const { advance: advanceFts } = useFullTextSearchTermManager();
-  const { advance: advanceDpl } = useDescendantsPageListForCurrentPathTermManager();
-  const { advance: advancePi } = usePageInfoTermManager();
+  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
 
   const [isInitialScrollCompleted, setIsInitialScrollCompleted] = useState(false);
 
@@ -151,27 +147,27 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
   }, [socket, ptDescCountMap, updatePtDescCountMap]);
 
   const onRenamed = useCallback((fromPath: string | undefined, toPath: string) => {
-    advancePt();
-    advanceFts();
-    advanceDpl();
+    mutatePageTree();
+    mutateSearching();
+    mutatePageList();
 
     if (currentPagePath === fromPath || currentPagePath === toPath) {
       mutateCurrentPage();
     }
-  }, [advanceDpl, advanceFts, advancePt, currentPagePath, mutateCurrentPage]);
+  }, [currentPagePath, mutateCurrentPage]);
 
   const onClickDuplicateMenuItem = useCallback((pageToDuplicate: IPageForPageDuplicateModal) => {
     // eslint-disable-next-line @typescript-eslint/no-unused-vars
     const duplicatedHandler: OnDuplicatedFunction = (fromPath, toPath) => {
       toastSuccess(t('duplicated_pages', { fromPath }));
 
-      advancePt();
-      advanceFts();
-      advanceDpl();
+      mutatePageTree();
+      mutateSearching();
+      mutatePageList();
     };
 
     openDuplicateModal(pageToDuplicate, { onDuplicated: duplicatedHandler });
-  }, [advanceDpl, advanceFts, advancePt, openDuplicateModal, t]);
+  }, [openDuplicateModal, t]);
 
   const onClickDeleteMenuItem = useCallback((pageToDelete: IPageToDeleteWithMeta) => {
     const onDeletedHandler: OnDeletedFunction = (pathOrPathsToDelete, isRecursively, isCompletely) => {
@@ -188,10 +184,10 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
         toastSuccess(t('deleted_pages', { path }));
       }
 
-      advancePt();
-      advanceFts();
-      advanceDpl();
-      advancePi();
+      mutatePageTree();
+      mutateSearching();
+      mutatePageList();
+      mutateAllPageInfo();
 
       if (currentPagePath === pathOrPathsToDelete) {
         mutateCurrentPage();
@@ -200,7 +196,7 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
     };
 
     openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });
-  }, [advanceDpl, advanceFts, advancePi, advancePt, currentPagePath, mutateCurrentPage, openDeleteModal, router, t]);
+  }, [currentPagePath, mutateCurrentPage, openDeleteModal, router, t]);
 
   // ***************************  Scroll on init ***************************
   const scrollOnInit = useCallback(() => {

+ 8 - 2
packages/app/src/components/Sidebar/SidebarNav.tsx

@@ -1,4 +1,6 @@
-import React, { FC, memo, useCallback } from 'react';
+import React, {
+  FC, memo, useCallback, useEffect, useState,
+} from 'react';
 
 import Link from 'next/link';
 
@@ -80,10 +82,14 @@ export const SidebarNav: FC<Props> = (props: Props) => {
 
   const { data: currentUser } = useCurrentUser();
 
-  const isAdmin = currentUser?.admin;
+  const [isAdmin, setAdmin] = useState(false);
 
   const { onItemSelected } = props;
 
+  useEffect(() => {
+    setAdmin(currentUser?.admin === true);
+  }, [currentUser?.admin]);
+
   return (
     <div className={`grw-sidebar-nav ${styles['grw-sidebar-nav']}`} data-vrt-blackout-sidebar-nav>
       <div className="grw-sidebar-nav-primary-container">

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

@@ -15,6 +15,7 @@ export type StickyStretchableScrollerProps = {
   stickyElemSelector: string,
   simplebarRef?: (ref: RefObject<SimpleBar>) => void,
   calcViewHeight?: (scrollElement: HTMLElement) => number,
+  children?: JSX.Element,
 }
 
 /**
@@ -39,7 +40,7 @@ export type StickyStretchableScrollerProps = {
     </StickyStretchableScroller>
   );
  */
-export const StickyStretchableScroller: FC<StickyStretchableScrollerProps> = (props) => {
+export const StickyStretchableScroller = (props: StickyStretchableScrollerProps): JSX.Element => {
 
   const {
     children, stickyElemSelector, calcViewHeight, simplebarRef: setSimplebarRef,

+ 24 - 6
packages/app/src/components/TrashPageList.tsx

@@ -1,6 +1,7 @@
-import React, { FC, useMemo, useCallback } from 'react';
+import React, { useMemo, useCallback } from 'react';
 
 import { useTranslation } from 'next-i18next';
+import dynamic from 'next/dynamic';
 
 import { toastSuccess } from '~/client/util/apiNotification';
 import {
@@ -9,13 +10,18 @@ import {
 import { IPagingResult } from '~/interfaces/paging-result';
 import { useShowPageLimitationXL } from '~/stores/context';
 import { useEmptyTrashModal } from '~/stores/modal';
-import { useSWRxDescendantsPageListForCurrrentPath, useSWRxPageInfoForList } from '~/stores/page-listing';
+import { useSWRxPageInfoForList, useSWRxPageList } from '~/stores/page-listing';
 
+import { MenuItemType } from './Common/Dropdown/PageItemControl';
 import CustomNavAndContents from './CustomNavigation/CustomNavAndContents';
-import { DescendantsPageListForCurrentPath } from './DescendantsPageList';
+import { DescendantsPageListProps } from './DescendantsPageList';
 import EmptyTrashButton from './EmptyTrashButton';
 import PageListIcon from './Icons/PageListIcon';
 
+
+const DescendantsPageList = dynamic<DescendantsPageListProps>(() => import('./DescendantsPageList').then(mod => mod.DescendantsPageList), { ssr: false });
+
+
 const convertToIDataWithMeta = (page) => {
   return { data: page };
 };
@@ -23,7 +29,7 @@ const convertToIDataWithMeta = (page) => {
 const useEmptyTrashButton = () => {
 
   const { data: limit } = useShowPageLimitationXL();
-  const { data: pagingResult, mutate: mutatePageLists } = useSWRxDescendantsPageListForCurrrentPath(1, limit);
+  const { data: pagingResult, mutate: mutatePageLists } = useSWRxPageList('/trash', 1, limit);
   const { t } = useTranslation();
   const { open: openEmptyTrashModal } = useEmptyTrashModal();
 
@@ -59,7 +65,19 @@ const useEmptyTrashButton = () => {
   return emptyTrashButton;
 };
 
-export const TrashPageList: FC = () => {
+const DescendantsPageListForTrash = (): JSX.Element => {
+  const { data: limit } = useShowPageLimitationXL();
+
+  return (
+    <DescendantsPageList
+      path="/trash"
+      limit={limit}
+      forceHideMenuItems={[MenuItemType.RENAME]}
+    />
+  );
+};
+
+export const TrashPageList = (): JSX.Element => {
   const { t } = useTranslation();
   const emptyTrashButton = useEmptyTrashButton();
 
@@ -67,7 +85,7 @@ export const TrashPageList: FC = () => {
     return {
       pagelist: {
         Icon: PageListIcon,
-        Content: DescendantsPageListForCurrentPath,
+        Content: DescendantsPageListForTrash,
         i18n: t('page_list'),
         index: 0,
       },

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

@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react';
+import React, { ReactNode, useEffect } from 'react';
 
 
 import EventEmitter from 'events';
@@ -7,7 +7,7 @@ import {
   isClient, isIPageInfoForEntity, pagePathUtils, pathUtils,
 } from '@growi/core';
 import type {
-  IDataWithMeta, IPageInfoForEntity, IPagePopulatedToShowRevision, IUser, IUserHasId,
+  IDataWithMeta, IPageInfoForEntity, IPagePopulatedToShowRevision, IUserHasId,
 } from '@growi/core';
 import ExtensibleCustomError from 'extensible-custom-error';
 import {
@@ -19,7 +19,7 @@ import Head from 'next/head';
 import { useRouter } from 'next/router';
 import superjson from 'superjson';
 
-import { useCurrentGrowiLayoutFluidClassName } from '~/client/services/layout';
+import { useCurrentGrowiLayoutFluidClassName, useEditorModeClassName } from '~/client/services/layout';
 import { PageView } from '~/components/Page/PageView';
 import RevisionRenderer from '~/components/Page/RevisionRenderer';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
@@ -46,7 +46,7 @@ import { useSetupGlobalSocket, useSetupGlobalSocketForPage } from '~/stores/webs
 import loggerFactory from '~/utils/logger';
 
 import { DescendantsPageListModal } from '../components/DescendantsPageListModal';
-import { BasicLayoutWithEditorMode } from '../components/Layout/BasicLayout';
+import { BasicLayout } from '../components/Layout/BasicLayout';
 import GrowiContextualSubNavigationSubstance from '../components/Navbar/GrowiContextualSubNavigation';
 import type { GrowiSubNavigationSwitcherProps } from '../components/Navbar/GrowiSubNavigationSwitcher';
 import { DisplaySwitcher } from '../components/Page/DisplaySwitcher';
@@ -137,8 +137,6 @@ const PutbackPageModal = (): JSX.Element => {
 };
 
 type Props = CommonProps & {
-  currentUser: IUser,
-
   pageWithMeta: IPageToShowRevisionWithMeta | null,
   // pageUser?: any,
   redirectFrom?: string;
@@ -342,14 +340,29 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   );
 };
 
+type LayoutProps = {
+  children?: ReactNode
+  className?: string
+}
+
+const Layout = ({ children }: LayoutProps): JSX.Element => {
+  const className = useEditorModeClassName();
+
+  return (
+    <BasicLayout className={className}>
+      {children}
+    </BasicLayout>
+  );
+};
+
 Page.getLayout = function getLayout(page) {
   return (
     <>
       <DrawioViewerScript />
 
-      <BasicLayoutWithEditorMode>
+      <Layout>
         {page}
-      </BasicLayoutWithEditorMode>
+      </Layout>
       <UnsavedAlertDialog />
       <DescendantsPageListModal />
       <DrawioModal />

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

@@ -1,6 +1,5 @@
 import React, { ReactElement, ReactNode, useEffect } from 'react';
 
-import { isServer } from '@growi/core';
 import { NextPage } from 'next';
 import { appWithTranslation } from 'next-i18next';
 import { AppProps } from 'next/app';
@@ -13,8 +12,7 @@ import { useI18nextHMR } from '~/services/i18next-hmr';
 import {
   useAppTitle, useConfidential, useGrowiVersion, useSiteUrl, useIsDefaultLogo, useForcedColorScheme,
 } from '~/stores/context';
-import { SWRConfigValue, swrGlobalConfiguration } from '~/utils/swr-utils';
-
+import { swrGlobalConfiguration } from '~/utils/swr-utils';
 
 import { CommonProps } from './utils/commons';
 import { registerTransformerForObjectId } from './utils/objectid-transformer';
@@ -26,14 +24,6 @@ import '~/styles/theme/_apply-colors.scss';
 
 const isDev = process.env.NODE_ENV === 'development';
 
-const swrConfig: SWRConfigValue = {
-  ...swrGlobalConfiguration,
-  // set the request scoped cache provider in server
-  provider: isServer()
-    ? cache => new Map(cache)
-    : undefined,
-};
-
 
 // eslint-disable-next-line @typescript-eslint/ban-types
 export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
@@ -72,7 +62,7 @@ function GrowiApp({ Component, pageProps }: GrowiAppProps): JSX.Element {
   const getLayout = Component.getLayout ?? (page => page);
 
   return (
-    <SWRConfig value={swrConfig}>
+    <SWRConfig value={swrGlobalConfiguration}>
       {getLayout(<Component {...pageProps} />)}
     </SWRConfig>
   );

+ 5 - 5
packages/app/src/pages/trash.page.tsx

@@ -1,7 +1,8 @@
 import React from 'react';
 
 import type { IUser, IUserHasId } from '@growi/core';
-import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
+import { GetServerSideProps, GetServerSidePropsContext } from 'next';
+import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
@@ -17,7 +18,7 @@ import {
   useCurrentProductNavWidth, useCurrentSidebarContents, useDrawerMode, usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed,
 } from '~/stores/ui';
 
-import { BasicLayoutWithEditorMode } from '../components/Layout/BasicLayout';
+import { BasicLayout } from '../components/Layout/BasicLayout';
 import {
   useCurrentUser, useCurrentPageId, useCurrentPathname,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
@@ -28,7 +29,6 @@ import { NextPageWithLayout } from './_app.page';
 import {
   CommonProps, getServerSideCommonProps, getNextI18NextConfig, generateCustomTitleForPage,
 } from './utils/commons';
-import { useTranslation } from 'next-i18next';
 
 const TrashPageList = dynamic(() => import('~/components/TrashPageList').then(mod => mod.TrashPageList), { ssr: false });
 const EmptyTrashModal = dynamic(() => import('~/components/EmptyTrashModal'), { ssr: false });
@@ -109,9 +109,9 @@ const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
 TrashPage.getLayout = function getLayout(page) {
   return (
     <>
-      <BasicLayoutWithEditorMode>
+      <BasicLayout>
         {page}
-      </BasicLayoutWithEditorMode>
+      </BasicLayout>
       <EmptyTrashModal />
       <PutbackPageModal />
     </>

+ 1 - 1
packages/app/src/pages/utils/commons.ts

@@ -22,7 +22,7 @@ export type CommonProps = {
   isMaintenanceMode: boolean,
   redirectDestination: string | null,
   isDefaultLogo: boolean,
-  currentUser?: IUser,
+  currentUser?: IUserHasId,
   forcedColorScheme?: ColorScheme,
 } & Partial<SSRConfig>;
 

+ 17 - 4
packages/app/src/server/routes/apiv3/users.js

@@ -8,7 +8,7 @@ import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 
 
-const logger = loggerFactory('growi:routes:apiv3:user-group');
+const logger = loggerFactory('growi:routes:apiv3:users');
 
 const express = require('express');
 
@@ -134,6 +134,19 @@ module.exports = (crowi) => {
     query('options').optional().isString().withMessage('options must be string'),
   ];
 
+  // express middleware
+  const certifyUserOperationOtherThenYourOwn = (req, res, next) => {
+    const { id } = req.params;
+
+    if (req.user._id.toString() === id) {
+      const msg = 'This API is not available for your own users';
+      logger.error(msg);
+      return res.apiv3Err(new ErrorV3(msg), 400);
+    }
+
+    next();
+  };
+
   const sendEmailByUserList = async(userList) => {
     const { appService, mailService } = crowi;
     const appTitle = appService.getAppTitle();
@@ -509,7 +522,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: data of removed admin user
    */
-  router.put('/:id/removeAdmin', loginRequiredStrictly, adminRequired, addActivity, async(req, res) => {
+  router.put('/:id/removeAdmin', loginRequiredStrictly, adminRequired, certifyUserOperationOtherThenYourOwn, addActivity, async(req, res) => {
     const { id } = req.params;
 
     try {
@@ -605,7 +618,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: data of deactivate user
    */
-  router.put('/:id/deactivate', loginRequiredStrictly, adminRequired, addActivity, async(req, res) => {
+  router.put('/:id/deactivate', loginRequiredStrictly, adminRequired, certifyUserOperationOtherThenYourOwn, addActivity, async(req, res) => {
     const { id } = req.params;
 
     try {
@@ -649,7 +662,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      description: data of delete user
    */
-  router.delete('/:id/remove', loginRequiredStrictly, adminRequired, addActivity, async(req, res) => {
+  router.delete('/:id/remove', loginRequiredStrictly, adminRequired, certifyUserOperationOtherThenYourOwn, addActivity, async(req, res) => {
     const { id } = req.params;
 
     try {

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

@@ -12,7 +12,7 @@ export const useSWRxActivity = (limit?: number, offset?: number, searchFilter?:
   const stringifiedSearchFilter = JSON.stringify(searchFilter);
   return useSWRImmutable(
     auditLogEnabled ? ['/activity', limit, offset, stringifiedSearchFilter] : null,
-    (endpoint, limit, offset, stringifiedSearchFilter) => apiv3Get(endpoint, { limit, offset, searchFilter: stringifiedSearchFilter })
+    ([endpoint, limit, offset, stringifiedSearchFilter]) => apiv3Get(endpoint, { limit, offset, searchFilter: stringifiedSearchFilter })
       .then(result => result.data.serializedPaginationResult),
   );
 };

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

@@ -23,7 +23,7 @@ type IDataAttachmentList = {
 export const useSWRxAttachments = (pageId?: Nullable<string>, pageNumber?: number): SWRResponseWithUtils<Util, IDataAttachmentList, Error> => {
   const shouldFetch = pageId != null && pageNumber != null;
 
-  const fetcher = useCallback(async(endpoint, pageId, pageNumber) => {
+  const fetcher = useCallback(async([endpoint, pageId, pageNumber]) => {
     const res = await apiv3Get<IResAttachmentList>(endpoint, { pageId, pageNumber });
     const resAttachmentList = res.data;
     const { paginateResult } = resAttachmentList;

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

@@ -20,7 +20,7 @@ export const useSWRxPageComment = (pageId: Nullable<string>): SWRResponse<IComme
 
   const swrResponse = useSWR(
     shouldFetch ? ['/comments.get', pageId] : null,
-    (endpoint, pageId) => apiGet(endpoint, { page_id: pageId }).then((response:IResponseComment) => response.comments),
+    ([endpoint, pageId]) => apiGet(endpoint, { page_id: pageId }).then((response:IResponseComment) => response.comments),
   );
 
   const update = async(comment: string, revisionId: string, commentId: string) => {

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

@@ -1,4 +1,4 @@
-import type { ColorScheme, IUser } from '@growi/core';
+import type { ColorScheme, IUser, IUserHasId } from '@growi/core';
 import { Key, SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
@@ -36,8 +36,8 @@ export const useConfidential = (initialData?: string): SWRResponse<string, Error
   return useContextSWR('confidential', initialData);
 };
 
-export const useCurrentUser = (initialData?: Nullable<IUser>): SWRResponse<Nullable<IUser>, Error> => {
-  return useContextSWR<Nullable<IUser>, Error>('currentUser', initialData);
+export const useCurrentUser = (initialData?: Nullable<IUserHasId>): SWRResponse<Nullable<IUserHasId>, Error> => {
+  return useContextSWR('currentUser', initialData);
 };
 
 export const useCurrentPathname = (initialData?: string): SWRResponse<string, Error> => {
@@ -230,12 +230,12 @@ export const useIsContainerFluid = (initialData?: boolean): SWRResponse<boolean,
  *********************************************************** */
 
 export const useIsGuestUser = (): SWRResponse<boolean, Error> => {
-  const { data: currentUser } = useCurrentUser();
+  const { data: currentUser, isLoading } = useCurrentUser();
 
   return useSWRImmutable(
-    ['isGuestUser', currentUser],
-    (key: Key, currentUser: IUser) => currentUser == null,
-    { fallbackData: currentUser == null },
+    isLoading ? null : ['isGuestUser', currentUser?._id],
+    ([, currentUserId]) => currentUserId == null,
+    { fallbackData: currentUser?._id == null },
   );
 };
 
@@ -247,7 +247,7 @@ export const useIsEditable = (): SWRResponse<boolean, Error> => {
 
   return useSWRImmutable(
     ['isEditable', isGuestUser, isForbidden, isNotCreatable, isIdenticalPath],
-    (key: Key, isGuestUser: boolean, isForbidden: boolean, isNotCreatable: boolean, isIdenticalPath: boolean) => {
+    ([, isGuestUser, isForbidden, isNotCreatable, isIdenticalPath]) => {
       return (!isForbidden && !isIdenticalPath && !isNotCreatable && !isGuestUser);
     },
   );

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

@@ -1,7 +1,7 @@
 import { useCallback } from 'react';
 
 import { Nullable, withUtils, SWRResponseWithUtils } from '@growi/core';
-import useSWR, { SWRResponse } from 'swr';
+import { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
 import { apiGet } from '~/client/util/apiv1-client';
@@ -39,9 +39,9 @@ export const useEditorSettings = (): SWRResponseWithUtils<EditorSettingsOperatio
   const { data: currentUser } = useCurrentUser();
   const { data: isGuestUser } = useIsGuestUser();
 
-  const swrResult = useSWRImmutable<IEditorSettings>(
+  const swrResult = useSWRImmutable(
     isGuestUser ? null : ['/personal-setting/editor-settings', currentUser?.username],
-    endpoint => apiv3Get(endpoint).then(result => result.data),
+    ([endpoint]) => apiv3Get(endpoint).then(result => result.data),
     {
       // use: [localStorageMiddleware], // store to localStorage for initialization fastly
       // fallbackData: undefined,
@@ -96,7 +96,7 @@ export const useSWRxSlackChannels = (currentPagePath: Nullable<string>): SWRResp
   const shouldFetch: boolean = currentPagePath != null;
   return useSWRImmutable(
     shouldFetch ? ['/pages.updatePost', currentPagePath] : null,
-    (endpoint, path) => apiGet(endpoint, { path }).then((response: SlackChannels) => response.updatePost),
+    ([endpoint, path]) => apiGet(endpoint, { path }).then((response: SlackChannels) => response.updatePost),
     { fallbackData: [''] },
   );
 };

+ 2 - 2
packages/app/src/stores/in-app-notification.ts

@@ -19,7 +19,7 @@ export const useSWRxInAppNotifications = <Data, Error>(
 ): SWRResponse<PaginateResult<IInAppNotification>, Error> => {
   return useSWR(
     ['/in-app-notification/list', limit, offset, status],
-    endpoint => apiv3Get(endpoint, { limit, offset, status }).then((response) => {
+    ([endpoint]) => apiv3Get(endpoint, { limit, offset, status }).then((response) => {
       const inAppNotificationPaginateResult = response.data as inAppNotificationPaginateResult;
       inAppNotificationPaginateResult.docs.forEach((doc) => {
         try {
@@ -39,7 +39,7 @@ export const useSWRxInAppNotifications = <Data, Error>(
 export const useSWRxInAppNotificationStatus = <Data, Error>(
 ): SWRResponse<number, Error> => {
   return useSWR(
-    ['/in-app-notification/status'],
+    '/in-app-notification/status',
     endpoint => apiv3Get(endpoint).then(response => response.data.count),
   );
 };

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

@@ -27,5 +27,5 @@ export const useIsMaintenanceMode = (initialData?: boolean): SWRResponseWithUtil
     },
   };
 
-  return withUtils(swrResult, utils);
+  return withUtils<maintenanceModeUtils, boolean>(swrResult, utils);
 };

+ 1 - 2
packages/app/src/stores/middlewares/user.ts

@@ -1,8 +1,7 @@
 import { Middleware, SWRHook } from 'swr';
 
-import { IUserHasId } from '~/interfaces/user';
-
 import { apiv3Put } from '~/client/util/apiv3-client';
+import { IUserHasId } from '~/interfaces/user';
 
 export const checkAndUpdateImageUrlCached: Middleware = (useSWRNext: SWRHook) => {
   return (key, fetcher, config) => {

+ 70 - 61
packages/app/src/stores/page-listing.tsx

@@ -1,5 +1,7 @@
+import assert from 'assert';
+
 import { Nullable, HasObjectId } from '@growi/core';
-import useSWR, { SWRResponse } from 'swr';
+import useSWR, { Arguments, mutate, SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRInfinite, { SWRInfiniteResponse } from 'swr/infinite';
 
@@ -13,14 +15,12 @@ import {
   AncestorsChildrenResult, ChildrenResult, V5MigrationStatus, RootPageResult,
 } from '../interfaces/page-listing-results';
 
-import { useCurrentPagePath } from './page';
-import { ITermNumberManagerUtil, useTermNumberManager } from './use-static-swr';
 
 export const useSWRxPagesByPath = (path?: Nullable<string>): SWRResponse<IPageHasId[], Error> => {
   const findAll = true;
-  return useSWR<IPageHasId[], Error>(
+  return useSWR(
     path != null ? ['/page', path, findAll] : null,
-    (endpoint, path, findAll) => apiv3Get(endpoint, { path, findAll }).then(result => result.data.pages),
+    ([endpoint, path, findAll]) => apiv3Get(endpoint, { path, findAll }).then(result => result.data.pages),
   );
 };
 
@@ -31,60 +31,52 @@ export const useSWRxRecentlyUpdated = (): SWRResponse<(IPageHasId)[], Error> =>
   );
 };
 export const useSWRInifinitexRecentlyUpdated = () : SWRInfiniteResponse<(IPageHasId)[], Error> => {
-  const getKey = (page: number) => {
+  const getKey = (page: number): string => {
     return `/pages/recent?offset=${page + 1}`;
   };
   return useSWRInfinite(
     getKey,
-    (endpoint: string) => apiv3Get<{ pages:(IPageHasId)[] }>(endpoint).then(response => response.data?.pages),
+    endpoint => apiv3Get<{ pages:(IPageHasId)[] }>(endpoint).then(response => response.data?.pages),
     {
       revalidateFirstPage: false,
       revalidateAll: false,
     },
   );
 };
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-export const useSWRxPageList = (
-    path: string | null, pageNumber?: number, termNumber?: number, limit?: number,
-): SWRResponse<IPagingResult<IPageHasId>, Error> => {
 
-  let key;
-  // if path not exist then the key is null
-  if (path == null) {
-    key = null;
-  }
-  else {
-    const pageListPath = `/pages/list?path=${path}&page=${pageNumber ?? 1}`;
-    // if limit exist then add it as query string
-    const requestPath = limit == null ? pageListPath : `${pageListPath}&limit=${limit}`;
-    key = [requestPath, termNumber];
-  }
-
-  return useSWR(
-    key,
-    (endpoint: string) => apiv3Get<{pages: IPageHasId[], totalCount: number, limit: number}>(endpoint).then((response) => {
-      return {
-        items: response.data.pages,
-        totalCount: response.data.totalCount,
-        limit: response.data.limit,
-      };
-    }),
+export const mutatePageList = async(): Promise<void[]> => {
+  return mutate(
+    key => Array.isArray(key) && key[0] === '/pages/list',
   );
 };
 
-export const useDescendantsPageListForCurrentPathTermManager = (isDisabled?: boolean) : SWRResponse<number, Error> & ITermNumberManagerUtil => {
-  return useTermNumberManager(isDisabled === true ? null : 'descendantsPageListForCurrentPathTermNumber');
-};
-
-export const useSWRxDescendantsPageListForCurrrentPath = (pageNumber?: number, limit?:number): SWRResponse<IPagingResult<IPageHasId>, Error> => {
-  const { data: currentPagePath } = useCurrentPagePath();
-  const { data: termNumber } = useDescendantsPageListForCurrentPathTermManager();
-
-  const path = currentPagePath == null || termNumber == null
-    ? null
-    : currentPagePath;
-
-  return useSWRxPageList(path, pageNumber, termNumber, limit);
+export const useSWRxPageList = (
+    path: string | null, pageNumber?: number, limit?: number,
+): SWRResponse<IPagingResult<IPageHasId>, Error> => {
+  return useSWR(
+    path == null
+      ? null
+      : ['/pages/list', path, pageNumber, limit],
+    ([endpoint, path, pageNumber, limit]) => {
+      const args = Object.assign(
+        { path, page: pageNumber ?? 1 },
+        // if limit exist then add it as query string
+        (limit != null) ? { limit } : {},
+      );
+
+      return apiv3Get<{pages: IPageHasId[], totalCount: number, limit: number}>(endpoint, args)
+        .then((response) => {
+          return {
+            items: response.data.pages,
+            totalCount: response.data.totalCount,
+            limit: response.data.limit,
+          };
+        });
+    },
+    {
+      keepPreviousData: true,
+    },
+  );
 };
 
 
@@ -105,9 +97,9 @@ export const useSWRxPageInfoForList = (
 
   const shouldFetch = (pageIds != null && pageIds.length > 0) || path != null;
 
-  const swrResult = useSWRImmutable<Record<string, IPageInfoForListing>>(
+  const swrResult = useSWRImmutable(
     shouldFetch ? ['/page-listing/info', pageIds, path, attachBookmarkCount, attachShortBody] : null,
-    (endpoint, pageIds, path, attachBookmarkCount, attachShortBody) => {
+    ([endpoint, pageIds, path, attachBookmarkCount, attachShortBody]) => {
       return apiv3Get(endpoint, {
         pageIds, path, attachBookmarkCount, attachShortBody,
       }).then(response => response.data);
@@ -133,10 +125,6 @@ export const useSWRxPageInfoForList = (
   };
 };
 
-export const usePageTreeTermManager = (isDisabled?: boolean) : SWRResponse<number, Error> & ITermNumberManagerUtil => {
-  return useTermNumberManager(isDisabled === true ? null : 'pageTreeTermManager');
-};
-
 export const useSWRxRootPage = (): SWRResponse<RootPageResult, Error> => {
   return useSWRImmutable(
     '/page-listing/root',
@@ -145,27 +133,41 @@ export const useSWRxRootPage = (): SWRResponse<RootPageResult, Error> => {
         rootPage: response.data.rootPage,
       };
     }),
+    {
+      keepPreviousData: true,
+    },
   );
 };
 
+const MUTATION_ID_FOR_PAGETREE = 'pageTree';
+const keyMatcherForPageTree = (key: Arguments): boolean => {
+  return Array.isArray(key) && key[0] === MUTATION_ID_FOR_PAGETREE;
+};
+export const mutatePageTree = async(): Promise<undefined[]> => {
+  return mutate(keyMatcherForPageTree);
+};
+
 export const useSWRxPageAncestorsChildren = (
     path: string | null,
 ): SWRResponse<AncestorsChildrenResult, Error> => {
-  const { data: termNumber } = usePageTreeTermManager();
+  const key = path ? [MUTATION_ID_FOR_PAGETREE, '/page-listing/ancestors-children', path] : null;
 
-  // HACKME: Consider using global mutation from useSWRConfig and not to use term number -- 2022/12/08 @hakumizuki
-  const prevTermNumber = termNumber ? termNumber - 1 : 0;
-  const prevSWRRes = useSWRImmutable(path ? [`/page-listing/ancestors-children?path=${path}`, prevTermNumber] : null);
+  // take care of the degration
+  // see: https://github.com/weseek/growi/pull/7038
+
+  if (key != null) {
+    assert(keyMatcherForPageTree(key));
+  }
 
   return useSWRImmutable(
-    path ? [`/page-listing/ancestors-children?path=${path}`, termNumber] : null,
-    endpoint => apiv3Get(endpoint).then((response) => {
+    key,
+    ([, endpoint, path]) => apiv3Get(endpoint, { path }).then((response) => {
       return {
         ancestorsChildren: response.data.ancestorsChildren,
       };
     }),
     {
-      fallbackData: prevSWRRes.data, // avoid data to be undefined due to the termNumber to change
+      keepPreviousData: true,
     },
   );
 };
@@ -173,15 +175,22 @@ export const useSWRxPageAncestorsChildren = (
 export const useSWRxPageChildren = (
     id?: string | null,
 ): SWRResponse<ChildrenResult, Error> => {
-  const { data: termNumber } = usePageTreeTermManager();
+  const key = id ? [MUTATION_ID_FOR_PAGETREE, '/page-listing/children', id] : null;
+
+  if (key != null) {
+    assert(keyMatcherForPageTree(key));
+  }
 
   return useSWR(
-    id ? [`/page-listing/children?id=${id}`, termNumber] : null,
-    endpoint => apiv3Get(endpoint).then((response) => {
+    key,
+    ([, endpoint, id]) => apiv3Get(endpoint, { id }).then((response) => {
       return {
         children: response.data.children,
       };
     }),
+    {
+      keepPreviousData: true,
+    },
   );
 };
 

+ 77 - 68
packages/app/src/stores/page.tsx

@@ -1,11 +1,12 @@
-import { useEffect } from 'react';
+import { useEffect, useMemo } from 'react';
 
 import type {
   IPageInfoForEntity, IPagePopulatedToShowRevision, Nullable,
 } from '@growi/core';
 import { isClient, pagePathUtils } from '@growi/core';
-import useSWR, { Key, SWRConfiguration, SWRResponse } from 'swr';
+import useSWR, { mutate, SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
+import useSWRMutation, { SWRMutationResponse } from 'swr/mutation';
 
 import { apiGet } from '~/client/util/apiv1-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
@@ -18,56 +19,33 @@ import { IRevisionsForPagination } from '~/interfaces/revision';
 import { IPageTagsInfo } from '../interfaces/tag';
 
 import {
-  useCurrentPageId, useCurrentPathname, useShareLinkId, useIsGuestUser,
+  useCurrentPageId, useCurrentPathname, useShareLinkId, useIsGuestUser, useIsNotFound,
 } from './context';
-import { ITermNumberManagerUtil, useTermNumberManager } from './use-static-swr';
 
 const { isPermalink: _isPermalink } = pagePathUtils;
 
-export const useSWRxPage = (
-    pageId?: string|null,
-    shareLinkId?: string,
-    revisionId?: string,
-    initialData?: IPagePopulatedToShowRevision|null,
-    config?: SWRConfiguration,
-): SWRResponse<IPagePopulatedToShowRevision|null, Error> => {
-  const swrResponse = useSWRImmutable<IPagePopulatedToShowRevision|null, Error>(
-    pageId != null ? ['/page', pageId, shareLinkId, revisionId] : null,
-    // TODO: upgrade SWR to v2 and use useSWRMutation
-    //        in order to avoid complicated fetcher settings
-    Object.assign({
-      fetcher: (endpoint, pageId, shareLinkId, revisionId) => apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { pageId, shareLinkId, revisionId })
-        .then(result => result.data.page)
-        .catch((errs) => {
-          if (!Array.isArray(errs)) { throw Error('error is not array') }
-          const statusCode = errs[0].status;
-          if (statusCode === 403 || statusCode === 404) {
-            // for NotFoundPage
-            return null;
-          }
-          throw Error('failed to get page');
-        }),
-    }, config ?? {}),
-  );
+
+export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision|null): SWRResponse<IPagePopulatedToShowRevision|null> => {
+  const key = 'currentPage';
 
   useEffect(() => {
     if (initialData !== undefined) {
-      swrResponse.mutate(initialData);
+      mutate(key, initialData, {
+        optimisticData: initialData,
+        populateCache: true,
+        revalidate: false,
+      });
     }
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [initialData]); // Only depends on `initialData`
+  }, [initialData, key]);
 
-  return swrResponse;
+  return useSWR(key, null, {
+    keepPreviousData: true,
+  });
 };
 
-export const useSWRxPageByPath = (path?: string): SWRResponse<IPagePopulatedToShowRevision, Error> => {
-  return useSWR<IPagePopulatedToShowRevision, Error>(
-    path != null ? ['/page', path] : null,
-    (endpoint, path) => apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { path }).then(result => result.data.page),
-  );
-};
+export const useSWRMUTxCurrentPage = (): SWRMutationResponse<IPagePopulatedToShowRevision|null> => {
+  const key = 'currentPage';
 
-export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision|null): SWRResponse<IPagePopulatedToShowRevision|null, Error> => {
   const { data: currentPageId } = useCurrentPageId();
   const { data: shareLinkId } = useShareLinkId();
 
@@ -79,53 +57,81 @@ export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision|nu
     revisionId = requestRevisionId != null ? requestRevisionId : undefined;
   }
 
-  const swrResult = useSWRxPage(
-    currentPageId, shareLinkId, revisionId,
-    initialData,
-    // overwrite fetcher if the current page is share link
-    shareLinkId == null
-      ? undefined
-      : {
-        fetcher: () => null,
-      },
+  return useSWRMutation(
+    key,
+    async() => {
+      return apiv3Get<{ page: IPagePopulatedToShowRevision }>('/page', { pageId: currentPageId, shareLinkId, revisionId })
+        .then(result => result.data.page)
+        .catch((errs) => {
+          if (!Array.isArray(errs)) { throw Error('error is not array') }
+          const statusCode = errs[0].status;
+          if (statusCode === 403 || statusCode === 404) {
+            // for NotFoundPage
+            return null;
+          }
+          throw Error('failed to get page');
+        });
+    },
+    {
+      populateCache: true,
+      revalidate: false,
+    },
   );
-
-  return swrResult;
 };
 
+export const useSWRxPageByPath = (path?: string): SWRResponse<IPagePopulatedToShowRevision, Error> => {
+  return useSWR(
+    path != null ? ['/page', path] : null,
+    ([endpoint, path]) => apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { path }).then(result => result.data.page),
+  );
+};
 
 export const useSWRxTagsInfo = (pageId: Nullable<string>): SWRResponse<IPageTagsInfo | undefined, Error> => {
   const { data: shareLinkId } = useShareLinkId();
 
   const endpoint = `/pages.getPageTag?pageId=${pageId}`;
 
-  return useSWRImmutable<IPageTagsInfo | undefined, Error>(
+  return useSWRImmutable(
     shareLinkId == null && pageId != null ? [endpoint, pageId] : null,
-    (endpoint, pageId) => apiGet<IPageTagsInfo>(endpoint, { pageId }).then(result => result),
+    ([endpoint, pageId]) => apiGet<IPageTagsInfo>(endpoint, { pageId }).then(result => result),
   );
 };
 
-export const usePageInfoTermManager = (isDisabled?: boolean) : SWRResponse<number, Error> & ITermNumberManagerUtil => {
-  return useTermNumberManager(isDisabled === true ? null : 'pageInfoTermNumber');
+export const mutateAllPageInfo = (): Promise<void[]> => {
+  return mutate(
+    key => Array.isArray(key) && key[0] === '/page/info',
+  );
 };
 
 export const useSWRxPageInfo = (
     pageId: string | null | undefined,
     shareLinkId?: string | null,
     initialData?: IPageInfoForEntity,
-): SWRResponse<IPageInfo | IPageInfoForOperation, Error> => {
-
-  const { data: termNumber } = usePageInfoTermManager();
+): SWRResponse<IPageInfo | IPageInfoForOperation> => {
 
   // assign null if shareLinkId is undefined in order to identify SWR key only by pageId
   const fixedShareLinkId = shareLinkId ?? null;
 
-  const swrResult = useSWRImmutable<IPageInfo | IPageInfoForOperation, Error>(
-    pageId != null && termNumber != null ? ['/page/info', pageId, fixedShareLinkId, termNumber] : null,
-    (endpoint, pageId, shareLinkId) => apiv3Get(endpoint, { pageId, shareLinkId }).then(response => response.data),
+  const key = useMemo(() => {
+    return pageId != null ? ['/page/info', pageId, fixedShareLinkId] : null;
+  }, [fixedShareLinkId, pageId]);
+
+  const swrResult = useSWRImmutable(
+    key,
+    ([endpoint, pageId, shareLinkId]: [string, string, string|null]) => apiv3Get(endpoint, { pageId, shareLinkId }).then(response => response.data),
     { fallbackData: initialData },
   );
 
+  useEffect(() => {
+    if (initialData !== undefined) {
+      mutate(key, initialData, {
+        optimisticData: initialData,
+        populateCache: true,
+        revalidate: false,
+      });
+    }
+  }, [initialData, key]);
+
   return swrResult;
 };
 
@@ -135,9 +141,9 @@ export const useSWRxPageRevisions = (
     pageId: string | null | undefined,
 ): SWRResponse<IRevisionsForPagination, Error> => {
 
-  return useSWRImmutable<IRevisionsForPagination, Error>(
+  return useSWRImmutable(
     ['/revisions/list', pageId, page, limit],
-    (endpoint, pageId, page, limit) => {
+    ([endpoint, pageId, page, limit]) => {
       return apiv3Get(endpoint, { pageId, page, limit }).then((response) => {
         const revisions = {
           revisions: response.data.docs,
@@ -157,12 +163,15 @@ export const useSWRxIsGrantNormalized = (
 ): SWRResponse<IResIsGrantNormalized, Error> => {
 
   const { data: isGuestUser } = useIsGuestUser();
+  const { data: isNotFound } = useIsNotFound();
 
-  const key = !isGuestUser && pageId != null ? ['/page/is-grant-normalized', pageId] : null;
+  const key = !isGuestUser && !isNotFound && pageId != null
+    ? ['/page/is-grant-normalized', pageId]
+    : null;
 
   return useSWRImmutable(
     key,
-    (endpoint, pageId) => apiv3Get(endpoint, { pageId }).then(response => response.data),
+    ([endpoint, pageId]) => apiv3Get(endpoint, { pageId }).then(response => response.data),
   );
 };
 
@@ -172,7 +181,7 @@ export const useSWRxApplicableGrant = (
 
   return useSWRImmutable(
     pageId != null ? ['/page/applicable-grant', pageId] : null,
-    (endpoint, pageId) => apiv3Get(endpoint, { pageId }).then(response => response.data),
+    ([endpoint, pageId]) => apiv3Get(endpoint, { pageId }).then(response => response.data),
   );
 };
 
@@ -187,7 +196,7 @@ export const useCurrentPagePath = (): SWRResponse<string | undefined, Error> =>
 
   return useSWRImmutable(
     ['currentPagePath', currentPage?.path, currentPathname],
-    (key: Key, pagePath: string|undefined, pathname: string|undefined) => {
+    ([, , pathname]) => {
       if (currentPage?.path != null) {
         return currentPage.path;
       }
@@ -206,7 +215,7 @@ export const useIsTrashPage = (): SWRResponse<boolean, Error> => {
 
   return useSWRImmutable(
     pagePath == null ? null : ['isTrashPage', pagePath],
-    (key: Key, pagePath: string) => pagePathUtils.isTrashPage(pagePath),
+    ([, pagePath]) => pagePathUtils.isTrashPage(pagePath),
     // TODO: set fallbackData
     // { fallbackData:  }
   );

+ 49 - 44
packages/app/src/stores/renderer.tsx

@@ -1,3 +1,7 @@
+import {
+  useCallback, useEffect, useRef,
+} from 'react';
+
 import { HtmlElementNode } from 'rehype-toc';
 import useSWR, { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
@@ -17,19 +21,30 @@ import { useCurrentPagePath } from './page';
 import { useCurrentPageTocNode } from './ui';
 
 
-export const useViewOptions = (storeTocNodeHandler: (toc: HtmlElementNode) => void): SWRResponse<RendererOptions, Error> => {
+export const useViewOptions = (): SWRResponse<RendererOptions, Error> => {
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: rendererConfig } = useRendererConfig();
+  const { mutate: mutateCurrentPageTocNode } = useCurrentPageTocNode();
 
-  const isAllDataValid = currentPagePath != null && rendererConfig != null;
+  const tocRef = useRef<HtmlElementNode|undefined>();
 
-  const key = isAllDataValid
-    ? ['viewOptions', currentPagePath, rendererConfig]
-    : null;
+  const storeTocNodeHandler = useCallback((toc: HtmlElementNode) => {
+    tocRef.current = toc;
+  }, []);
+
+  useEffect(() => {
+    mutateCurrentPageTocNode(tocRef.current);
+  // using useRef not to re-render
+  // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [mutateCurrentPageTocNode, tocRef.current]);
+
+  const isAllDataValid = currentPagePath != null && rendererConfig != null;
 
-  return useSWR<RendererOptions, Error>(
-    key,
-    (rendererId, currentPagePath, rendererConfig) => {
+  return useSWR(
+    isAllDataValid
+      ? ['viewOptions', currentPagePath, rendererConfig]
+      : null,
+    ([, currentPagePath, rendererConfig]) => {
       // determine options generator
       const optionsGenerator = getGrowiFacade().markdownRenderer?.optionsGenerators?.customGenerateViewOptions ?? generateViewOptions;
       return optionsGenerator(currentPagePath, rendererConfig, storeTocNodeHandler);
@@ -47,13 +62,11 @@ export const useTocOptions = (): SWRResponse<RendererOptions, Error> => {
 
   const isAllDataValid = currentPagePath != null && rendererConfig != null && tocNode != null;
 
-  const key = isAllDataValid
-    ? ['tocOptions', currentPagePath, tocNode, rendererConfig]
-    : null;
-
-  return useSWRImmutable<RendererOptions, Error>(
-    key,
-    (rendererId, path, tocNode, rendererConfig) => generateTocOptions(rendererConfig, tocNode),
+  return useSWRImmutable(
+    isAllDataValid
+      ? ['tocOptions', currentPagePath, tocNode, rendererConfig]
+      : null,
+    ([, , tocNode, rendererConfig]) => generateTocOptions(rendererConfig, tocNode),
     {
       fallbackData: isAllDataValid ? generateTocOptions(rendererConfig, tocNode) : undefined,
     },
@@ -66,16 +79,14 @@ export const usePreviewOptions = (): SWRResponse<RendererOptions, Error> => {
 
   const isAllDataValid = currentPagePath != null && rendererConfig != null;
 
-  const key = isAllDataValid
-    ? ['previewOptions', rendererConfig, currentPagePath]
-    : null;
-
-  return useSWR<RendererOptions, Error>(
-    key,
-    (rendererId, rendererConfig, pagePath, highlightKeywords) => {
+  return useSWR(
+    isAllDataValid
+      ? ['previewOptions', rendererConfig, currentPagePath]
+      : null,
+    ([, rendererConfig, pagePath]) => {
       // determine options generator
       const optionsGenerator = getGrowiFacade().markdownRenderer?.optionsGenerators?.customGeneratePreviewOptions ?? generatePreviewOptions;
-      return optionsGenerator(rendererConfig, pagePath, highlightKeywords);
+      return optionsGenerator(rendererConfig, pagePath);
     },
     {
       fallbackData: isAllDataValid ? generatePreviewOptions(rendererConfig, currentPagePath) : undefined,
@@ -89,13 +100,11 @@ export const useCommentForCurrentPageOptions = (): SWRResponse<RendererOptions,
 
   const isAllDataValid = currentPagePath != null && rendererConfig != null;
 
-  const key = isAllDataValid
-    ? ['commentPreviewOptions', rendererConfig, currentPagePath]
-    : null;
-
-  return useSWRImmutable<RendererOptions, Error>(
-    key,
-    (rendererId, rendererConfig, currentPagePath) => generateSimpleViewOptions(
+  return useSWRImmutable(
+    isAllDataValid
+      ? ['commentPreviewOptions', rendererConfig, currentPagePath]
+      : null,
+    ([, rendererConfig, currentPagePath]) => generateSimpleViewOptions(
       rendererConfig,
       currentPagePath,
       undefined,
@@ -118,13 +127,11 @@ export const useSelectedPagePreviewOptions = (pagePath: string, highlightKeyword
 
   const isAllDataValid = rendererConfig != null;
 
-  const key = isAllDataValid
-    ? ['selectedPagePreviewOptions', rendererConfig, pagePath, highlightKeywords]
-    : null;
-
-  return useSWRImmutable<RendererOptions, Error>(
-    key,
-    (rendererId, rendererConfig, pagePath, highlightKeywords) => generateSimpleViewOptions(rendererConfig, pagePath, highlightKeywords),
+  return useSWRImmutable(
+    isAllDataValid
+      ? ['selectedPagePreviewOptions', rendererConfig, pagePath, highlightKeywords]
+      : null,
+    ([, rendererConfig, pagePath, highlightKeywords]) => generateSimpleViewOptions(rendererConfig, pagePath, highlightKeywords),
     {
       fallbackData: isAllDataValid ? generateSimpleViewOptions(rendererConfig, pagePath, highlightKeywords) : undefined,
     },
@@ -139,13 +146,11 @@ export const useCustomSidebarOptions = (): SWRResponse<RendererOptions, Error> =
 
   const isAllDataValid = rendererConfig != null;
 
-  const key = isAllDataValid
-    ? ['customSidebarOptions', rendererConfig]
-    : null;
-
-  return useSWRImmutable<RendererOptions, Error>(
-    key,
-    (rendererId, rendererConfig, pagePath, highlightKeywords) => generateSimpleViewOptions(rendererConfig, pagePath, highlightKeywords),
+  return useSWRImmutable(
+    isAllDataValid
+      ? ['customSidebarOptions', rendererConfig]
+      : null,
+    ([, rendererConfig]) => generateSimpleViewOptions(rendererConfig, '/'),
     {
       fallbackData: isAllDataValid ? generateSimpleViewOptions(rendererConfig, '/') : undefined,
     },

+ 10 - 14
packages/app/src/stores/search.tsx

@@ -1,17 +1,9 @@
-import { SWRResponse } from 'swr';
+import { mutate, SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
 import { apiGet } from '~/client/util/apiv1-client';
-
 import { IFormattedSearchResult, SORT_AXIS, SORT_ORDER } from '~/interfaces/search';
 
-import { ITermNumberManagerUtil, useTermNumberManager } from './use-static-swr';
-
-
-export const useFullTextSearchTermManager = (isDisabled?: boolean) : SWRResponse<number, Error> & ITermNumberManagerUtil => {
-  return useTermNumberManager(isDisabled === true ? null : 'fullTextSearchTermNumber');
-};
-
 
 export type ISearchConfigurations = {
   limit: number,
@@ -50,11 +42,15 @@ const createSearchQuery = (keyword: string, includeTrashPages: boolean, includeU
   return query;
 };
 
+export const mutateSearching = async(): Promise<void[]> => {
+  return mutate(
+    key => Array.isArray(key) && key[0] === '/search',
+  );
+};
+
 export const useSWRxSearch = (
-    keyword: string | null, nqName: string | null, configurations: ISearchConfigurations, disableTermManager = false,
+    keyword: string | null, nqName: string | null, configurations: ISearchConfigurations,
 ): SWRResponse<IFormattedSearchResult, Error> & { conditions: ISearchConditions } => {
-  const { data: termNumber } = useFullTextSearchTermManager(disableTermManager);
-
   const {
     limit, offset, sort, order, includeTrashPages, includeUserPages,
   } = configurations;
@@ -72,8 +68,8 @@ export const useSWRxSearch = (
   const isKeywordValid = keyword != null && keyword.length > 0;
 
   const swrResult = useSWRImmutable(
-    isKeywordValid ? ['/search', keyword, fixedConfigurations, termNumber] : null,
-    (endpoint, keyword, fixedConfigurations) => {
+    isKeywordValid ? ['/search', keyword, fixedConfigurations] : null,
+    ([endpoint, , fixedConfigurations]) => {
       const {
         limit, offset, sort, order,
       } = fixedConfigurations;

+ 4 - 1
packages/app/src/stores/share-link.tsx

@@ -10,5 +10,8 @@ const fetchShareLinks = async(endpoint, pageId) => {
 };
 
 export const useSWRxSharelink = (currentPageId: Nullable<string>): SWRResponse<IResShareLinkList['shareLinksResult'], Error> => {
-  return useSWR(currentPageId == null ? null : ['/share-links/', currentPageId], (endpoint => fetchShareLinks(endpoint, currentPageId)));
+  return useSWR(
+    currentPageId == null ? null : ['/share-links/', currentPageId],
+    ([endpoint]) => fetchShareLinks(endpoint, currentPageId),
+  );
 };

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

@@ -7,13 +7,13 @@ import { IResTagsListApiv1, IResTagsSearchApiv1 } from '~/interfaces/tag';
 export const useSWRxTagsList = (limit?: number, offset?: number): SWRResponse<IResTagsListApiv1, Error> => {
   return useSWRImmutable(
     ['/tags.list', limit, offset],
-    (endpoint, limit, offset) => apiGet(endpoint, { limit, offset }).then((result: IResTagsListApiv1) => result),
+    ([endpoint, limit, offset]) => apiGet(endpoint, { limit, offset }).then((result: IResTagsListApiv1) => result),
   );
 };
 
 export const useSWRxTagsSearch = (query: string): SWRResponse<IResTagsSearchApiv1, Error> => {
   return useSWRImmutable(
     ['/tags.search', query],
-    (endpoint, query) => apiGet(endpoint, { q: query }).then((result: IResTagsSearchApiv1) => result),
+    ([endpoint, query]) => apiGet(endpoint, { q: query }).then((result: IResTagsSearchApiv1) => result),
   );
 };

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

@@ -48,7 +48,7 @@ yyyy/mm/dd (予定、時間は追って連絡)`,
 ];
 
 export const useTemplates = (): SWRResponse<ITemplate[], Error> => {
-  return useSWR<ITemplate[], Error>(
+  return useSWR(
     'templates',
     () => [
       ...presetTemplates,

+ 27 - 21
packages/app/src/stores/ui.tsx

@@ -8,7 +8,7 @@ import { Breakpoint, addBreakpointListener, cleanupBreakpointListener } from '@g
 import { HtmlElementNode } from 'rehype-toc';
 import type SimpleBar from 'simplebar-react';
 import {
-  useSWRConfig, SWRResponse, Key, Fetcher,
+  useSWRConfig, SWRResponse, Key,
 } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
@@ -155,7 +155,7 @@ export const useEditorMode = (): SWRResponseWithUtils<EditorModeUtils, EditorMod
   const isEditable = !isLoading && _isEditable;
   const initialData = isEditable ? editorModeByHash : EditorMode.View;
 
-  const swrResponse = useSWRImmutable<EditorMode>(
+  const swrResponse = useSWRImmutable(
     isLoading ? null : ['editorMode', isEditable],
     null,
     { fallbackData: initialData },
@@ -199,8 +199,8 @@ export const useIsDeviceSmallerThanMd = (): SWRResponse<boolean, Error> => {
       const mql = addBreakpointListener(Breakpoint.MD, mdOrAvobeHandler);
 
       // initialize
-      if (cache.get(key) == null) {
-        mutate(key, !mql.matches);
+      if (cache.get(key)?.data == null) {
+        cache.set(key, { ...cache.get(key), data: !mql.matches });
       }
 
       return () => {
@@ -227,8 +227,8 @@ export const useIsDeviceSmallerThanLg = (): SWRResponse<boolean, Error> => {
       const mql = addBreakpointListener(Breakpoint.LG, lgOrAvobeHandler);
 
       // initialize
-      if (cache.get(key) == null) {
-        mutate(key, !mql.matches);
+      if (cache.get(key)?.data == null) {
+        cache.set(key, { ...cache.get(key), data: !mql.matches });
       }
 
       return () => {
@@ -260,7 +260,7 @@ export const usePreferDrawerModeByUser = (initialData?: boolean): SWRResponseWit
     },
   };
 
-  return withUtils(swrResponse, utils);
+  return withUtils<PreferDrawerModeByUserUtils>(swrResponse, utils);
 
 };
 
@@ -286,29 +286,36 @@ export const useDrawerMode = (): SWRResponse<boolean, Error> => {
   const { data: editorMode } = useEditorMode();
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
 
-  const condition = editorMode != null || preferDrawerModeByUser != null || preferDrawerModeOnEditByUser != null || isDeviceSmallerThanMd != null;
+  const condition = editorMode != null && preferDrawerModeByUser != null && preferDrawerModeOnEditByUser != null && isDeviceSmallerThanMd != null;
 
-  const calcDrawerMode: Fetcher<boolean> = (
-      key: Key, editorMode: EditorMode, preferDrawerModeByUser: boolean, preferDrawerModeOnEditByUser: boolean, isDeviceSmallerThanMd: boolean,
+  const calcDrawerMode = (
+      endpoint: string,
+      editorMode: EditorMode,
+      preferDrawerModeByUser: boolean,
+      preferDrawerModeOnEditByUser: boolean,
+      isDeviceSmallerThanMd: boolean,
   ): boolean => {
-
     // get preference on view or edit
     const preferDrawerMode = editorMode !== EditorMode.View ? preferDrawerModeOnEditByUser : preferDrawerModeByUser;
 
-    return isDeviceSmallerThanMd || preferDrawerMode;
+    return isDeviceSmallerThanMd ?? preferDrawerMode ?? false;
   };
 
   const isViewModeWithPreferDrawerMode = editorMode === EditorMode.View && preferDrawerModeByUser;
   const isEditModeWithPreferDrawerMode = editorMode === EditorMode.Editor && preferDrawerModeOnEditByUser;
-  const useFallbackData = isViewModeWithPreferDrawerMode || isEditModeWithPreferDrawerMode;
-  const fallbackOption = useFallbackData
-    ? { fallbackData: true }
-    : { fallback: calcDrawerMode };
+  const isDrawerModeFixed = isViewModeWithPreferDrawerMode || isEditModeWithPreferDrawerMode;
 
   return useSWRImmutable(
     condition ? ['isDrawerMode', editorMode, preferDrawerModeByUser, preferDrawerModeOnEditByUser, isDeviceSmallerThanMd] : null,
-    calcDrawerMode,
-    fallbackOption,
+    // calcDrawerMode,
+    key => calcDrawerMode(...key),
+    condition
+      ? {
+        fallbackData: isDrawerModeFixed
+          ? true
+          : calcDrawerMode('isDrawerMode', editorMode, preferDrawerModeByUser, preferDrawerModeOnEditByUser, isDeviceSmallerThanMd),
+      }
+      : undefined,
   );
 };
 
@@ -321,7 +328,7 @@ type SidebarConfigOption = {
 }
 
 export const useSWRxSidebarConfig = (): SWRResponse<ISidebarConfig, Error> & SidebarConfigOption => {
-  const swrResponse = useSWRImmutable<ISidebarConfig>(
+  const swrResponse = useSWRImmutable(
     '/customize-setting/sidebar',
     endpoint => apiv3Get(endpoint).then(result => result.data),
   );
@@ -439,8 +446,7 @@ export const useIsAbleToShowPageManagement = (): SWRResponse<boolean, Error> =>
 
   return useSWRImmutable(
     includesUndefined ? null : [key, pageId, isPageExist, isEmptyPage, isTrashPage, isSharedUser],
-    // eslint-disable-next-line max-len
-    (key: string, pageId: string, isPageExist: boolean, isTrashPage: boolean, isSharedUser: boolean) => (isPageExist && !isTrashPage && !isSharedUser) || isEmptyPage,
+    ([, , isPageExist, isEmptyPage, isTrashPage, isSharedUser]) => (isPageExist && !isTrashPage && !isSharedUser) || isEmptyPage,
   );
 };
 

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

@@ -23,12 +23,12 @@ export function useContextSWR<Data, Error>(
   const { cache } = useSWRConfig();
   const swrResponse = useSWRImmutable(key, null, {
     ...configuration,
-    fallbackData: configuration?.fallbackData ?? cache.get(key),
+    fallbackData: configuration?.fallbackData ?? cache.get(key)?.data,
   });
 
   // write data to cache directly
   if (data !== undefined) {
-    cache.set(key, data);
+    cache.set(key, { ...cache.get(key), data });
   }
 
   const result = Object.assign(swrResponse, { mutate: () => { throw Error('mutate can not be used in context') } });

+ 0 - 25
packages/app/src/stores/use-static-swr.tsx

@@ -33,28 +33,3 @@ export function useStaticSWR<Data, Error>(
 
   return swrResponse;
 }
-
-
-const ADVANCE_DELAY_MS = 800;
-
-export type ITermNumberManagerUtil = {
-  advance(): void,
-}
-
-export const useTermNumberManager = (key: Key) : SWRResponse<number, Error> & ITermNumberManagerUtil => {
-  const swrResult = useStaticSWR<number, Error>(key, undefined, { fallbackData: 0 });
-
-  return {
-    ...swrResult,
-    advance: () => {
-      const { data: currentNum } = swrResult;
-      if (currentNum == null) {
-        return;
-      }
-
-      setTimeout(() => {
-        swrResult.mutate(currentNum + 1);
-      }, ADVANCE_DELAY_MS);
-    },
-  };
-};

+ 11 - 11
packages/app/src/stores/user-group.tsx

@@ -25,13 +25,13 @@ export const useSWRxMyUserGroupRelations = (shouldFetch: boolean): SWRResponse<I
 
 export const useSWRxUserGroup = (groupId: string | undefined): SWRResponse<IUserGroupHasId, Error> => {
   return useSWRImmutable(
-    groupId != null ? [`/user-groups/${groupId}`] : null,
+    groupId != null ? `/user-groups/${groupId}` : null,
     endpoint => apiv3Get<UserGroupResult>(endpoint).then(result => result.data.userGroup),
   );
 };
 
 export const useSWRxUserGroupList = (initialData?: IUserGroupHasId[]): SWRResponse<IUserGroupHasId[], Error> => {
-  return useSWRImmutable<IUserGroupHasId[], Error>(
+  return useSWRImmutable(
     '/user-groups',
     endpoint => apiv3Get<UserGroupListResult>(endpoint, { pagination: false }).then(result => result.data.userGroups),
     {
@@ -44,9 +44,9 @@ export const useSWRxChildUserGroupList = (
     parentIds?: string[], includeGrandChildren?: boolean,
 ): SWRResponse<ChildUserGroupListResult, Error> => {
   const shouldFetch = parentIds != null && parentIds.length > 0;
-  return useSWRImmutable<ChildUserGroupListResult, Error>(
+  return useSWRImmutable(
     shouldFetch ? ['/user-groups/children', parentIds, includeGrandChildren] : null,
-    (endpoint, parentIds, includeGrandChildren) => apiv3Get<ChildUserGroupListResult>(
+    ([endpoint, parentIds, includeGrandChildren]) => apiv3Get<ChildUserGroupListResult>(
       endpoint, { parentIds, includeGrandChildren },
     ).then((result => result.data)),
   );
@@ -54,7 +54,7 @@ export const useSWRxChildUserGroupList = (
 
 export const useSWRxUserGroupRelations = (groupId: string): SWRResponse<IUserGroupRelationHasIdPopulatedUser[], Error> => {
   return useSWRImmutable(
-    groupId != null ? [`/user-groups/${groupId}/user-group-relations`] : null,
+    groupId != null ? `/user-groups/${groupId}/user-group-relations` : null,
     endpoint => apiv3Get<UserGroupRelationsResult>(endpoint).then(result => result.data.userGroupRelations),
   );
 };
@@ -62,9 +62,9 @@ export const useSWRxUserGroupRelations = (groupId: string): SWRResponse<IUserGro
 export const useSWRxUserGroupRelationList = (
     groupIds: string[] | undefined, childGroupIds?: string[], initialData?: IUserGroupRelationHasId[],
 ): SWRResponse<IUserGroupRelationHasId[], Error> => {
-  return useSWRImmutable<IUserGroupRelationHasId[], Error>(
+  return useSWRImmutable(
     groupIds != null ? ['/user-group-relations', groupIds, childGroupIds] : null,
-    (endpoint, groupIds, childGroupIds) => apiv3Get<UserGroupRelationListResult>(
+    ([endpoint, groupIds, childGroupIds]) => apiv3Get<UserGroupRelationListResult>(
       endpoint, { groupIds, childGroupIds },
     ).then(result => result.data.userGroupRelations),
     {
@@ -76,27 +76,27 @@ export const useSWRxUserGroupRelationList = (
 export const useSWRxUserGroupPages = (groupId: string | undefined, limit: number, offset: number): SWRResponse<IPageHasId[], Error> => {
   return useSWRImmutable(
     groupId != null ? [`/user-groups/${groupId}/pages`, limit, offset] : null,
-    endpoint => apiv3Get<UserGroupPagesResult>(endpoint, { limit, offset }).then(result => result.data.pages),
+    ([endpoint, limit, offset]) => apiv3Get<UserGroupPagesResult>(endpoint, { limit, offset }).then(result => result.data.pages),
   );
 };
 
 export const useSWRxSelectableParentUserGroups = (groupId: string | undefined): SWRResponse<IUserGroupHasId[], Error> => {
   return useSWRImmutable(
     groupId != null ? ['/user-groups/selectable-parent-groups', groupId] : null,
-    endpoint => apiv3Get<SelectableParentUserGroupsResult>(endpoint, { groupId }).then(result => result.data.selectableParentGroups),
+    ([endpoint, groupId]) => apiv3Get<SelectableParentUserGroupsResult>(endpoint, { groupId }).then(result => result.data.selectableParentGroups),
   );
 };
 
 export const useSWRxSelectableChildUserGroups = (groupId: string | undefined): SWRResponse<IUserGroupHasId[], Error> => {
   return useSWRImmutable(
     groupId != null ? ['/user-groups/selectable-child-groups', groupId] : null,
-    endpoint => apiv3Get<SelectableUserChildGroupsResult>(endpoint, { groupId }).then(result => result.data.selectableChildGroups),
+    ([endpoint, groupId]) => apiv3Get<SelectableUserChildGroupsResult>(endpoint, { groupId }).then(result => result.data.selectableChildGroups),
   );
 };
 
 export const useSWRxAncestorUserGroups = (groupId: string | undefined): SWRResponse<IUserGroupHasId[], Error> => {
   return useSWRImmutable(
     groupId != null ? ['/user-groups/ancestors', groupId] : null,
-    endpoint => apiv3Get<AncestorUserGroupsResult>(endpoint, { groupId }).then(result => result.data.ancestorUserGroups),
+    ([endpoint, groupId]) => apiv3Get<AncestorUserGroupsResult>(endpoint, { groupId }).then(result => result.data.ancestorUserGroups),
   );
 };

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

@@ -9,7 +9,7 @@ export const useSWRxUsersList = (userIds: string[]): SWRResponse<IUserHasId[], E
   const distinctUserIds = userIds.length > 0 ? Array.from(new Set(userIds)).sort() : [];
   return useSWR(
     distinctUserIds.length > 0 ? ['/users/list', distinctUserIds] : null,
-    (endpoint, userIds) => apiv3Get(endpoint, { userIds: userIds.join(',') }).then((response) => {
+    ([endpoint, userIds]) => apiv3Get(endpoint, { userIds: userIds.join(',') }).then((response) => {
       return response.data.users;
     }),
     {
@@ -43,7 +43,7 @@ type usernameResult = {
 export const useSWRxUsernames = (q: string, offset?: number, limit?: number, options?: usernameRequestOptions): SWRResponse<usernameResult, Error> => {
   return useSWRImmutable(
     (q != null && q.trim() !== '') ? ['/users/usernames', q, offset, limit, options] : null,
-    (endpoint, q, offset, limit, options) => apiv3Get(endpoint, {
+    ([endpoint, q, offset, limit, options]) => apiv3Get(endpoint, {
       q, offset, limit, options,
     }).then(result => result.data),
   );

+ 3 - 1
packages/app/src/stores/xss.ts

@@ -1,8 +1,10 @@
 
-import { useStaticSWR } from './use-static-swr';
 import { SWRResponse } from 'swr';
+
 import Xss from '~/services/xss';
 
+import { useStaticSWR } from './use-static-swr';
+
 export const useXss = (initialData?: Xss): SWRResponse<Xss, Error> => {
   return useStaticSWR<Xss, Error>('xss', initialData);
 };

+ 12 - 8
packages/app/src/utils/swr-utils.ts

@@ -1,9 +1,13 @@
-import { ProviderConfiguration, PublicConfiguration } from 'swr/dist/types';
+import { isServer } from '@growi/core';
 
-export type SWRConfigValue = Partial<PublicConfiguration> & Partial<ProviderConfiguration> & {
-  provider?: (cache) => any | undefined,
-};
-
-export const swrGlobalConfiguration: SWRConfigValue = {
-  errorRetryCount: 1,
-};
+export const swrGlobalConfiguration = Object.assign(
+  {
+    errorRetryCount: 1,
+  },
+  // set the request scoped cache provider in server
+  isServer()
+    ? {
+      provider: (cache: any) => new Map<string, any>(cache),
+    }
+    : {},
+);

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

@@ -68,6 +68,61 @@ context('Access to page', () => {
     cy.screenshot(`${ssPrefix}-Sandbox-edit-page`);
   })
 
+  it('View and Edit contents are successfully loaded', () => {
+    const body1 = 'hello';
+    cy.visit('/Sandbox/testForUseEditingMarkdown');
+
+    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('.grw-editor-navbar-bottom').should('be.visible');
+
+    // check edited contents after save
+    cy.get('.CodeMirror').type(body1);
+    cy.get('.CodeMirror').contains(body1);
+    cy.get('.page-editor-preview-body').contains(body1);
+    cy.getByTestid('page-editor').should('be.visible');
+    cy.getByTestid('save-page-btn').click();
+    cy.get('.wiki').should('be.visible');
+    cy.get('.wiki').children().first().should('have.text', body1);
+  })
+
+  it('Editing contents are successfully loaded with shortcut key', () => {
+    const body2 = ' world!';
+    const savePageShortcutKey = '{ctrl+s}';
+
+    cy.visit('/Sandbox/testForUseEditingMarkdown');
+
+    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('.grw-editor-navbar-bottom').should('be.visible');
+
+    // check editing contents with shortcut key
+    cy.get('.CodeMirror-line').children().first().invoke('text').then((text) => {
+      cy.get('.CodeMirror').type(body2);
+      cy.get('.CodeMirror').contains(text+body2);
+      cy.get('.page-editor-preview-body').contains(text+body2);
+      cy.get('.CodeMirror').type(savePageShortcutKey);
+      cy.get('.CodeMirror').contains(text+body2);
+      cy.get('.page-editor-preview-body').contains(text+body2);
+    })
+  })
+
   it('/user/admin is successfully loaded', () => {
     cy.visit('/user/admin');
 

+ 12 - 18
yarn.lock

@@ -4437,18 +4437,10 @@
   dependencies:
     "@types/react" "*"
 
-"@types/react@*":
-  version "16.9.23"
-  resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.23.tgz#1a66c6d468ba11a8943ad958a8cb3e737568271c"
-  integrity sha512-SsGVT4E7L2wLN3tPYLiF20hmZTPGuzaayVunfgXzUn1x4uHVsKH6QDJQ/TdpHqwsTLd4CwrmQ2vOgxN7gE24gw==
-  dependencies:
-    "@types/prop-types" "*"
-    csstype "^2.2.0"
-
-"@types/react@>=16.9.11":
-  version "17.0.40"
-  resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.40.tgz#dc010cee6254d5239a138083f3799a16638e6bad"
-  integrity sha512-UrXhD/JyLH+W70nNSufXqMZNuUD2cXHu6UjCllC6pmOQgBX4SGXOH8fjRka0O0Ee0HrFxapDD8Bwn81Kmiz6jQ==
+"@types/react@*", "@types/react@>=16.9.11":
+  version "18.0.27"
+  resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.27.tgz#d9425abe187a00f8a5ec182b010d4fd9da703b71"
+  integrity sha512-3vtRKHgVxu3Jp9t718R9BuzoD4NcQ8YJ5XRzsSKxNDiDonD2MXIT1TmSkenxuCycZJoQT5d2vE8LwWJxBC1gmA==
   dependencies:
     "@types/prop-types" "*"
     "@types/scheduler" "*"
@@ -7553,11 +7545,6 @@ cssfilter@0.0.10:
   version "0.0.10"
   resolved "https://registry.yarnpkg.com/cssfilter/-/cssfilter-0.0.10.tgz#c6d2672632a2e5c83e013e6864a42ce8defd20ae"
 
-csstype@^2.2.0:
-  version "2.6.9"
-  resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.9.tgz#05141d0cd557a56b8891394c1911c40c8a98d098"
-  integrity sha512-xz39Sb4+OaTsULgUERcCk+TJj8ylkL4aSVDQiX/ksxbELSqwkgt4d4RD7fovIdgJGSuNYqwZEiVjYY5l0ask+Q==
-
 csstype@^3.0.2:
   version "3.0.11"
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33"
@@ -21803,6 +21790,13 @@ swr@^1.3.0:
   resolved "https://registry.yarnpkg.com/swr/-/swr-1.3.0.tgz#c6531866a35b4db37b38b72c45a63171faf9f4e8"
   integrity sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==
 
+swr@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/swr/-/swr-2.0.2.tgz#fd34f3aac354f6b70f9134eb4218c747cc899a8d"
+  integrity sha512-iHbQW17hsduonMEliZnr6/yaxb+yvLe2r0+AH+ZfeqKzwc2bb+QRYpZm5/b/H0Lxgy7VWow4o71JeSazSun+9A==
+  dependencies:
+    use-sync-external-store "^1.2.0"
+
 synckit@^0.7.2:
   version "0.7.2"
   resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.7.2.tgz#43c07b5a8101ee45355aebf0216895309fd32a6f"
@@ -23432,7 +23426,7 @@ url@0.10.3:
     punycode "1.3.2"
     querystring "0.2.0"
 
-use-sync-external-store@1.2.0:
+use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
   integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==