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

Merge branch 'master' into feat/detect-browser-language

jam411 3 лет назад
Родитель
Сommit
60e2236c75
36 измененных файлов с 457 добавлено и 395 удалено
  1. 1 2
      .github/workflows/draft-release.yml
  2. 1 1
      .github/workflows/pr-to-master.yml
  3. 1 1
      .github/workflows/release-slackbot-proxy.yml
  4. 1 0
      bin/data-migrations/v6/README.md
  5. 57 0
      bin/data-migrations/v6/src/migration.js
  6. 65 0
      bin/data-migrations/v6/src/processor.js
  7. 1 1
      packages/app/public/static/locales/ja_JP/commons.json
  8. 2 2
      packages/app/src/client/services/page-operation.ts
  9. 13 46
      packages/app/src/components/DescendantsPageList.tsx
  10. 2 8
      packages/app/src/components/DescendantsPageListModal.tsx
  11. 2 2
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  12. 11 4
      packages/app/src/components/NotFoundPage.tsx
  13. 1 8
      packages/app/src/components/Page/PageContents.tsx
  14. 4 4
      packages/app/src/components/Page/PageView.tsx
  15. 10 10
      packages/app/src/components/PageEditor.tsx
  16. 11 10
      packages/app/src/components/PageEditorByHackmd.tsx
  17. 3 2
      packages/app/src/components/PageStatusAlert.tsx
  18. 8 8
      packages/app/src/components/PrivateLegacyPages.tsx
  19. 2 4
      packages/app/src/components/PutbackPageModal.jsx
  20. 2 5
      packages/app/src/components/SearchPage/SearchPageBase.tsx
  21. 14 19
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  22. 15 19
      packages/app/src/components/SearchPage/SearchResultList.tsx
  23. 17 21
      packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx
  24. 24 6
      packages/app/src/components/TrashPageList.tsx
  25. 1 0
      packages/app/src/interfaces/services/renderer.ts
  26. 1 0
      packages/app/src/pages/share/[[...path]].page.tsx
  27. 4 4
      packages/app/src/services/renderer/renderer.tsx
  28. 1 1
      packages/app/src/stores/editor.tsx
  29. 1 2
      packages/app/src/stores/middlewares/user.ts
  30. 64 55
      packages/app/src/stores/page-listing.tsx
  31. 68 62
      packages/app/src/stores/page.tsx
  32. 9 12
      packages/app/src/stores/search.tsx
  33. 0 25
      packages/app/src/stores/use-static-swr.tsx
  34. 21 2
      packages/remark-lsx/src/components/Lsx.tsx
  35. 7 1
      packages/remark-lsx/src/services/renderer/lsx.ts
  36. 12 48
      yarn.lock

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

@@ -55,9 +55,8 @@ jobs:
           RELEASE_VERSION=`npx semver -i patch ${{ needs.update-release-draft.outputs.CURRENT_VERSION }}`
           echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_OUTPUT
 
-      # See: https://github.com/bakunyo/git-pr-release-action/issues/15, https://github.com/samunohito/SimpleVolumeMixer/commit/2059044c71236509466cf9b1bb2d56d515274938
       - name: Create/Update Pull Request
-        uses: bakunyo/git-pr-release-action@281e1fe424fac01f3992542266805e4202a22fe0
+        uses: bakunyo/git-pr-release-action@master
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           GIT_PR_RELEASE_BRANCH_PRODUCTION: release/current

+ 1 - 1
.github/workflows/pr-to-master.yml

@@ -36,7 +36,7 @@ jobs:
         !startsWith( github.head_ref, 'dependabot/' ))
 
     steps:
-      - uses: amannn/action-semantic-pull-request@v4.2.0
+      - uses: amannn/action-semantic-pull-request@v5.0.2
         with:
           types: |
             feat

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

@@ -58,7 +58,7 @@ jobs:
       uses: docker/setup-buildx-action@v2
 
     - name: Build and push
-      uses: docker/build-push-action@v2
+      uses: docker/build-push-action@v4
       with:
         context: .
         file: ./packages/slackbot-proxy/docker/Dockerfile

+ 1 - 0
bin/data-migrations/v6/README.md

@@ -0,0 +1 @@
+WIP

+ 57 - 0
bin/data-migrations/v6/src/migration.js

