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

Merge branch 'dev/7.0.x' into support/reactstrap-version-up

ryoji-s 2 лет назад
Родитель
Сommit
08fcd9c76f
28 измененных файлов с 326 добавлено и 307 удалено
  1. 34 0
      apps/app/src/components/Sidebar/Custom/CustomSidebar.tsx
  2. 11 0
      apps/app/src/components/Sidebar/Custom/CustomSidebarNotFound.tsx
  3. 7 0
      apps/app/src/components/Sidebar/Custom/CustomSidebarSubstance.module.scss
  4. 37 0
      apps/app/src/components/Sidebar/Custom/CustomSidebarSubstance.tsx
  5. 1 0
      apps/app/src/components/Sidebar/Custom/index.tsx
  6. 0 19
      apps/app/src/components/Sidebar/CustomSidebar.module.scss
  7. 0 81
      apps/app/src/components/Sidebar/CustomSidebar.tsx
  8. 3 3
      apps/app/src/components/Sidebar/PageTree/ItemsTree.tsx
  9. 32 0
      apps/app/src/components/Sidebar/PageTree/PageTree.tsx
  10. 1 1
      apps/app/src/components/Sidebar/PageTree/PageTreeContentSkeleton.tsx
  11. 38 38
      apps/app/src/components/Sidebar/PageTree/PageTreeSubstance.tsx
  12. 1 0
      apps/app/src/components/Sidebar/PageTree/index.ts
  13. 34 0
      apps/app/src/components/Sidebar/RecentChanges/RecentChanges.tsx
  14. 1 1
      apps/app/src/components/Sidebar/RecentChanges/RecentChangesContentSkeleton.tsx
  15. 0 0
      apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.module.scss
  16. 58 58
      apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx
  17. 1 0
      apps/app/src/components/Sidebar/RecentChanges/index.ts
  18. 0 8
      apps/app/src/components/Sidebar/Sidebar.module.scss
  19. 4 11
      apps/app/src/components/Sidebar/Sidebar.tsx
  20. 3 3
      apps/app/src/components/Sidebar/SidebarContents.tsx
  21. 0 18
      apps/app/src/components/Sidebar/Skeleton/CustomSidebarContentSkeleton.tsx
  22. 18 0
      apps/app/src/components/Sidebar/Skeleton/DefaultContentSkeleton.tsx
  23. 14 0
      apps/app/src/components/Sidebar/Skeleton/DefaultContentSkelton.module.scss
  24. 0 6
      apps/app/src/components/Sidebar/Skeleton/SidebarSkeleton.module.scss
  25. 0 49
      apps/app/src/components/Sidebar/Skeleton/SidebarSkeleton.tsx
  26. 15 7
      apps/app/src/stores/page-listing.tsx
  27. 10 2
      apps/app/src/stores/page.tsx
  28. 3 2
      apps/app/src/stores/renderer.tsx

+ 34 - 0
apps/app/src/components/Sidebar/Custom/CustomSidebar.tsx

@@ -0,0 +1,34 @@
+import { Suspense } from 'react';
+
+import Link from 'next/link';
+import { useTranslation } from 'react-i18next';
+
+import { useSWRxPageByPath } from '~/stores/page';
+
+import { SidebarHeaderReloadButton } from '../SidebarHeaderReloadButton';
+import DefaultContentSkeleton from '../Skeleton/DefaultContentSkeleton';
+
+import { CustomSidebarSubstance } from './CustomSidebarSubstance';
+
+
+export const CustomSidebar = (): JSX.Element => {
+  const { t } = useTranslation();
+
+  const { mutate, isLoading } = useSWRxPageByPath('/Sidebar');
+
+  return (
+    <div className="px-3">
+      <div className="grw-sidebar-content-header py-3 d-flex">
+        <h3 className="mb-0">
+          {t('CustomSidebar')}
+          { !isLoading && <Link href="/Sidebar#edit" className="h6 ml-2"><i className="icon-pencil"></i></Link> }
+        </h3>
+        { !isLoading && <SidebarHeaderReloadButton onClick={() => mutate()} /> }
+      </div>
+
+      <Suspense fallback={<DefaultContentSkeleton />}>
+        <CustomSidebarSubstance />
+      </Suspense>
+    </div>
+  );
+};

+ 11 - 0
apps/app/src/components/Sidebar/Custom/CustomSidebarNotFound.tsx

@@ -0,0 +1,11 @@
+import Link from 'next/link';
+
+export const SidebarNotFound = (): JSX.Element => {
+  return (
+    <div className="grw-sidebar-content-header h5 text-center py-3">
+      <Link href="/Sidebar#edit">
+        <i className="icon-magic-wand"></i>Create<strong>/Sidebar</strong>page
+      </Link>
+    </div>
+  );
+};

+ 7 - 0
apps/app/src/components/Sidebar/Custom/CustomSidebarSubstance.module.scss