@@ -0,0 +1,57 @@
+
+/* eslint-disable no-undef, no-var, vars-on-top, no-restricted-globals, regex/invalid, import/extensions */
+// ignore lint error because this file is js as mongoshell
+
+var pagesCollection = db.getCollection('pages');
+var revisionsCollection = db.getCollection('revisions');
+
+var getProcessorArray = require('./processor.js');
+
+var migrationType = process.env.MIGRATION_TYPE;
+var processors = getProcessorArray(migrationType);
+
+var operations = [];
+
+var batchSize = process.env.BATCH_SIZE ?? 100; // default 100 revisions in 1 bulkwrite
+var batchSizeInterval = process.env.BATCH_INTERVAL ?? 3000; // default 3 sec
+
+// ===========================================
+// replace method with processors
+// ===========================================
+function replaceLatestRevisions(body, processors) {
+  var replacedBody = body;
+  processors.forEach((processor) => {
+    replacedBody = processor(replacedBody);
+  });
+  return replacedBody;
+}
+
+if (processors.length === 0) {
+  throw Error('No valid processors found. Please enter a valid environment variable');
+}
+
+pagesCollection.find({}).forEach((doc) => {
+  if (doc.revision) {
+    var revision = revisionsCollection.findOne({ _id: doc.revision });
+    var replacedBody = replaceLatestRevisions(revision.body, [...processors]);
+    var operation = {
+      updateOne: {
+        filter: { _id: revision._id },
+        update: {
+          $set: { body: replacedBody },
+        },
+      },
+    };
+    operations.push(operation);
+
+    // bulkWrite per 100 revisions
+    if (operations.length > (batchSize - 1)) {
+      revisionsCollection.bulkWrite(operations);
+      // sleep time can be set from env var
+      sleep(batchSizeInterval);
+      operations = [];
+    }
+  }
+});
+revisionsCollection.bulkWrite(operations);
+print('migration complete!');

+ 65 - 0
bin/data-migrations/v6/src/processor.js

@@ -0,0 +1,65 @@
+
+/* eslint-disable no-undef, no-var, vars-on-top, no-restricted-globals, regex/invalid */
+// ignore lint error because this file is js as mongoshell
+
+// ===========================================
+// processors for old format
+// ===========================================
+function drawioProcessor(body) {
+  var oldDrawioRegExp = /:::\s?drawio\n(.+?)\n:::/g; // drawio old format
+  return body.replace(oldDrawioRegExp, '``` drawio\n$1\n```');
+}
+
+function plantumlProcessor(body) {
+  var oldPlantUmlRegExp = /@startuml\n([\s\S]*?)\n@enduml/g; // plantUML old format
+  return body.replace(oldPlantUmlRegExp, '``` plantuml\n$1\n```');
+}
+
+function tsvProcessor(body) {
+  var oldTsvTableRegExp = /::: tsv(-h)?\n([\s\S]*?)\n:::/g; // TSV old format
+  return body.replace(oldTsvTableRegExp, '``` tsv$1\n$2\n```');
+}
+
+function csvProcessor(body) {
+  var oldCsvTableRegExp = /::: csv(-h)?\n([\s\S]*?)\n:::/g; // CSV old format
+  return body.replace(oldCsvTableRegExp, '``` csv$1\n$2\n```');
+}
+
+function bracketlinkProcessor(body) {
+  // https://regex101.com/r/btZ4hc/1
+  var oldBracketLinkRegExp = /(?<!\[)\[{1}(\/.*?)\]{1}(?!\])/g; // Page Link old format
+  return body.replace(oldBracketLinkRegExp, '[[$1]]');
+}
+
+// ===========================================
+// define processors
+// ===========================================
+
+function getProcessorArray(migrationType) {
+  var oldFormatProcessors;
+  switch (migrationType) {
+    case 'v6-drawio':
+      oldFormatProcessors = [drawioProcessor];
+      break;
+    case 'v6-plantuml':
+      oldFormatProcessors = [plantumlProcessor];
+      break;
+    case 'v6-tsv':
+      oldFormatProcessors = [tsvProcessor];
+      break;
+    case 'v6-csv':
+      oldFormatProcessors = [csvProcessor];
+      break;
+    case 'v6-bracketlink':
+      oldFormatProcessors = [bracketlinkProcessor];
+      break;
+    case 'v6':
+      oldFormatProcessors = [drawioProcessor, plantumlProcessor, tsvProcessor, csvProcessor, bracketlinkProcessor];
+      break;
+    default:
+      oldFormatProcessors = [];
+  }
+  return oldFormatProcessors;
+}
+
+module.exports = getProcessorArray;

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

@@ -104,7 +104,7 @@
     "transfer_data_to_this_growi": "別GROWIのデータをこのGROWIへ移行する",
     "publish_transfer_key": "移行キーを発行する",
     "transfer_key_limit": "※ 移行キーの有効期限は発行から1時間となります。",
-    "once_transfer_key_used": "※ 移行キーは一度移行に利用するとそれ移行はご利用いただけなくなります。",
+    "once_transfer_key_used": "※ 移行キーは一度移行に利用するとそれ以降はご利用いただけなくなります。",
     "transfer_to_growi_cloud": "※ GROWI.cloud への移行を実施する場合はこちらをご確認ください。"
   }
 }

+ 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);

+ 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();

+ 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 ?? '');

+ 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">

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

@@ -1,14 +1,11 @@
 import React, { useEffect } from 'react';
 
-import { pagePathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 
 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 { registerGrowiFacade } from '~/utils/growi-facade';
@@ -23,11 +20,7 @@ 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, mutate: mutateCurrentPage } = useSWRxCurrentPage();
-  const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
+  const { data: currentPage } = useSWRxCurrentPage();
   const updateStateAfterSave = useUpdateStateAfterSave(currentPage?._id);
 
   const { data: rendererOptions, mutate: mutateRendererOptions } = useViewOptions();

+ 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);
 

+ 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) {

+ 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);

+ 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">