@@ -0,0 +1,7 @@
+@use '~/styles/organisms/wiki-custom-sidebar.scss';
+
+.grw-custom-sidebar-content :global {
+  .wiki {
+    @extend %grw-custom-sidebar-content;
+  }
+}

+ 37 - 0
apps/app/src/components/Sidebar/Custom/CustomSidebarSubstance.tsx

@@ -0,0 +1,37 @@
+import React from 'react';
+
+import RevisionRenderer from '~/components/Page/RevisionRenderer';
+import { useSWRxPageByPath } from '~/stores/page';
+import { useCustomSidebarOptions } from '~/stores/renderer';
+import loggerFactory from '~/utils/logger';
+
+import { SidebarNotFound } from './CustomSidebarNotFound';
+
+import styles from './CustomSidebarSubstance.module.scss';
+
+
+const logger = loggerFactory('growi:components:CustomSidebarSubstance');
+
+
+export const CustomSidebarSubstance = (): JSX.Element => {
+  const { data: rendererOptions } = useCustomSidebarOptions({ suspense: true });
+  const { data: page } = useSWRxPageByPath('/Sidebar', { suspense: true });
+
+  if (rendererOptions == null) return <></>;
+
+  const markdown = page?.revision.body;
+
+  return (
+    <div className={`py-3 grw-custom-sidebar-content ${styles['grw-custom-sidebar-content']}`}>
+      { markdown === undefined
+        ? <SidebarNotFound />
+        : (
+          <RevisionRenderer
+            rendererOptions={rendererOptions}
+            markdown={markdown}
+          />
+        )
+      }
+    </div>
+  );
+};

+ 1 - 0
apps/app/src/components/Sidebar/Custom/index.tsx

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

+ 0 - 19
apps/app/src/components/Sidebar/CustomSidebar.module.scss

@@ -1,19 +0,0 @@
-@use '~/styles/organisms/wiki-custom-sidebar.scss';
-@use '~/styles/mixins' as *;
-
-.grw-custom-sidebar-content :global {
-  .wiki {
-    @extend %grw-custom-sidebar-content;
-  }
-
-  .grw-custom-sidebar-skeleton-text {
-    @include grw-skeleton-text($font-size:15px, $line-height:21.42px);
-    max-width: 160px;
-    margin: 15px 0;
-  }
-
-  .grw-custom-sidebar-skeleton-text-full {
-    @extend .grw-custom-sidebar-skeleton-text;
-    max-width: 100%;
-  }
-}

+ 0 - 81
apps/app/src/components/Sidebar/CustomSidebar.tsx

@@ -1,81 +0,0 @@
-import React, { FC } from 'react';
-
-import type { IRevision } from '@growi/core';
-import { useTranslation } from 'next-i18next';
-import Link from 'next/link';
-
-import { useSWRxPageByPath } from '~/stores/page';
-import { useCustomSidebarOptions } from '~/stores/renderer';
-import loggerFactory from '~/utils/logger';
-
-import RevisionRenderer from '../Page/RevisionRenderer';
-
-import { SidebarHeaderReloadButton } from './SidebarHeaderReloadButton';
-import CustomSidebarContentSkeleton from './Skeleton/CustomSidebarContentSkeleton';
-
-import styles from './CustomSidebar.module.scss';
-
-
-const logger = loggerFactory('growi:cli:CustomSidebar');
-
-
-const SidebarNotFound = () => {
-  return (
-    <div className="grw-sidebar-content-header h5 text-center py-3">
-      <Link href="/Sidebar#edit">
-        <i className="icon-magic-wand"></i>Create<strong>/Sidebar</strong>page
-      </Link>
-    </div>
-  );
-};
-
-const CustomSidebar: FC = () => {
-  const { t } = useTranslation();
-  const { data: rendererOptions } = useCustomSidebarOptions();
-
-  const { data: page, error, mutate } = useSWRxPageByPath('/Sidebar');
-
-  if (rendererOptions == null) {
-    return <></>;
-  }
-
-  const isLoading = page === undefined && error == null;
-  const markdown = (page?.revision as IRevision | undefined)?.body;
-
-  return (
-    <div className="px-3">
-      <div className="grw-sidebar-content-header py-3 d-flex">
-        <h3 className="mb-0">
-          {t('CustomSidebar')}
-          <Link href="/Sidebar#edit" className="h6 ml-2"><i className="icon-pencil"></i></Link>
-        </h3>
-        <SidebarHeaderReloadButton onClick={() => mutate()} />
-      </div>
-
-      {
-        isLoading && (
-          <CustomSidebarContentSkeleton />
-        )
-      }
-
-      {
-        (!isLoading && markdown != null) && (
-          <div className={`py-3 grw-custom-sidebar-content ${styles['grw-custom-sidebar-content']}`}>
-            <RevisionRenderer
-              rendererOptions={rendererOptions}
-              markdown={markdown}
-            />
-          </div>
-        )
-      }
-
-      {
-        (!isLoading && markdown === undefined) && (
-          <SidebarNotFound />
-        )
-      }
-    </div>
-  );
-};
-
-export default CustomSidebar;

+ 3 - 3
apps/app/src/components/Sidebar/PageTree/ItemsTree.tsx

@@ -25,10 +25,10 @@ import { usePageTreeDescCountMap, useSidebarScrollerRef } from '~/stores/ui';
 import { useGlobalSocket } from '~/stores/websocket';
 import loggerFactory from '~/utils/logger';
 
-import PageTreeContentSkeleton from '../Skeleton/PageTreeContentSkeleton';
 
 import Item from './Item';
 import { ItemNode } from './ItemNode';
+import PageTreeContentSkeleton from './PageTreeContentSkeleton';
 
 import styles from './ItemsTree.module.scss';
 
@@ -106,8 +106,8 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
   const { t } = useTranslation();
   const router = useRouter();
 
-  const { data: ancestorsChildrenResult, error: error1 } = useSWRxPageAncestorsChildren(targetPath);
-  const { data: rootPageResult, error: error2 } = useSWRxRootPage();
+  const { data: ancestorsChildrenResult, error: error1 } = useSWRxPageAncestorsChildren(targetPath, { suspense: true });
+  const { data: rootPageResult, error: error2 } = useSWRxRootPage({ suspense: true });
   const { data: currentPagePath } = useCurrentPagePath();
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openDeleteModal } = usePageDeleteModal();

+ 32 - 0
apps/app/src/components/Sidebar/PageTree/PageTree.tsx

@@ -0,0 +1,32 @@
+import { Suspense } from 'react';
+
+import dynamic from 'next/dynamic';
+import { useTranslation } from 'react-i18next';
+
+import PageTreeContentSkeleton from './PageTreeContentSkeleton';
+import { PageTreeHeader } from './PageTreeSubstance';
+
+const PageTreeContent = dynamic(
+  () => import('./PageTreeSubstance').then(mod => mod.PageTreeContent),
+  { ssr: false, loading: PageTreeContentSkeleton },
+);
+
+
+export const PageTree = (): JSX.Element => {
+  const { t } = useTranslation();
+
+  return (
+    <div className="px-3">
+      <div className="grw-sidebar-content-header py-3 d-flex">
+        <h3 className="mb-0">{t('Page Tree')}</h3>
+        <Suspense>
+          <PageTreeHeader />
+        </Suspense>
+      </div>
+
+      <Suspense fallback={<PageTreeContentSkeleton />}>
+        <PageTreeContent />
+      </Suspense>
+    </div>
+  );
+};

+ 1 - 1
apps/app/src/components/Sidebar/Skeleton/PageTreeContentSkeleton.tsx → apps/app/src/components/Sidebar/PageTree/PageTreeContentSkeleton.tsx

@@ -2,7 +2,7 @@ import React from 'react';
 
 import { Skeleton } from '~/components/Skeleton';
 