+ 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(() => {

+ 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,
       },

+ 1 - 0
packages/app/src/interfaces/services/renderer.ts

@@ -1,6 +1,7 @@
 import { XssOptionConfig } from '~/services/xss/xssOption';
 
 export type RendererConfig = {
+  isSharedPage?: boolean
   isEnabledLinebreaks: boolean,
   isEnabledLinebreaksInComments: boolean,
   adminPreferredIndentSize: number,

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

@@ -221,6 +221,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
   props.drawioUri = configManager.getConfig('crowi', 'app:drawioUri');
 
   props.rendererConfig = {
+    isSharedPage: true,
     isEnabledLinebreaks: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
     isEnabledLinebreaksInComments: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
     adminPreferredIndentSize: configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),

+ 4 - 4
packages/app/src/services/renderer/renderer.tsx

@@ -174,7 +174,7 @@ export const generateViewOptions = (
   // add rehype plugins
   rehypePlugins.push(
     slug,
-    [lsxGrowiPlugin.rehypePlugin, { pagePath }],
+    [lsxGrowiPlugin.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
     rehypeSanitizePlugin,
     katex,
     [toc.rehypePluginStore, { storeTocNode }],
@@ -270,7 +270,7 @@ export const generateSimpleViewOptions = (
 
   // add rehype plugins
   rehypePlugins.push(
-    [lsxGrowiPlugin.rehypePlugin, { pagePath }],
+    [lsxGrowiPlugin.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
     [keywordHighlighter.rehypePlugin, { keywords: highlightKeywords }],
     rehypeSanitizePlugin,
     katex,
@@ -324,7 +324,7 @@ export const generateSSRViewOptions = (
 
   // add rehype plugins
   rehypePlugins.push(
-    [lsxGrowiPlugin.rehypePlugin, { pagePath }],
+    [lsxGrowiPlugin.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
     rehypeSanitizePlugin,
     katex,
   );
@@ -374,7 +374,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
 
   // add rehype plugins
   rehypePlugins.push(
-    [lsxGrowiPlugin.rehypePlugin, { pagePath }],
+    [lsxGrowiPlugin.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
     addLineNumberAttribute.rehypePlugin,
     rehypeSanitizePlugin,
     katex,

+ 1 - 1
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';

+ 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) => {

+ 64 - 55
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,8 +15,6 @@ 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;
@@ -43,48 +43,40 @@ export const useSWRInifinitexRecentlyUpdated = () : SWRInfiniteResponse<(IPageHa
     },
   );
 };
-// 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: [string, number|undefined] | null;
-  // 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]) => 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,
+    },
+  );
 };
 
 
@@ -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;
+
+  // take care of the degration
+  // see: https://github.com/weseek/growi/pull/7038
 
-  // 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);
+  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,
+    },
   );
 };
 

+ 68 - 62
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,59 +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(
-    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]: [string, string, string|undefined, string|undefined]) => {
-        return 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(
-    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();
 
@@ -82,20 +57,34 @@ 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();
@@ -108,27 +97,41 @@ export const useSWRxTagsInfo = (pageId: Nullable<string>): SWRResponse<IPageTags
   );
 };
 
-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 key = useMemo(() => {
+    return pageId != null ? ['/page/info', pageId, fixedShareLinkId] : null;
+  }, [fixedShareLinkId, pageId]);
+
   const swrResult = useSWRImmutable(
-    pageId != null && termNumber != null ? ['/page/info', pageId, fixedShareLinkId, termNumber] : null,
-    ([endpoint, pageId, shareLinkId]) => apiv3Get(endpoint, { pageId, shareLinkId }).then(response => response.data),
+    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;
 };
 
@@ -160,8 +163,11 @@ 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,

+ 9 - 12
packages/app/src/stores/search.tsx

@@ -1,16 +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,
@@ -49,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;
@@ -71,7 +68,7 @@ export const useSWRxSearch = (
   const isKeywordValid = keyword != null && keyword.length > 0;
 
   const swrResult = useSWRImmutable(
-    isKeywordValid ? ['/search', keyword, fixedConfigurations, termNumber] : null,
+    isKeywordValid ? ['/search', keyword, fixedConfigurations] : null,
     ([endpoint, , fixedConfigurations]) => {
       const {
         limit, offset, sort, order,

+ 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);
-    },
-  };
-};

+ 21 - 2
packages/remark-lsx/src/components/Lsx.tsx

@@ -8,7 +8,6 @@ import { LsxContext } from './lsx-context';
 
 import styles from './Lsx.module.scss';
 
-
 type Props = {
   children: React.ReactNode,
   className?: string,
@@ -22,9 +21,10 @@ type Props = {
   except?: string,
 
   isImmutable?: boolean,
+  isSharedPage?: boolean,
 };
 
-export const Lsx = React.memo(({
+const LsxSubstance = React.memo(({
   prefix,
   num, depth, sort, reverse, filter, except,
   isImmutable,
@@ -90,6 +90,25 @@ export const Lsx = React.memo(({
     </div>
   );
 });
+LsxSubstance.displayName = 'LsxSubstance';
+
+const LsxDisabled = React.memo((): JSX.Element => {
+  return (
+    <div className="text-muted">
+      <i className="fa fa-fw fa-info-circle"></i>
+      <small>lsx is not available on the share link page</small>
+    </div>
+  );
+});
+LsxDisabled.displayName = 'LsxDisabled';
+
+export const Lsx = React.memo((props: Props): JSX.Element => {
+  if (props.isSharedPage) {
+    return <LsxDisabled />;
+  }
+
+  return <LsxSubstance {...props} />;
+});
 Lsx.displayName = 'Lsx';
 
 export const LsxImmutable = React.memo((props: Omit<Props, 'isImmutable'>): JSX.Element => {

+ 7 - 1
packages/remark-lsx/src/services/renderer/lsx.ts

@@ -9,7 +9,7 @@ import { Plugin } from 'unified';
 import { visit } from 'unist-util-visit';
 
 const NODE_NAME_PATTERN = new RegExp(/ls|lsx/);
-const SUPPORTED_ATTRIBUTES = ['prefix', 'num', 'depth', 'sort', 'reverse', 'filter', 'except'];
+const SUPPORTED_ATTRIBUTES = ['prefix', 'num', 'depth', 'sort', 'reverse', 'filter', 'except', 'isSharedPage'];
 
 const { addHeadingSlash, hasHeadingSlash } = pathUtils;
 
@@ -65,6 +65,7 @@ export const remarkPlugin: Plugin = function() {
 
 export type LsxRehypePluginParams = {
   pagePath?: string,
+  isSharedPage?: boolean,
 }
 
 const pathResolver = (href: string, basePath: string): string => {
@@ -97,6 +98,11 @@ export const rehypePlugin: Plugin<[LsxRehypePluginParams]> = (options = {}) => {
         return;
       }
 
+      const isSharedPage = lsxElem.properties.isSharedPage;
+      if (isSharedPage == null || typeof isSharedPage !== 'boolean') {
+        lsxElem.properties.isSharedPage = options.isSharedPage;
+      }
+
       const prefix = lsxElem.properties.prefix;
 
       // set basePagePath when prefix is undefined or invalid

+ 12 - 48
yarn.lock

@@ -5045,7 +5045,7 @@ anymatch@^3.0.3:
     normalize-path "^3.0.0"
     picomatch "^2.0.4"
 
-anymatch@~3.1.1, anymatch@~3.1.2:
+anymatch@~3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
   integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
@@ -6554,7 +6554,7 @@ check-node-version@^4.1.0:
     run-parallel "^1.1.4"
     semver "^6.3.0"
 
-"chokidar@>=3.0.0 <4.0.0", chokidar@^3.0.0:
+"chokidar@>=3.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.5.0, chokidar@^3.5.1:
   version "3.5.3"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
   integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
@@ -6585,21 +6585,6 @@ chokidar@^1.6.0:
   optionalDependencies:
     fsevents "^1.0.0"
 
-chokidar@^3.5.0, chokidar@^3.5.1:
-  version "3.5.1"
-  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a"
-  integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==
-  dependencies:
-    anymatch "~3.1.1"
-    braces "~3.0.2"
-    glob-parent "~5.1.0"
-    is-binary-path "~2.1.0"
-    is-glob "~4.0.1"
-    normalize-path "~3.0.0"
-    readdirp "~3.5.0"
-  optionalDependencies:
-    fsevents "~2.3.1"
-
 chownr@^1.1.4:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
@@ -10366,7 +10351,7 @@ fsevents@^1.0.0:
     bindings "^1.5.0"
     nan "^2.12.1"
 
-fsevents@^2.3.2, fsevents@~2.3.1, fsevents@~2.3.2:
+fsevents@^2.3.2, fsevents@~2.3.2:
   version "2.3.2"
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
   integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
@@ -10696,7 +10681,7 @@ glob-parent@^2.0.0:
   dependencies:
     is-glob "^2.0.0"
 
-glob-parent@^5.0.0, glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.0, glob-parent@~5.1.2:
+glob-parent@^5.0.0, glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
   integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
@@ -11463,9 +11448,9 @@ htmlparser2@3.8.x:
     readable-stream "1.1"
 
 http-cache-semantics@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
-  integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
+  integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==
 
 http-errors@1.6.2:
   version "1.6.2"
@@ -18463,13 +18448,6 @@ readdirp@^2.0.0:
     micromatch "^3.1.10"
     readable-stream "^2.0.2"
 
-readdirp@~3.5.0:
-  version "3.5.0"
-  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e"
-  integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==
-  dependencies:
-    picomatch "^2.2.1"
-
 readdirp@~3.6.0:
   version "3.6.0"
   resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
@@ -20347,19 +20325,10 @@ saslprep@^1.0.3:
   dependencies:
     sparse-bitfield "^3.0.3"
 
-sass@^1.53.0:
-  version "1.53.0"
-  resolved "https://registry.yarnpkg.com/sass/-/sass-1.53.0.tgz#eab73a7baac045cc57ddc1d1ff501ad2659952eb"
-  integrity sha512-zb/oMirbKhUgRQ0/GFz8TSAwRq2IlR29vOUJZOx0l8sV+CkHUfHa4u5nqrG+1VceZp7Jfj59SVW9ogdhTvJDcQ==
-  dependencies:
-    chokidar ">=3.0.0 <4.0.0"
-    immutable "^4.0.0"
-    source-map-js ">=0.6.2 <2.0.0"
-
-sass@^1.55.0:
-  version "1.56.1"
-  resolved "https://registry.yarnpkg.com/sass/-/sass-1.56.1.tgz#94d3910cd468fd075fa87f5bb17437a0b617d8a7"
-  integrity sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ==
+sass@^1.53.0, sass@^1.55.0:
+  version "1.57.1"
+  resolved "https://registry.yarnpkg.com/sass/-/sass-1.57.1.tgz#dfafd46eb3ab94817145e8825208ecf7281119b5"
+  integrity sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==
   dependencies:
     chokidar ">=3.0.0 <4.0.0"
     immutable "^4.0.0"
@@ -21043,7 +21012,7 @@ sort-keys@^4.0.0:
   dependencies:
     is-plain-obj "^2.0.0"
 
-"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2:
+"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
   integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
@@ -21053,11 +21022,6 @@ source-map-js@^0.6.2:
   resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e"
   integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==
 
-source-map-js@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.1.tgz#a1741c131e3c77d048252adfa24e23b908670caf"
-  integrity sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==
-
 source-map-resolve@^0.5.0:
   version "0.5.2"
   resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259"