-import styles from '../PageTree/ItemsTree.module.scss';
+import styles from './ItemsTree.module.scss';
 
 const PageTreeContentSkeleton = (): JSX.Element => {
 

+ 38 - 38
apps/app/src/components/Sidebar/PageTree.tsx → apps/app/src/components/Sidebar/PageTree/PageTreeSubstance.tsx

@@ -1,58 +1,61 @@
-import React, { FC, memo } from 'react';
+import React, { memo, useCallback } from 'react';
 
 import { useTranslation } from 'next-i18next';
 
 import { useTargetAndAncestors, useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
-import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
+import { mutatePageTree, useSWRxRootPage, useSWRxV5MigrationStatus } from '~/stores/page-listing';
 
-import ItemsTree from './PageTree/ItemsTree';
-import { PrivateLegacyPagesLink } from './PageTree/PrivateLegacyPagesLink';
-import PageTreeContentSkeleton from './Skeleton/PageTreeContentSkeleton';
+import { SidebarHeaderReloadButton } from '../SidebarHeaderReloadButton';
 
-const PageTreeHeader = () => {
+import ItemsTree from './ItemsTree';
+import { PrivateLegacyPagesLink } from './PrivateLegacyPagesLink';
+
+
+export const PageTreeHeader = memo(() => {
+  const { mutate: mutateRootPage } = useSWRxRootPage({ suspense: true });
+  useSWRxV5MigrationStatus({ suspense: true });
+
+  const mutate = useCallback(() => {
+    mutateRootPage();
+    mutatePageTree();
+  }, [mutateRootPage]);
+
+  return (
+    <>
+      <SidebarHeaderReloadButton onClick={() => mutate()}/>
+    </>
+  );
+});
+PageTreeHeader.displayName = 'PageTreeHeader';
+
+
+const PageTreeUnavailable = () => {
   const { t } = useTranslation();
 
+  // TODO : improve design
+  // Story : https://redmine.weseek.co.jp/issues/83755
   return (
-    <div className="grw-sidebar-content-header py-3 d-flex">
-      <h3 className="mb-0">{t('Page Tree')}</h3>
+    <div className="mt-5 mx-2 text-center">
+      <h3 className="text-gray">{t('v5_page_migration.page_tree_not_avaliable')}</h3>
+      <a href="/admin">{t('v5_page_migration.go_to_settings')}</a>
     </div>
   );
 };
 
-const PageTree: FC = memo(() => {
-  const { t } = useTranslation();
-
+export const PageTreeContent = memo(() => {
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: currentPath } = useCurrentPagePath();
   const { data: targetId } = useCurrentPageId();
   const { data: targetAndAncestorsData } = useTargetAndAncestors();
-  const { data: migrationStatus } = useSWRxV5MigrationStatus();
 
-  const targetPathOrId = targetId || currentPath;
+  const { data: migrationStatus } = useSWRxV5MigrationStatus({ suspense: true });
 
-  if (migrationStatus == null) {
-    return (
-      <div className="px-3">
-        <PageTreeHeader />
-        <PageTreeContentSkeleton />
-      </div>
-    );
-  }
+  const targetPathOrId = targetId || currentPath;
 
   if (!migrationStatus?.isV5Compatible) {
-    // TODO : improve design
-    // Story : https://redmine.weseek.co.jp/issues/83755
-    return (
-      <div className="px-3">
-        <PageTreeHeader />
-        <div className="mt-5 mx-2 text-center">
-          <h3 className="text-gray">{t('v5_page_migration.page_tree_not_avaliable')}</h3>
-          <a href="/admin">{t('v5_page_migration.go_to_settings')}</a>
-        </div>
-      </div>
-    );
+    return <PageTreeUnavailable />;
   }
 
   /*
@@ -65,8 +68,7 @@ const PageTree: FC = memo(() => {
   const path = currentPath || '/';
 
   return (
-    <div className="px-3">
-      <PageTreeHeader />
+    <>
       <ItemsTree
         isEnableActions={!isGuestUser}
         isReadOnlyUser={!!isReadOnlyUser}
@@ -82,10 +84,8 @@ const PageTree: FC = memo(() => {
           </div>
         </div>
       )}
-    </div>
+    </>
   );
 });
 
-PageTree.displayName = 'PageTree';
-
-export default PageTree;
+PageTreeContent.displayName = 'PageTreeContent';

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

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

+ 34 - 0
apps/app/src/components/Sidebar/RecentChanges/RecentChanges.tsx

@@ -0,0 +1,34 @@
+import { Suspense, useState } from 'react';
+
+import dynamic from 'next/dynamic';
+import { useTranslation } from 'react-i18next';
+
+import RecentChangesContentSkeleton from './RecentChangesContentSkeleton';
+
+const RecentChangesHeader = dynamic(() => import('./RecentChangesSubstance').then(mod => mod.RecentChangesHeader), { ssr: false });
+const RecentChangesContent = dynamic(
+  () => import('./RecentChangesSubstance').then(mod => mod.RecentChangesContent),
+  { ssr: false, loading: RecentChangesContentSkeleton },
+);
+
+
+export const RecentChanges = (): JSX.Element => {
+  const { t } = useTranslation();
+
+  const [isSmall, setIsSmall] = useState(false);
+
+  return (
+    <div className="px-3" data-testid="grw-recent-changes">
+      <div className="grw-sidebar-content-header py-3 d-flex">
+        <h3 className="mb-0 text-nowrap">{t('Recent Changes')}</h3>
+        <Suspense>
+          <RecentChangesHeader isSmall={isSmall} onSizeChange={setIsSmall} />
+        </Suspense>
+      </div>
+
+      <Suspense fallback={<RecentChangesContentSkeleton />}>
+        <RecentChangesContent isSmall={isSmall} />
+      </Suspense>
+    </div>
+  );
+};

+ 1 - 1
apps/app/src/components/Sidebar/Skeleton/RecentChangesContentSkeleton.tsx → apps/app/src/components/Sidebar/RecentChanges/RecentChangesContentSkeleton.tsx

@@ -2,7 +2,7 @@ import React from 'react';
 
 import { Skeleton } from '~/components/Skeleton';
 
-import styles from '../RecentChanges.module.scss';
+import styles from './RecentChangesSubstance.module.scss';
 
 // TODO: enable skeltonItem https://redmine.weseek.co.jp/issues/128495
 // const SkeletonItem = () => {

+ 0 - 0
apps/app/src/components/Sidebar/RecentChanges.module.scss → apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.module.scss


+ 58 - 58
apps/app/src/components/Sidebar/RecentChanges.tsx → apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx

@@ -1,25 +1,22 @@
 import React, {
-  memo, useCallback, useEffect, useState,
+  memo, useCallback, useEffect,
 } from 'react';
 
 import { isPopulated, type IPageHasId } from '@growi/core';
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { UserPicture, FootstampIcon } from '@growi/ui/dist/components';
-import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
 
+import FormattedDistanceDate from '~/components/FormattedDistanceDate';
+import InfiniteScroll from '~/components/InfiniteScroll';
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
 import LinkedPagePath from '~/models/linked-page-path';
 import { useSWRINFxRecentlyUpdated } from '~/stores/page-listing';
 import loggerFactory from '~/utils/logger';
 
-import FormattedDistanceDate from '../FormattedDistanceDate';
-import InfiniteScroll from '../InfiniteScroll';
+import { SidebarHeaderReloadButton } from '../SidebarHeaderReloadButton';
 
-import { SidebarHeaderReloadButton } from './SidebarHeaderReloadButton';
-import RecentChangesContentSkeleton from './Skeleton/RecentChangesContentSkeleton';
-
-import styles from './RecentChanges.module.scss';
+import styles from './RecentChangesSubstance.module.scss';
 
 
 const logger = loggerFactory('growi:History');
@@ -102,29 +99,27 @@ const PageItem = memo(({ page, isSmall }: PageItemProps): JSX.Element => {
 });
 PageItem.displayName = 'PageItem';
 
-const RecentChanges = (): JSX.Element => {
 
-  const PER_PAGE = 20;
-  const { t } = useTranslation();
-  const swrInifinitexRecentlyUpdated = useSWRINFxRecentlyUpdated(PER_PAGE);
-  const {
-    data, mutate, isLoading,
-  } = swrInifinitexRecentlyUpdated;
+type HeaderProps = {
+  isSmall: boolean,
+  onSizeChange: (isSmall: boolean) => void,
+}
 
-  const [isRecentChangesSidebarSmall, setIsRecentChangesSidebarSmall] = useState(false);
-  const isEmpty = data?.[0]?.pages.length === 0;
-  const isReachingEnd = isEmpty || (data != null && data[data.length - 1]?.pages.length < PER_PAGE);
+const PER_PAGE = 20;
+export const RecentChangesHeader = ({ isSmall, onSizeChange }: HeaderProps): JSX.Element => {
+
+  const { mutate } = useSWRINFxRecentlyUpdated(PER_PAGE, { suspense: true });
 
   const retrieveSizePreferenceFromLocalStorage = useCallback(() => {
     if (window.localStorage.isRecentChangesSidebarSmall === 'true') {
-      setIsRecentChangesSidebarSmall(true);
+      onSizeChange(true);
     }
-  }, []);
+  }, [onSizeChange]);
 
   const changeSizeHandler = useCallback((e) => {
-    setIsRecentChangesSidebarSmall(e.target.checked);
+    onSizeChange(e.target.checked);
     window.localStorage.setItem('isRecentChangesSidebarSmall', e.target.checked);
-  }, []);
+  }, [onSizeChange]);
 
   // componentDidMount
   useEffect(() => {
@@ -132,44 +127,49 @@ const RecentChanges = (): JSX.Element => {
   }, [retrieveSizePreferenceFromLocalStorage]);
 
   return (
-    <div className="px-3" data-testid="grw-recent-changes">
-      <div className="grw-sidebar-content-header py-3 d-flex">
-        <h3 className="mb-0 text-nowrap">{t('Recent Changes')}</h3>
-        <SidebarHeaderReloadButton onClick={() => mutate()}/>
-        <div className="d-flex align-items-center">
-          <div className={`grw-recent-changes-resize-button ${styles['grw-recent-changes-resize-button']} custom-control custom-switch ml-1`}>
-            <input
-              id="recentChangesResize"
-              className="custom-control-input"
-              type="checkbox"
-              checked={isRecentChangesSidebarSmall}
-              onChange={changeSizeHandler}
-            />
-            <label className="custom-control-label" htmlFor="recentChangesResize">
-            </label>
-          </div>
+    <>
+      <SidebarHeaderReloadButton onClick={() => mutate()}/>
+      <div className="d-flex align-items-center">
+        <div className={`grw-recent-changes-resize-button ${styles['grw-recent-changes-resize-button']} custom-control custom-switch ml-1`}>
+          <input
+            id="recentChangesResize"
+            className="custom-control-input"
+            type="checkbox"
+            checked={isSmall}
+            onChange={changeSizeHandler}
+          />
+          <label className="custom-control-label" htmlFor="recentChangesResize" />
         </div>
       </div>
-      {
-        isLoading ? <RecentChangesContentSkeleton /> : (
-          <div className="grw-recent-changes py-3">
-            <ul className="list-group list-group-flush">
-              <InfiniteScroll
-                swrInifiniteResponse={swrInifinitexRecentlyUpdated}
-                isReachingEnd={isReachingEnd}
-              >
-                { data != null && data.map(apiResult => apiResult.pages).flat()
-                  .map(page => (
-                    <PageItem key={page._id} page={page} isSmall={isRecentChangesSidebarSmall} />
-                  ))
-                }
-              </InfiniteScroll>
-            </ul>
-          </div>
-        )
-      }
-    </div>
+    </>
   );
+};
+
+type ContentProps = {
+  isSmall: boolean,
+}
+
+export const RecentChangesContent = ({ isSmall }: ContentProps): JSX.Element => {
+  const swrInifinitexRecentlyUpdated = useSWRINFxRecentlyUpdated(PER_PAGE, { suspense: true });
+  const { data } = swrInifinitexRecentlyUpdated;
+
+  const isEmpty = data?.[0]?.pages.length === 0;
+  const isReachingEnd = isEmpty || (data != null && data[data.length - 1]?.pages.length < PER_PAGE);
 
+  return (
+    <div className="grw-recent-changes py-3">
+      <ul className="list-group list-group-flush">
+        <InfiniteScroll
+          swrInifiniteResponse={swrInifinitexRecentlyUpdated}
+          isReachingEnd={isReachingEnd}
+        >
+          { data != null && data.map(apiResult => apiResult.pages).flat()
+            .map(page => (
+              <PageItem key={page._id} page={page} isSmall={isSmall} />
+            ))
+          }
+        </InfiniteScroll>
+      </ul>
+    </div>
+  );
 };
-export default RecentChanges;

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

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

+ 0 - 8
apps/app/src/components/Sidebar/Sidebar.module.scss

@@ -218,11 +218,3 @@
     }
   }
 }
-
-// '&' could not be set after :global
-// workaround from https://github.com/css-modules/css-modules/issues/295#issuecomment-952885628
-.grw-sidebar-backdrop {
-  &:global(.modal-backdrop) {
-    z-index: bs.$zindex-fixed + 1;
-  }
-}

+ 4 - 11
apps/app/src/components/Sidebar/Sidebar.tsx

@@ -19,14 +19,15 @@ import { StickyStretchableScrollerProps } from '../StickyStretchableScroller';
 
 import { NavigationResizeHexagon } from './NavigationResizeHexagon';
 import { SidebarNav } from './SidebarNav';
-import { SidebarSkeleton } from './Skeleton/SidebarSkeleton';
 
 import styles from './Sidebar.module.scss';
 
+
+const SidebarContents = dynamic(() => import('./SidebarContents').then(mod => mod.SidebarContents), { ssr: false });
+
+
 const StickyStretchableScroller = dynamic<StickyStretchableScrollerProps>(() => import('../StickyStretchableScroller')
   .then(mod => mod.StickyStretchableScroller), { ssr: false });
-const SidebarContents = dynamic(() => import('./SidebarContents')
-  .then(mod => mod.SidebarContents), { ssr: false, loading: () => <SidebarSkeleton /> });
 
 const sidebarMinWidth = 240;
 const sidebarMinimizeWidth = 20;
@@ -129,11 +130,6 @@ export const Sidebar = memo((): JSX.Element => {
     }
   }, [isResizeDisabled, mutateSidebarResizeDisabled]);
 
-  const backdropClickedHandler = useCallback(() => {
-    mutateDrawerOpened(false, false);
-  }, [mutateDrawerOpened]);
-
-
   const setContentWidth = useCallback((newWidth: number) => {
     if (resizableContainer.current == null) {
       return;
@@ -350,9 +346,6 @@ export const Sidebar = memo((): JSX.Element => {
         </div>
       </div>
 
-      { isDrawerOpened && (
-        <div className={`${styles['grw-sidebar-backdrop']} modal-backdrop show`} onClick={backdropClickedHandler}></div>
-      ) }
     </>
   );
 

+ 3 - 3
apps/app/src/components/Sidebar/SidebarContents.tsx

@@ -4,9 +4,9 @@ import { SidebarContentsType } from '~/interfaces/ui';
 import { useCurrentSidebarContents } from '~/stores/ui';
 
 import { Bookmarks } from './Bookmarks';
-import CustomSidebar from './CustomSidebar';
-import PageTree from './PageTree';
-import RecentChanges from './RecentChanges';
+import { CustomSidebar } from './Custom';
+import { PageTree } from './PageTree';
+import { RecentChanges } from './RecentChanges';
 import Tag from './Tag';
 
 export const SidebarContents = memo(() => {

+ 0 - 18
apps/app/src/components/Sidebar/Skeleton/CustomSidebarContentSkeleton.tsx

@@ -1,18 +0,0 @@
-import React from 'react';
-
-import { Skeleton } from '~/components/Skeleton';
-
-import styles from '../CustomSidebar.module.scss';
-
-const CustomSidebarContentSkeleton = (): JSX.Element => {
-
-  return (
-    <div className={`py-3 grw-custom-sidebar-content ${styles['grw-custom-sidebar-content']}`}>
-      <Skeleton additionalClass={`grw-custom-sidebar-skeleton-text-full ${styles['grw-custom-sidebar-skeleton-text-full']}`} />
-      <Skeleton additionalClass={`grw-custom-sidebar-skeleton-text-full ${styles['grw-custom-sidebar-skeleton-text-full']}`} />
-      <Skeleton additionalClass={`grw-custom-sidebar-skeleton-text ${styles['grw-custom-sidebar-skeleton-text']}`} />
-    </div>
-  );
-};
-
-export default CustomSidebarContentSkeleton;

+ 18 - 0
apps/app/src/components/Sidebar/Skeleton/DefaultContentSkeleton.tsx

@@ -0,0 +1,18 @@
+import React from 'react';
+
+import { Skeleton } from '~/components/Skeleton';
+
+import styles from './DefaultContentSkelton.module.scss';
+
+const DefaultContentSkeleton = (): JSX.Element => {
+
+  return (
+    <div className={`py-3 grw-default-content-skelton ${styles['grw-default-content-skelton']}`}>
+      <Skeleton additionalClass={`grw-skeleton-text-full ${styles['grw-skeleton-text-full']}`} />
+      <Skeleton additionalClass={`grw-skeleton-text-full ${styles['grw-skeleton-text-full']}`} />
+      <Skeleton additionalClass={`grw-skeleton-text ${styles['grw-skeleton-text']}`} />
+    </div>
+  );
+};
+
+export default DefaultContentSkeleton;

+ 14 - 0
apps/app/src/components/Sidebar/Skeleton/DefaultContentSkelton.module.scss

@@ -0,0 +1,14 @@
+@use '~/styles/mixins';
+
+.grw-default-content-skelton :global {
+  .grw-skeleton-text {
+    @include mixins.grw-skeleton-text($font-size:15px, $line-height:21.42px);
+    max-width: 160px;
+    margin: 15px 0;
+  }
+
+  .grw-skeleton-text-full {
+    @extend .grw-skeleton-text;
+    max-width: 100%;
+  }
+}

+ 0 - 6
apps/app/src/components/Sidebar/Skeleton/SidebarSkeleton.module.scss

@@ -1,6 +0,0 @@
-@use '~/styles/mixins' as *;
-
-.grw-sidebar-content-header-skeleton {
-  @include grw-skeleton-h3;
-  max-width: 100%;
-}

+ 0 - 49
apps/app/src/components/Sidebar/Skeleton/SidebarSkeleton.tsx

@@ -1,49 +0,0 @@
-import React, { memo } from 'react';
-
-import { useTranslation } from 'next-i18next';
-
-import { SidebarContentsType } from '~/interfaces/ui';
-import { useCurrentSidebarContents } from '~/stores/ui';
-
-import CustomSidebarContentSkeleton from './CustomSidebarContentSkeleton';
-import PageTreeContentSkeleton from './PageTreeContentSkeleton';
-import RecentChangesContentSkeleton from './RecentChangesContentSkeleton';
-import TagContentSkeleton from './TagContentSkeleton';
-
-export const SidebarSkeleton = memo(() => {
-  const { t } = useTranslation();
-  const { data: currentSidebarContents } = useCurrentSidebarContents();
-
-  let Contents: () => JSX.Element;
-  let title: string;
-  switch (currentSidebarContents) {
-
-    case SidebarContentsType.RECENT:
-      Contents = RecentChangesContentSkeleton;
-      title = t('Recent Changes');
-      break;
-    case SidebarContentsType.CUSTOM:
-      Contents = CustomSidebarContentSkeleton;
-      title = t('CustomSidebar');
-      break;
-    case SidebarContentsType.TAG:
-      Contents = TagContentSkeleton;
-      title = t('Tags');
-      break;
-    case SidebarContentsType.TREE:
-    default:
-      Contents = PageTreeContentSkeleton;
-      title = t('Page Tree');
-      break;
-  }
-
-  return (
-    <div className={currentSidebarContents === SidebarContentsType.TAG ? 'px-4' : 'px-3'}>
-      <div className="grw-sidebar-content-header py-3">
-        <h3 className="mb-0">{title}</h3>
-      </div>
-      <Contents />
-    </div>
-  );
-});
-SidebarSkeleton.displayName = 'SidebarSkeleton';

+ 15 - 7
apps/app/src/stores/page-listing.tsx

@@ -4,7 +4,9 @@ import type {
   Nullable, HasObjectId,
   IDataWithMeta, IPageHasId, IPageInfoForListing, IPageInfoForOperation,
 } from '@growi/core';
-import useSWR, { Arguments, mutate, SWRResponse } from 'swr';
+import useSWR, {
+  mutate, type SWRConfiguration, type SWRResponse, type Arguments,
+} from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRInfinite, { SWRInfiniteResponse } from 'swr/infinite';
 
@@ -30,7 +32,7 @@ type RecentApiResult = {
   totalCount: number,
   offset: number,
 }
-export const useSWRINFxRecentlyUpdated = (limit: number) : SWRInfiniteResponse<RecentApiResult, Error> => {
+export const useSWRINFxRecentlyUpdated = (limit: number, config?: SWRConfiguration) : SWRInfiniteResponse<RecentApiResult, Error> => {
   return useSWRInfinite(
     (pageIndex, previousPageData) => {
       if (previousPageData != null && previousPageData.pages.length === 0) return null;
@@ -44,6 +46,7 @@ export const useSWRINFxRecentlyUpdated = (limit: number) : SWRInfiniteResponse<R
     },
     ([endpoint, offset, limit]) => apiv3Get<RecentApiResult>(endpoint, { offset, limit }).then(response => response.data),
     {
+      ...config,
       revalidateFirstPage: false,
       revalidateAll: false,
     },
@@ -131,8 +134,8 @@ export const useSWRxPageInfoForList = (
   };
 };
 
-export const useSWRxRootPage = (): SWRResponse<RootPageResult, Error> => {
-  return useSWRImmutable(
+export const useSWRxRootPage = (config?: SWRConfiguration): SWRResponse<RootPageResult, Error> => {
+  return useSWR(
     '/page-listing/root',
     endpoint => apiv3Get(endpoint).then((response) => {
       return {
@@ -140,7 +143,10 @@ export const useSWRxRootPage = (): SWRResponse<RootPageResult, Error> => {
       };
     }),
     {
+      ...config,
       keepPreviousData: true,
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
     },
   );
 };
@@ -155,6 +161,7 @@ export const mutatePageTree = async(): Promise<undefined[]> => {
 
 export const useSWRxPageAncestorsChildren = (
     path: string | null,
+    config?: SWRConfiguration,
 ): SWRResponse<AncestorsChildrenResult, Error> => {
   const key = path ? [MUTATION_ID_FOR_PAGETREE, '/page-listing/ancestors-children', path] : null;
 
@@ -173,6 +180,7 @@ export const useSWRxPageAncestorsChildren = (
       };
     }),
     {
+      ...config,
       keepPreviousData: true,
     },
   );
@@ -200,9 +208,8 @@ export const useSWRxPageChildren = (
   );
 };
 
-export const useSWRxV5MigrationStatus = (
-): SWRResponse<V5MigrationStatus, Error> => {
-  return useSWR(
+export const useSWRxV5MigrationStatus = (config?: SWRConfiguration): SWRResponse<V5MigrationStatus, Error> => {
+  return useSWRImmutable(
     '/pages/v5-migration-status',
     endpoint => apiv3Get(endpoint).then((response) => {
       return {
@@ -210,5 +217,6 @@ export const useSWRxV5MigrationStatus = (
         migratablePagesCount: response.data.migratablePagesCount,
       };
     }),
+    config,
   );
 };

+ 10 - 2
apps/app/src/stores/page.tsx

@@ -8,7 +8,9 @@ import type {
   IRevision, IRevisionHasId,
 } from '@growi/core';
 import { isClient, pagePathUtils } from '@growi/core/dist/utils';
-import useSWR, { mutate, useSWRConfig, type SWRResponse } from 'swr';
+import useSWR, {
+  mutate, useSWRConfig, type SWRResponse, type SWRConfiguration,
+} from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRInfinite, { type SWRInfiniteResponse } from 'swr/infinite';
 import useSWRMutation, { type SWRMutationResponse } from 'swr/mutation';
@@ -105,10 +107,16 @@ export const useSWRMUTxCurrentPage = (): SWRMutationResponse<IPagePopulatedToSho
   );
 };
 
-export const useSWRxPageByPath = (path?: string): SWRResponse<IPagePopulatedToShowRevision, Error> => {
+export const useSWRxPageByPath = (path?: string, config?: SWRConfiguration): SWRResponse<IPagePopulatedToShowRevision, Error> => {
   return useSWR(
     path != null ? ['/page', path] : null,
     ([endpoint, path]) => apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { path }).then(result => result.data.page),
+    {
+      ...config,
+      keepPreviousData: true,
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
+    },
   );
 };
 

+ 3 - 2
apps/app/src/stores/renderer.tsx

@@ -1,7 +1,7 @@
 import { useCallback } from 'react';
 
 import type { HtmlElementNode } from 'rehype-toc';
-import useSWR, { type SWRResponse } from 'swr';
+import useSWR, { type SWRConfiguration, type SWRResponse } from 'swr';
 
 import { getGrowiFacade } from '~/features/growi-plugin/client/utils/growi-facade-utils';
 import type { RendererOptions } from '~/interfaces/renderer-options';
@@ -147,7 +147,7 @@ export const useSearchResultOptions = useSelectedPagePreviewOptions;
 
 export const useTimelineOptions = useSelectedPagePreviewOptions;
 
-export const useCustomSidebarOptions = (): SWRResponse<RendererOptions, Error> => {
+export const useCustomSidebarOptions = (config?: SWRConfiguration): SWRResponse<RendererOptions, Error> => {
   const { data: rendererConfig } = useRendererConfig();
 
   const isAllDataValid = rendererConfig != null;
@@ -161,6 +161,7 @@ export const useCustomSidebarOptions = (): SWRResponse<RendererOptions, Error> =
       return generateSimpleViewOptions(rendererConfig, '/');
     },
     {
+      ...config,
       keepPreviousData: true,
       revalidateOnFocus: false,
       revalidateOnReconnect: false,