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

Merge pull request #8904 from weseek/imprv/ssr-performance-pageview

imprv: SSR performance for PageView
Yuki Takei 1 год назад
Родитель
Сommit
654c3727d3
100 измененных файлов с 587 добавлено и 402 удалено
  1. 1 0
      apps/app/package.json
  2. 2 1
      apps/app/src/client/services/create-page/use-create-page.tsx
  3. 1 1
      apps/app/src/client/services/side-effects/hash-changed.ts
  4. 1 1
      apps/app/src/client/services/side-effects/page-updated.ts
  5. 1 1
      apps/app/src/components-universal/Admin/Common/AdminNavigation.tsx
  6. 0 0
      apps/app/src/components-universal/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.module.scss
  7. 1 1
      apps/app/src/components-universal/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.tsx
  8. 0 0
      apps/app/src/components-universal/Common/PagePathHierarchicalLink/index.ts
  9. 0 21
      apps/app/src/components-universal/Common/PagePathNav/PagePathNav.module.scss
  10. 69 0
      apps/app/src/components-universal/Common/PagePathNav/PagePathNav.tsx
  11. 62 0
      apps/app/src/components-universal/Common/PagePathNav/PagePathNavLayout.tsx
  12. 4 0
      apps/app/src/components-universal/Common/PagePathNav/Separator.module.scss
  13. 5 0
      apps/app/src/components-universal/Common/PagePathNav/Separator.tsx
  14. 3 0
      apps/app/src/components-universal/Common/PagePathNav/index.ts
  15. 5 0
      apps/app/src/components-universal/Common/PagePathNavTitle/PagePathNavTitle.module.scss
  16. 43 0
      apps/app/src/components-universal/Common/PagePathNavTitle/PagePathNavTitle.tsx
  17. 1 0
      apps/app/src/components-universal/Common/PagePathNavTitle/index.ts
  18. 1 1
      apps/app/src/components-universal/Layout/NoLoginLayout.tsx
  19. 4 3
      apps/app/src/components-universal/PageView/PageAlerts/FixPageGrantAlert.tsx
  20. 0 0
      apps/app/src/components-universal/PageView/PageAlerts/OldRevisionAlert.tsx
  21. 1 2
      apps/app/src/components-universal/PageView/PageAlerts/PageAlerts.tsx
  22. 0 0
      apps/app/src/components-universal/PageView/PageAlerts/PageGrantAlert.tsx
  23. 2 2
      apps/app/src/components-universal/PageView/PageAlerts/PageRedirectedAlert.tsx
  24. 2 2
      apps/app/src/components-universal/PageView/PageAlerts/PageStaleAlert.tsx
  25. 4 3
      apps/app/src/components-universal/PageView/PageAlerts/TrashPageAlert.tsx
  26. 7 4
      apps/app/src/components-universal/PageView/PageAlerts/WipPageAlert.tsx
  27. 1 0
      apps/app/src/components-universal/PageView/PageAlerts/index.ts
  28. 0 0
      apps/app/src/components-universal/PageView/PageContentFooter.module.scss
  29. 1 3
      apps/app/src/components-universal/PageView/PageContentFooter.tsx
  30. 0 0
      apps/app/src/components-universal/PageView/PageView.module.scss
  31. 20 24
      apps/app/src/components-universal/PageView/PageView.tsx
  32. 0 0
      apps/app/src/components-universal/PageView/PageViewLayout.module.scss
  33. 3 1
      apps/app/src/components-universal/PageView/PageViewLayout.tsx
  34. 0 0
      apps/app/src/components-universal/PageView/RevisionRenderer.tsx
  35. 1 0
      apps/app/src/components-universal/PageView/index.ts
  36. 0 0
      apps/app/src/components-universal/ShareLinkPageView/ShareLinkAlert.tsx
  37. 12 15
      apps/app/src/components-universal/ShareLinkPageView/ShareLinkPageView.tsx
  38. 1 0
      apps/app/src/components-universal/ShareLinkPageView/index.ts
  39. 0 0
      apps/app/src/components-universal/User/UserDate.jsx
  40. 0 0
      apps/app/src/components-universal/User/UserInfo.module.scss
  41. 0 0
      apps/app/src/components-universal/User/UserInfo.tsx
  42. 0 0
      apps/app/src/components-universal/User/Username.tsx
  43. 1 1
      apps/app/src/components/Comments.tsx
  44. 0 174
      apps/app/src/components/Common/PagePathNav/PagePathNav.tsx
  45. 0 1
      apps/app/src/components/Common/PagePathNav/index.ts
  46. 1 1
      apps/app/src/components/Hotkeys/Subscribers/EditPage.jsx
  47. 3 3
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  48. 2 1
      apps/app/src/components/Navbar/PageEditorModeManager.tsx
  49. 8 24
      apps/app/src/components/Page/DisplaySwitcher.tsx
  50. 11 0
      apps/app/src/components/Page/EditablePageEffects.tsx
  51. 1 1
      apps/app/src/components/Page/RevisionLoader.tsx
  52. 1 2
      apps/app/src/components/PageAttachment/DeleteAttachmentModal.tsx
  53. 2 2
      apps/app/src/components/PageComment/Comment.tsx
  54. 1 1
      apps/app/src/components/PageComment/CommentPreview.tsx
  55. 1 1
      apps/app/src/components/PageComment/DeleteCommentModal.tsx
  56. 4 1
      apps/app/src/components/PageControls/PageControls.tsx
  57. 3 5
      apps/app/src/components/PageEditor/PageEditor.tsx
  58. 1 1
      apps/app/src/components/PageEditor/PageEditorReadOnly.tsx
  59. 1 1
      apps/app/src/components/PageEditor/Preview.tsx
  60. 1 1
      apps/app/src/components/PageEditor/conflict.tsx
  61. 1 1
      apps/app/src/components/PageHeader/PagePathHeader.tsx
  62. 2 1
      apps/app/src/components/PageHeader/PageTitleHeader.tsx
  63. 2 2
      apps/app/src/components/PageHistory/Revision.tsx
  64. 1 1
      apps/app/src/components/PageHistory/RevisionDiff.tsx
  65. 1 1
      apps/app/src/components/PageList/PageListItemL.tsx
  66. 0 0
      apps/app/src/components/PagePathNavSticky/CollapsedParentsDropdown.module.scss
  67. 0 0
      apps/app/src/components/PagePathNavSticky/CollapsedParentsDropdown.tsx
  68. 15 0
      apps/app/src/components/PagePathNavSticky/PagePathNavSticky.module.scss
  69. 114 0
      apps/app/src/components/PagePathNavSticky/PagePathNavSticky.tsx
  70. 1 0
      apps/app/src/components/PagePathNavSticky/index.ts
  71. 2 3
      apps/app/src/components/PageSideContents/PageSideContents.tsx
  72. 2 2
      apps/app/src/components/PageStatusAlert.tsx
  73. 2 4
      apps/app/src/components/SavePageControls.tsx
  74. 7 5
      apps/app/src/components/SearchPage/SearchResultContent.tsx
  75. 1 1
      apps/app/src/components/Sidebar/Custom/CustomSidebarSubstance.tsx
  76. 1 1
      apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx
  77. 3 13
      apps/app/src/components/Sidebar/ResizableArea/ResizableArea.tsx
  78. 25 0
      apps/app/src/components/Sidebar/ResizableArea/ResizableAreaFallback.tsx
  79. 10 0
      apps/app/src/components/Sidebar/ResizableArea/props.d.ts
  80. 56 27
      apps/app/src/components/Sidebar/Sidebar.tsx
  81. 1 1
      apps/app/src/components/UsersHomepageFooter.tsx
  82. 2 0
      apps/app/src/pages/.eslintrc.js
  83. 20 15
      apps/app/src/pages/[[...path]].page.tsx
  84. 1 1
      apps/app/src/pages/_app.page.tsx
  85. 1 1
      apps/app/src/pages/_private-legacy-pages.page.tsx
  86. 1 1
      apps/app/src/pages/_search.page.tsx
  87. 1 1
      apps/app/src/pages/admin/[...path].page.tsx
  88. 1 1
      apps/app/src/pages/admin/app.page.tsx
  89. 1 1
      apps/app/src/pages/admin/audit-log.page.tsx
  90. 1 1
      apps/app/src/pages/admin/customize.page.tsx
  91. 1 1
      apps/app/src/pages/admin/data-transfer.page.tsx
  92. 1 1
      apps/app/src/pages/admin/export.page.tsx
  93. 1 1
      apps/app/src/pages/admin/global-notification/[globalNotificationId].page.tsx
  94. 1 1
      apps/app/src/pages/admin/global-notification/new.page.tsx
  95. 1 1
      apps/app/src/pages/admin/importer.page.tsx
  96. 1 1
      apps/app/src/pages/admin/index.page.tsx
  97. 1 1
      apps/app/src/pages/admin/markdown.page.tsx
  98. 1 1
      apps/app/src/pages/admin/notification.page.tsx
  99. 1 1
      apps/app/src/pages/admin/plugins.page.tsx
  100. 1 1
      apps/app/src/pages/admin/search.page.tsx

+ 1 - 0
apps/app/package.json

@@ -143,6 +143,7 @@
     "multer-autoreap": "^1.0.3",
     "mustache": "^4.2.0",
     "next": "^14.1.3",
+    "next-dynamic-loading-props": "^0.1.1",
     "next-i18next": "^15.2.0",
     "next-superjson": "^0.0.4",
     "next-themes": "^0.2.1",

+ 2 - 1
apps/app/src/client/services/create-page/use-create-page.tsx

@@ -6,9 +6,10 @@ import { useTranslation } from 'react-i18next';
 import { exist, getIsNonUserRelatedGroupsGranted } from '~/client/services/page-operation';
 import { toastWarning } from '~/client/util/toastr';
 import type { IApiv3PageCreateParams } from '~/interfaces/apiv3';
+import { EditorMode, useEditorMode } from '~/stores-universal/ui';
 import { useGrantedGroupsInheritanceSelectModal } from '~/stores/modal';
 import { useCurrentPagePath } from '~/stores/page';
-import { EditorMode, useEditorMode, useIsUntitledPage } from '~/stores/ui';
+import { useIsUntitledPage } from '~/stores/ui';
 
 import { createPage } from './create-page';
 

+ 1 - 1
apps/app/src/client/services/side-effects/hash-changed.ts

@@ -2,8 +2,8 @@ import { useCallback, useEffect } from 'react';
 
 import { useRouter } from 'next/router';
 
+import { useEditorMode, determineEditorModeByHash } from '~/stores-universal/ui';
 import { useIsEditable } from '~/stores/context';
-import { useEditorMode, determineEditorModeByHash } from '~/stores/ui';
 
 /**
  * Change editorMode by browser forward/back operation

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

@@ -3,10 +3,10 @@ import { useCallback, useEffect } from 'react';
 import { useGlobalSocket } from '@growi/core/dist/swr';
 
 import { SocketEventName } from '~/interfaces/websocket';
+import { useEditorMode, EditorMode } from '~/stores-universal/ui';
 import { usePageStatusAlert } from '~/stores/alert';
 import { useSWRxCurrentPage, useSWRMUTxCurrentPage } from '~/stores/page';
 import { useSetRemoteLatestPageData, type RemoteRevisionData } from '~/stores/remote-latest-page';
-import { useEditorMode, EditorMode } from '~/stores/ui';
 
 
 export const usePageUpdatedEffect = (): void => {

+ 1 - 1
apps/app/src/components-universal/Admin/Common/AdminNavigation.tsx

@@ -5,7 +5,7 @@ import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
 import urljoin from 'url-join';
 
-import { useGrowiCloudUri, useGrowiAppIdForGrowiCloud } from '~/stores/context';
+import { useGrowiCloudUri, useGrowiAppIdForGrowiCloud } from '~/stores-universal/context';
 
 import styles from './AdminNavigation.module.scss';
 

+ 0 - 0
apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.module.scss → apps/app/src/components-universal/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.module.scss


+ 1 - 1
apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.tsx → apps/app/src/components-universal/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.tsx

@@ -3,7 +3,7 @@ import React, { memo, useCallback } from 'react';
 import Link from 'next/link';
 import urljoin from 'url-join';
 
-import type LinkedPagePath from '../../../models/linked-page-path';
+import type LinkedPagePath from '~/models/linked-page-path';
 
 import styles from './PagePathHierarchicalLink.module.scss';
 

+ 0 - 0
apps/app/src/components/Common/PagePathHierarchicalLink/index.ts → apps/app/src/components-universal/Common/PagePathHierarchicalLink/index.ts


+ 0 - 21
apps/app/src/components/Common/PagePathNav/PagePathNav.module.scss → apps/app/src/components-universal/Common/PagePathNav/PagePathNav.module.scss

@@ -1,27 +1,6 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 @use '@growi/ui/scss/atoms/btn-muted';
 
-.grw-mx-02em {
-  margin-right: 0.2em;
-  margin-left: 0.2em;
-}
-
-.grw-page-path-nav-sticky :global {
-  min-height: 75px;
-
-  .sticky-inner-wrapper {
-    z-index: bs.$zindex-sticky;
-  }
-
-  // TODO:Responsive font size
-  // set smaller font-size when sticky
-  .sticky-inner-wrapper.active {
-    h1 {
-      font-size: 1.75rem !important;
-    }
-  }
-}
-
 .grw-page-path-nav :global {
   .btn-copy {
     @include btn-muted.colorize(bs.$orange);

+ 69 - 0
apps/app/src/components-universal/Common/PagePathNav/PagePathNav.tsx

@@ -0,0 +1,69 @@
+import { useMemo } from 'react';
+
+import { DevidedPagePath } from '@growi/core/dist/models';
+import { pagePathUtils } from '@growi/core/dist/utils';
+
+import LinkedPagePath from '~/models/linked-page-path';
+
+import { PagePathHierarchicalLink } from '../PagePathHierarchicalLink';
+
+import type { PagePathNavLayoutProps } from './PagePathNavLayout';
+import { PagePathNavLayout } from './PagePathNavLayout';
+
+import styles from './PagePathNav.module.scss';
+
+
+const { isTrashPage } = pagePathUtils;
+
+
+const Separator = ({ className }: {className?: string}): JSX.Element => {
+  return <span className={`separator ${className ?? ''} ${styles['grw-mx-02em']}`}>/</span>;
+};
+
+export const PagePathNav = (props: PagePathNavLayoutProps): JSX.Element => {
+  const { pagePath } = props;
+
+  const isInTrash = isTrashPage(pagePath);
+
+  const formerLink = useMemo(() => {
+    const dPagePath = new DevidedPagePath(pagePath, false, true);
+
+    // one line
+    if (dPagePath.isRoot || dPagePath.isFormerRoot) {
+      return undefined;
+    }
+
+    // two line
+    const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
+    return (
+      <>
+        <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} isInTrash={isInTrash} />
+        <Separator />
+      </>
+    );
+  }, [isInTrash, pagePath]);
+
+  const latterLink = useMemo(() => {
+    const dPagePath = new DevidedPagePath(pagePath, false, true);
+
+    // one line
+    if (dPagePath.isRoot || dPagePath.isFormerRoot) {
+      const linkedPagePath = new LinkedPagePath(pagePath);
+      return <PagePathHierarchicalLink linkedPagePath={linkedPagePath} isInTrash={isInTrash} />;
+    }
+
+    // two line
+    const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
+    return (
+      <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.former} isInTrash={isInTrash} />
+    );
+  }, [isInTrash, pagePath]);
+
+  return (
+    <PagePathNavLayout
+      {...props}
+      formerLink={formerLink}
+      latterLink={latterLink}
+    />
+  );
+};

+ 62 - 0
apps/app/src/components-universal/Common/PagePathNav/PagePathNavLayout.tsx

@@ -0,0 +1,62 @@
+import type { ReactNode } from 'react';
+
+import dynamic from 'next/dynamic';
+
+import { useIsNotFound } from '~/stores/page';
+
+import styles from './PagePathNav.module.scss';
+
+
+export type PagePathNavLayoutProps = {
+  className?: string,
+  pagePath: string,
+  pageId?: string | null,
+  isWipPage?: boolean,
+  maxWidth?: number,
+  formerLinkClassName?: string,
+  latterLinkClassName?: string,
+}
+
+type Props = PagePathNavLayoutProps & {
+  formerLink?: ReactNode,
+  latterLink?: ReactNode,
+}
+
+const CopyDropdown = dynamic(() => import('~/components/Common/CopyDropdown').then(mod => mod.CopyDropdown), { ssr: false });
+
+export const PagePathNavLayout = (props: Props): JSX.Element => {
+  const {
+    className = '',
+    pageId, pagePath, isWipPage,
+    formerLink,
+    formerLinkClassName = '',
+    latterLink,
+    latterLinkClassName = '',
+    maxWidth,
+  } = props;
+
+  const { data: isNotFound } = useIsNotFound();
+
+  const copyDropdownId = `copydropdown-${pageId}`;
+
+  return (
+    <div className={className} style={{ maxWidth }}>
+      <span className={`${formerLinkClassName ?? ''} ${styles['grw-former-link']}`}>{formerLink}</span>
+      <div className="d-flex align-items-center">
+        <h1 className={`m-0 ${latterLinkClassName}`}>
+          {latterLink}
+        </h1>
+        { pageId != null && !isNotFound && (
+          <div className="d-flex align-items-center ms-2">
+            { isWipPage && (
+              <span className="badge text-bg-secondary ms-1 me-1">WIP</span>
+            )}
+            <CopyDropdown pageId={pageId} pagePath={pagePath} dropdownToggleId={copyDropdownId} dropdownToggleClassName="p-2">
+              <span className="material-symbols-outlined">content_paste</span>
+            </CopyDropdown>
+          </div>
+        ) }
+      </div>
+    </div>
+  );
+};

+ 4 - 0
apps/app/src/components-universal/Common/PagePathNav/Separator.module.scss

@@ -0,0 +1,4 @@
+.grw-mx-02em {
+  margin-right: 0.2em;
+  margin-left: 0.2em;
+}

+ 5 - 0
apps/app/src/components-universal/Common/PagePathNav/Separator.tsx

@@ -0,0 +1,5 @@
+import styles from './Separator.module.scss';
+
+export const Separator = ({ className }: {className?: string}): JSX.Element => (
+  <span className={`separator ${className ?? ''} ${styles['grw-mx-02em']}`}>/</span>
+);

+ 3 - 0
apps/app/src/components-universal/Common/PagePathNav/index.ts

@@ -0,0 +1,3 @@
+export * from './PagePathNav';
+export * from './PagePathNavLayout';
+export * from './Separator';

+ 5 - 0
apps/app/src/components-universal/Common/PagePathNavTitle/PagePathNavTitle.module.scss

@@ -0,0 +1,5 @@
+@use '@growi/core-styles/scss/bootstrap/init' as bs;
+
+.grw-page-path-nav-title :global {
+  min-height: 75px;
+}

+ 43 - 0
apps/app/src/components-universal/Common/PagePathNavTitle/PagePathNavTitle.tsx

@@ -0,0 +1,43 @@
+import { useState } from 'react';
+
+import withLoadingProps from 'next-dynamic-loading-props';
+import dynamic from 'next/dynamic';
+import { useIsomorphicLayoutEffect } from 'usehooks-ts';
+
+import { PagePathNav } from '../PagePathNav';
+import type { PagePathNavLayoutProps } from '../PagePathNav';
+
+import styles from './PagePathNavTitle.module.scss';
+
+const moduleClass = styles['grw-page-path-nav-title'] ?? '';
+
+
+const PagePathNavSticky = withLoadingProps<PagePathNavLayoutProps>(useLoadingProps => dynamic(
+  () => import('~/components/PagePathNavSticky').then(mod => mod.PagePathNavSticky),
+  {
+    ssr: false,
+    loading: () => {
+      // eslint-disable-next-line react-hooks/rules-of-hooks
+      const props = useLoadingProps();
+      return <PagePathNav {...props} />;
+    },
+  },
+));
+
+/**
+ * Switch PagePathNav and PagePathNavSticky
+ * @returns
+ */
+export const PagePathNavTitle = (props: PagePathNavLayoutProps): JSX.Element => {
+
+  const [isClient, setClient] = useState(false);
+
+  useIsomorphicLayoutEffect(() => {
+    setClient(true);
+  }, []);
+
+  return isClient
+    ? <PagePathNavSticky {...props} className={moduleClass} latterLinkClassName="fs-2" />
+    : <PagePathNav {...props} className={moduleClass} latterLinkClassName="fs-2" />;
+
+};

+ 1 - 0
apps/app/src/components-universal/Common/PagePathNavTitle/index.ts

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

+ 1 - 1
apps/app/src/components-universal/Layout/NoLoginLayout.tsx

@@ -1,7 +1,7 @@
 import type { ReactNode } from 'react';
 import React from 'react';
 
-import { useAppTitle } from '~/stores/context';
+import { useAppTitle } from '~/stores-universal/context';
 
 import GrowiLogo from '../Common/GrowiLogo';
 

+ 4 - 3
apps/app/src/components/PageAlert/FixPageGrantAlert.tsx → apps/app/src/components-universal/PageView/PageAlerts/FixPageGrantAlert.tsx

@@ -6,11 +6,9 @@ import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 
-import { apiv3Put } from '~/client/util/apiv3-client';
-import { toastError, toastSuccess } from '~/client/util/toastr';
 import { UserGroupPageGrantStatus, type IPageGrantData } from '~/interfaces/page';
 import type { PopulatedGrantedGroup, IRecordApplicableGrant, IResGrantData } from '~/interfaces/page-grant';
-import { useCurrentUser } from '~/stores/context';
+import { useCurrentUser } from '~/stores-universal/context';
 import { useSWRxApplicableGrant, useSWRxCurrentGrantData, useSWRxCurrentPage } from '~/stores/page';
 
 type ModalProps = {
@@ -66,6 +64,7 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
     close();
 
     try {
+      const apiv3Put = (await import('~/client/util/apiv3-client')).apiv3Put;
       await apiv3Put(`/page/${pageId}/grant`, {
         grant: selectedGrant,
         userRelatedGrantedGroups: selectedGroups.length !== 0 ? selectedGroups.map((g) => {
@@ -73,9 +72,11 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
         }) : null,
       });
 
+      const toastSuccess = (await import('~/client/util/toastr')).toastSuccess;
       toastSuccess(t('Successfully updated'));
     }
     catch (err) {
+      const toastError = (await import('~/client/util/toastr')).toastError;
       toastError(t('Failed to update'));
     }
   };

+ 0 - 0
apps/app/src/components/PageAlert/OldRevisionAlert.tsx → apps/app/src/components-universal/PageView/PageAlerts/OldRevisionAlert.tsx


+ 1 - 2
apps/app/src/components/PageAlert/PageAlerts.tsx → apps/app/src/components-universal/PageView/PageAlerts/PageAlerts.tsx

@@ -6,12 +6,11 @@ import { useIsNotFound } from '~/stores/page';
 
 import { OldRevisionAlert } from './OldRevisionAlert';
 import { PageGrantAlert } from './PageGrantAlert';
-import { PageRedirectedAlert } from './PageRedirectedAlert';
 import { PageStaleAlert } from './PageStaleAlert';
 import { WipPageAlert } from './WipPageAlert';
 
+const PageRedirectedAlert = dynamic(() => import('./PageRedirectedAlert').then(mod => mod.PageRedirectedAlert), { ssr: false });
 const FixPageGrantAlert = dynamic(() => import('./FixPageGrantAlert').then(mod => mod.FixPageGrantAlert), { ssr: false });
-// dynamic import because TrashPageAlert uses localStorageMiddleware
 const TrashPageAlert = dynamic(() => import('./TrashPageAlert').then(mod => mod.TrashPageAlert), { ssr: false });
 
 export const PageAlerts = (): JSX.Element => {

+ 0 - 0
apps/app/src/components/PageAlert/PageGrantAlert.tsx → apps/app/src/components-universal/PageView/PageAlerts/PageGrantAlert.tsx


+ 2 - 2
apps/app/src/components/PageAlert/PageRedirectedAlert.tsx → apps/app/src/components-universal/PageView/PageAlerts/PageRedirectedAlert.tsx

@@ -2,8 +2,6 @@ import React, { useState, useCallback } from 'react';
 
 import { useTranslation } from 'next-i18next';
 
-import { unlink } from '~/client/services/page-operation';
-import { toastError } from '~/client/util/toastr';
 import { useCurrentPagePath } from '~/stores/page';
 import { useRedirectFrom } from '~/stores/page-redirect';
 
@@ -19,10 +17,12 @@ export const PageRedirectedAlert = React.memo((): JSX.Element => {
       return;
     }
     try {
+      const unlink = (await import('~/client/services/page-operation')).unlink;
       await unlink(currentPagePath);
       setIsUnlinked(true);
     }
     catch (err) {
+      const toastError = (await import('~/client/util/toastr')).toastError;
       toastError(err);
     }
   }, [currentPagePath]);

+ 2 - 2
apps/app/src/components/PageAlert/PageStaleAlert.tsx → apps/app/src/components-universal/PageView/PageAlerts/PageStaleAlert.tsx

@@ -2,8 +2,8 @@ import { isIPageInfoForEntity } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 
 
-import { useIsEnabledStaleNotification } from '../../stores/context';
-import { useSWRxCurrentPage, useSWRxPageInfo } from '../../stores/page';
+import { useIsEnabledStaleNotification } from '~/stores-universal/context';
+import { useSWRxCurrentPage, useSWRxPageInfo } from '~/stores/page';
 
 export const PageStaleAlert = ():JSX.Element => {
   const { t } = useTranslation();

+ 4 - 3
apps/app/src/components/PageAlert/TrashPageAlert.tsx → apps/app/src/components-universal/PageView/PageAlerts/TrashPageAlert.tsx

@@ -5,8 +5,6 @@ import { format } from 'date-fns/format';
 import { useRouter } from 'next/router';
 import { useTranslation } from 'react-i18next';
 
-import { unlink } from '~/client/services/page-operation';
-import { toastError } from '~/client/util/toastr';
 import { usePageDeleteModal, usePutBackPageModal } from '~/stores/modal';
 import {
   useCurrentPagePath, useSWRxPageInfo, useSWRxCurrentPage, useIsTrashPage, useSWRMUTxCurrentPage,
@@ -49,16 +47,19 @@ export const TrashPageAlert = (): JSX.Element => {
     if (isEmptyPage) {
       return;
     }
-    const putBackedHandler = () => {
+    const putBackedHandler = async() => {
       if (currentPagePath == null) {
         return;
       }
       try {
+        const unlink = (await import('~/client/services/page-operation')).unlink;
         unlink(currentPagePath);
+
         router.push(`/${pageId}`);
         mutateCurrentPage();
       }
       catch (err) {
+        const toastError = (await import('~/client/util/toastr')).toastError;
         toastError(err);
       }
     };

+ 7 - 4
apps/app/src/components/PageAlert/WipPageAlert.tsx → apps/app/src/components-universal/PageView/PageAlerts/WipPageAlert.tsx

@@ -2,11 +2,7 @@ import React, { useCallback } from 'react';
 
 import { useTranslation } from 'react-i18next';
 
-import { toastSuccess, toastError } from '~/client/util/toastr';
 import { useSWRMUTxCurrentPage, useSWRxCurrentPage } from '~/stores/page';
-import { mutatePageTree } from '~/stores/page-listing';
-
-import { publish } from '../../client/services/page-operation';
 
 
 export const WipPageAlert = (): JSX.Element => {
@@ -22,12 +18,19 @@ export const WipPageAlert = (): JSX.Element => {
     }
 
     try {
+      const publish = (await import('~/client/services/page-operation')).publish;
       await publish(pageId);
+
       await mutateCurrentPage();
+
+      const mutatePageTree = (await import('~/stores/page-listing')).mutatePageTree;
       await mutatePageTree();
+
+      const toastSuccess = (await import('~/client/util/toastr')).toastSuccess;
       toastSuccess(t('wip_page.success_publish_page'));
     }
     catch {
+      const toastError = (await import('~/client/util/toastr')).toastError;
       toastError(t('wip_page.fail_publish_page'));
     }
   }, [currentPage?._id, mutateCurrentPage, t]);

+ 1 - 0
apps/app/src/components-universal/PageView/PageAlerts/index.ts

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

+ 0 - 0
apps/app/src/components/PageContentFooter.module.scss → apps/app/src/components-universal/PageView/PageContentFooter.module.scss


+ 1 - 3
apps/app/src/components/PageContentFooter.tsx → apps/app/src/components-universal/PageView/PageContentFooter.tsx

@@ -1,11 +1,9 @@
-import React from 'react';
-
 import type { IPage, IPagePopulatedToShowRevision } from '@growi/core';
 import dynamic from 'next/dynamic';
 
 import styles from './PageContentFooter.module.scss';
 
-const AuthorInfo = dynamic(() => import('./AuthorInfo').then(mod => mod.AuthorInfo), { ssr: false });
+const AuthorInfo = dynamic(() => import('~/components/AuthorInfo').then(mod => mod.AuthorInfo), { ssr: false });
 
 export type PageContentFooterProps = {
   page: IPage | IPagePopulatedToShowRevision,

+ 0 - 0
apps/app/src/components/Page/PageView.module.scss → apps/app/src/components-universal/PageView/PageView.module.scss


+ 20 - 24
apps/app/src/components/Page/PageView.tsx → apps/app/src/components-universal/PageView/PageView.tsx

@@ -7,47 +7,45 @@ import { isUsersHomepage } from '@growi/core/dist/utils/page-path-utils';
 import { useSlidesByFrontmatter } from '@growi/presentation/dist/services';
 import dynamic from 'next/dynamic';
 
-import { useShouldExpandContent } from '~/client/services/layout';
+import { PagePathNavTitle } from '~/components-universal/Common/PagePathNavTitle';
 import type { RendererConfig } from '~/interfaces/services/renderer';
+import { useShouldExpandContent } from '~/services/layout/use-should-expand-content';
 import { generateSSRViewOptions } from '~/services/renderer/renderer';
 import {
   useIsForbidden, useIsIdenticalPath, useIsNotCreatable,
-} from '~/stores/context';
+} from '~/stores-universal/context';
 import { useSWRxCurrentPage, useIsNotFound } from '~/stores/page';
 import { useViewOptions } from '~/stores/renderer';
 import { useIsMobile } from '~/stores/ui';
 
-
-import type { CommentsProps } from '../Comments';
-import { PagePathNavSticky } from '../Common/PagePathNav';
-import { PageViewLayout } from '../Common/PageViewLayout';
-import { PageAlerts } from '../PageAlert/PageAlerts';
-import { PageContentFooter } from '../PageContentFooter';
-import type { PageSideContentsProps } from '../PageSideContents';
 import { UserInfo } from '../User/UserInfo';
-import type { UsersHomepageFooterProps } from '../UsersHomepageFooter';
 
+import { PageAlerts } from './PageAlerts/PageAlerts';
+import { PageContentFooter } from './PageContentFooter';
+import { PageViewLayout } from './PageViewLayout';
 import RevisionRenderer from './RevisionRenderer';
 
+
 import styles from './PageView.module.scss';
 
 
-const NotCreatablePage = dynamic(() => import('../NotCreatablePage').then(mod => mod.NotCreatablePage), { ssr: false });
-const ForbiddenPage = dynamic(() => import('../ForbiddenPage'), { ssr: false });
-const NotFoundPage = dynamic(() => import('../NotFoundPage'), { ssr: false });
-const PageSideContents = dynamic<PageSideContentsProps>(() => import('../PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
-const PageContentsUtilities = dynamic(() => import('./PageContentsUtilities').then(mod => mod.PageContentsUtilities), { ssr: false });
-const Comments = dynamic<CommentsProps>(() => import('../Comments').then(mod => mod.Comments), { ssr: false });
-const UsersHomepageFooter = dynamic<UsersHomepageFooterProps>(() => import('../UsersHomepageFooter')
+const NotCreatablePage = dynamic(() => import('../../components/NotCreatablePage').then(mod => mod.NotCreatablePage), { ssr: false });
+const ForbiddenPage = dynamic(() => import('../../components/ForbiddenPage'), { ssr: false });
+const NotFoundPage = dynamic(() => import('../../components/NotFoundPage'), { ssr: false });
+const PageSideContents = dynamic(() => import('../../components/PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
+const PageContentsUtilities = dynamic(() => import('../../components/Page/PageContentsUtilities').then(mod => mod.PageContentsUtilities), { ssr: false });
+const Comments = dynamic(() => import('../../components/Comments').then(mod => mod.Comments), { ssr: false });
+const UsersHomepageFooter = dynamic(() => import('../../components/UsersHomepageFooter')
   .then(mod => mod.UsersHomepageFooter), { ssr: false });
-const IdenticalPathPage = dynamic(() => import('../IdenticalPathPage').then(mod => mod.IdenticalPathPage), { ssr: false });
-const SlideRenderer = dynamic(() => import('./SlideRenderer').then(mod => mod.SlideRenderer), { ssr: false });
+const IdenticalPathPage = dynamic(() => import('../../components/IdenticalPathPage').then(mod => mod.IdenticalPathPage), { ssr: false });
+const SlideRenderer = dynamic(() => import('../../components/Page/SlideRenderer').then(mod => mod.SlideRenderer), { ssr: false });
 
 
 type Props = {
   pagePath: string,
   rendererConfig: RendererConfig,
   initialPage?: IPagePopulatedToShowRevision,
+  className?: string,
 }
 
 export const PageView = (props: Props): JSX.Element => {
@@ -57,7 +55,7 @@ export const PageView = (props: Props): JSX.Element => {
   const [isCommentsLoaded, setCommentsLoaded] = useState(false);
 
   const {
-    pagePath, initialPage, rendererConfig,
+    pagePath, initialPage, rendererConfig, className,
   } = props;
 
   const { data: isIdenticalPathPage } = useIsIdenticalPath();
@@ -96,7 +94,6 @@ export const PageView = (props: Props): JSX.Element => {
   }, [isCommentsLoaded]);
   // *******************************  end  *******************************
 
-
   const specialContents = useMemo(() => {
     if (isIdenticalPathPage) {
       return <IdenticalPathPage />;
@@ -109,9 +106,7 @@ export const PageView = (props: Props): JSX.Element => {
     }
   }, [isForbidden, isIdenticalPathPage, isNotCreatable]);
 
-  const headerContents = (
-    <PagePathNavSticky pageId={page?._id} pagePath={pagePath} isWipPage={page?.wip} />
-  );
+  const headerContents = <PagePathNavTitle pageId={page?._id} pagePath={pagePath} />;
 
   const sideContents = !isNotFound && !isNotCreatable
     ? (
@@ -168,6 +163,7 @@ export const PageView = (props: Props): JSX.Element => {
 
   return (
     <PageViewLayout
+      className={className}
       headerContents={headerContents}
       sideContents={sideContents}
       footerContents={footerContents}

+ 0 - 0
apps/app/src/components/Common/PageViewLayout.module.scss → apps/app/src/components-universal/PageView/PageViewLayout.module.scss


+ 3 - 1
apps/app/src/components/Common/PageViewLayout.tsx → apps/app/src/components-universal/PageView/PageViewLayout.tsx

@@ -6,6 +6,7 @@ const pageViewLayoutClass = styles['page-view-layout'] ?? '';
 const _fluidLayoutClass = styles['fluid-layout'] ?? '';
 
 type Props = {
+  className?: string,
   children?: ReactNode,
   headerContents?: ReactNode,
   sideContents?: ReactNode,
@@ -15,6 +16,7 @@ type Props = {
 
 export const PageViewLayout = (props: Props): JSX.Element => {
   const {
+    className,
     children, headerContents, sideContents, footerContents,
     expandContentWidth,
   } = props;
@@ -23,7 +25,7 @@ export const PageViewLayout = (props: Props): JSX.Element => {
 
   return (
     <>
-      <div className={`main ${pageViewLayoutClass} ${fluidLayoutClass} flex-expand-vert ps-sidebar`}>
+      <div className={`main ${className} ${pageViewLayoutClass} ${fluidLayoutClass} flex-expand-vert ps-sidebar`}>
         <div className="container-lg wide-gutter-x-lg grw-container-convertible flex-expand-vert">
           { headerContents != null && headerContents }
           { sideContents != null

+ 0 - 0
apps/app/src/components/Page/RevisionRenderer.tsx → apps/app/src/components-universal/PageView/RevisionRenderer.tsx


+ 1 - 0
apps/app/src/components-universal/PageView/index.ts

@@ -0,0 +1 @@
+// Do not re-export in this directory for performance reasons

+ 0 - 0
apps/app/src/components/Page/ShareLinkAlert.tsx → apps/app/src/components-universal/ShareLinkPageView/ShareLinkAlert.tsx


+ 12 - 15
apps/app/src/components/ShareLinkPageView.tsx → apps/app/src/components-universal/ShareLinkPageView/ShareLinkPageView.tsx

@@ -1,32 +1,31 @@
-import React, { useMemo } from 'react';
+import { useMemo } from 'react';
 
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import { useSlidesByFrontmatter } from '@growi/presentation/dist/services';
 import dynamic from 'next/dynamic';
 
-import { useShouldExpandContent } from '~/client/services/layout';
+import { PagePathNavTitle } from '~/components-universal/Common/PagePathNavTitle';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { IShareLinkHasId } from '~/interfaces/share-link';
+import { useShouldExpandContent } from '~/services/layout/use-should-expand-content';
 import { generateSSRViewOptions } from '~/services/renderer/renderer';
-import { useIsEnabledMarp } from '~/stores/context';
 import { useIsNotFound } from '~/stores/page';
 import { useViewOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
 
-import { PagePathNavSticky } from './Common/PagePathNav';
-import { PageViewLayout } from './Common/PageViewLayout';
-import RevisionRenderer from './Page/RevisionRenderer';
-import ShareLinkAlert from './Page/ShareLinkAlert';
-import { PageContentFooter } from './PageContentFooter';
-import type { PageSideContentsProps } from './PageSideContents';
+import { PageContentFooter } from '../PageView/PageContentFooter';
+import { PageViewLayout } from '../PageView/PageViewLayout';
+import RevisionRenderer from '../PageView/RevisionRenderer';
+
+import ShareLinkAlert from './ShareLinkAlert';
 
 
 const logger = loggerFactory('growi:Page');
 
 
-const PageSideContents = dynamic<PageSideContentsProps>(() => import('./PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
-const ForbiddenPage = dynamic(() => import('./ForbiddenPage'), { ssr: false });
-const SlideRenderer = dynamic(() => import('./Page/SlideRenderer').then(mod => mod.SlideRenderer), { ssr: false });
+const PageSideContents = dynamic(() => import('../../components/PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
+const ForbiddenPage = dynamic(() => import('../../components/ForbiddenPage'), { ssr: false });
+const SlideRenderer = dynamic(() => import('../../components/Page/SlideRenderer').then(mod => mod.SlideRenderer), { ssr: false });
 
 type Props = {
   pagePath: string,
@@ -62,9 +61,7 @@ export const ShareLinkPageView = (props: Props): JSX.Element => {
     }
   }, [disableLinkSharing, props.disableLinkSharing]);
 
-  const headerContents = (
-    <PagePathNavSticky pageId={page?._id} pagePath={pagePath} />
-  );
+  const headerContents = <PagePathNavTitle pageId={page?._id} pagePath={pagePath} />;
 
   const sideContents = !isNotFound
     ? (

+ 1 - 0
apps/app/src/components-universal/ShareLinkPageView/index.ts

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

+ 0 - 0
apps/app/src/components/User/UserDate.jsx → apps/app/src/components-universal/User/UserDate.jsx


+ 0 - 0
apps/app/src/components/User/UserInfo.module.scss → apps/app/src/components-universal/User/UserInfo.module.scss


+ 0 - 0
apps/app/src/components/User/UserInfo.tsx → apps/app/src/components-universal/User/UserInfo.tsx


+ 0 - 0
apps/app/src/components/User/Username.tsx → apps/app/src/components-universal/User/Username.tsx


+ 1 - 1
apps/app/src/components/Comments.tsx

@@ -17,7 +17,7 @@ const { isTopPage } = pagePathUtils;
 const PageComment = dynamic(() => import('~/components/PageComment').then(mod => mod.PageComment), { ssr: false });
 const CommentEditorPre = dynamic(() => import('./PageComment/CommentEditor').then(mod => mod.CommentEditorPre), { ssr: false });
 
-export type CommentsProps = {
+type CommentsProps = {
   pageId: string,
   pagePath: string,
   revision: IRevisionHasId,

+ 0 - 174
apps/app/src/components/Common/PagePathNav/PagePathNav.tsx

@@ -1,174 +0,0 @@
-import React, {
-  useEffect,
-  useRef,
-  useState,
-} from 'react';
-
-import { DevidedPagePath } from '@growi/core/dist/models';
-import { pagePathUtils } from '@growi/core/dist/utils';
-import dynamic from 'next/dynamic';
-import Sticky from 'react-stickynode';
-
-import { useIsNotFound } from '~/stores/page';
-import {
-  usePageControlsX, useCurrentProductNavWidth, useSidebarMode,
-} from '~/stores/ui';
-
-import LinkedPagePath from '../../../models/linked-page-path';
-import { PagePathHierarchicalLink } from '../PagePathHierarchicalLink';
-import { CollapsedParentsDropdown } from '../PagePathHierarchicalLink/CollapsedParentsDropdown';
-
-import styles from './PagePathNav.module.scss';
-
-
-const { isTrashPage } = pagePathUtils;
-
-type Props = {
-  pagePath: string,
-  pageId?: string | null,
-  isWipPage?: boolean,
-  isSingleLineMode?: boolean,
-  isCollapseParents?: boolean,
-  formerLinkClassName?: string,
-  latterLinkClassName?: string,
-  maxWidth?: number,
-}
-
-const CopyDropdown = dynamic(() => import('../CopyDropdown').then(mod => mod.CopyDropdown), { ssr: false });
-
-const Separator = ({ className }: {className?: string}): JSX.Element => {
-  return <span className={`separator ${className ?? ''} ${styles['grw-mx-02em']}`}>/</span>;
-};
-
-export const PagePathNav = (props: Props): JSX.Element => {
-  const {
-    pageId, pagePath, isWipPage, isSingleLineMode, isCollapseParents,
-    formerLinkClassName, latterLinkClassName, maxWidth,
-  } = props;
-  const dPagePath = new DevidedPagePath(pagePath, false, true);
-
-  const { data: isNotFound } = useIsNotFound();
-
-  const isInTrash = isTrashPage(pagePath);
-
-  let formerLink;
-  let latterLink;
-
-  // one line
-  if (dPagePath.isRoot || dPagePath.isFormerRoot || (!isCollapseParents && isSingleLineMode)) {
-    const linkedPagePath = new LinkedPagePath(pagePath);
-    latterLink = <PagePathHierarchicalLink linkedPagePath={linkedPagePath} isInTrash={isInTrash} />;
-  }
-  // collapse parents
-  else if (isCollapseParents) {
-    const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
-    const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
-    latterLink = (
-      <>
-        <CollapsedParentsDropdown linkedPagePath={linkedPagePathFormer} />
-        <Separator />
-        <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.former} isInTrash={isInTrash} />
-      </>
-    );
-  }
-  // two line
-  else {
-    const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
-    const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
-    formerLink = (
-      <>
-        <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} isInTrash={isInTrash} />
-        <Separator />
-      </>
-    );
-    latterLink = (
-      <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.former} isInTrash={isInTrash} />
-    );
-  }
-
-  const copyDropdownId = `copydropdown-${pageId}`;
-
-  return (
-    <div style={{ maxWidth }}>
-      <span className={`${formerLinkClassName ?? ''} ${styles['grw-former-link']}`}>{formerLink}</span>
-      <div className="d-flex align-items-center">
-        <h1 className={`m-0 ${latterLinkClassName}`}>
-          {latterLink}
-        </h1>
-        { pageId != null && !isNotFound && (
-          <div className="d-flex align-items-center ms-2">
-            { isWipPage && (
-              <span className="badge text-bg-secondary ms-1 me-1">WIP</span>
-            )}
-            <CopyDropdown pageId={pageId} pagePath={pagePath} dropdownToggleId={copyDropdownId} dropdownToggleClassName="p-2">
-              <span className="material-symbols-outlined">content_paste</span>
-            </CopyDropdown>
-          </div>
-        ) }
-      </div>
-    </div>
-  );
-};
-
-PagePathNav.displayName = 'PagePathNav';
-
-
-type PagePathNavStickyProps = Omit<Props, 'isCollapseParents'>;
-
-export const PagePathNavSticky = (props: PagePathNavStickyProps): JSX.Element => {
-
-  const { data: pageControlsX } = usePageControlsX();
-  const { data: sidebarWidth } = useCurrentProductNavWidth();
-  const { data: sidebarMode } = useSidebarMode();
-  const pagePathNavRef = useRef<HTMLDivElement>(null);
-
-  const [navMaxWidth, setNavMaxWidth] = useState<number | undefined>();
-
-  useEffect(() => {
-    if (pageControlsX == null || pagePathNavRef.current == null || sidebarWidth == null) {
-      return;
-    }
-    setNavMaxWidth(pageControlsX - pagePathNavRef.current.getBoundingClientRect().x - 10);
-  }, [pageControlsX, pagePathNavRef, sidebarWidth]);
-
-  useEffect(() => {
-    // wait for the end of the animation of the opening and closing of the sidebar
-    const timeout = setTimeout(() => {
-      if (pageControlsX == null || pagePathNavRef.current == null || sidebarMode == null) {
-        return;
-      }
-      setNavMaxWidth(pageControlsX - pagePathNavRef.current.getBoundingClientRect().x - 10);
-    }, 200);
-    return () => {
-      clearTimeout(timeout);
-    };
-  }, [pageControlsX, pagePathNavRef, sidebarMode]);
-
-  return (
-    // Controlling pointer-events
-    //  1. disable pointer-events with 'pe-none'
-    <div ref={pagePathNavRef}>
-      <Sticky className={`${styles['grw-page-path-nav-sticky']} mb-4`} innerClass="mt-1 pe-none" innerActiveClass="active">
-        {({ status }) => {
-          const isCollapseParents = status === Sticky.STATUS_FIXED;
-          return (
-          // Controlling pointer-events
-          //  2. enable pointer-events with 'pe-auto' only against the children
-          //      which width is minimized by 'd-inline-block'
-          //
-            <div className="d-inline-block pe-auto">
-              <PagePathNav
-                {...props}
-                isCollapseParents={isCollapseParents}
-                latterLinkClassName={isCollapseParents ? 'fs-3  text-truncate' : 'fs-2'}
-                maxWidth={isCollapseParents ? navMaxWidth : undefined}
-              />
-            </div>
-          );
-        }}
-      </Sticky>
-    </div>
-  );
-};
-
-PagePathNavSticky.displayName = 'PagePathNavSticky';

+ 0 - 1
apps/app/src/components/Common/PagePathNav/index.ts

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

+ 1 - 1
apps/app/src/components/Hotkeys/Subscribers/EditPage.jsx

@@ -2,8 +2,8 @@ import { useEffect } from 'react';
 
 import PropTypes from 'prop-types';
 
+import { EditorMode, useEditorMode } from '~/stores-universal/ui';
 import { useIsEditable } from '~/stores/context';
-import { EditorMode, useEditorMode } from '~/stores/ui';
 
 const EditPage = (props) => {
   const { data: isEditable } = useIsEditable();

+ 3 - 3
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -14,10 +14,11 @@ import { useRouter } from 'next/router';
 import Sticky from 'react-stickynode';
 import { DropdownItem } from 'reactstrap';
 
-import { useShouldExpandContent } from '~/client/services/layout';
 import { exportAsMarkdown, updateContentWidth } from '~/client/services/page-operation';
 import { GroundGlassBar } from '~/components-universal/Navbar/GroundGlassBar';
 import type { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
+import { useShouldExpandContent } from '~/services/layout/use-should-expand-content';
+import { useEditorMode } from '~/stores-universal/ui';
 import {
   useCurrentPathname,
   useCurrentUser, useIsGuestUser, useIsReadOnlyUser, useIsSharedUser, useShareLinkId,
@@ -31,12 +32,11 @@ import {
 } from '~/stores/page';
 import { mutatePageTree } from '~/stores/page-listing';
 import {
-  useEditorMode, useIsAbleToShowPageManagement,
+  useIsAbleToShowPageManagement,
   useIsAbleToChangeEditorMode,
   useIsDeviceLargerThanMd,
 } from '~/stores/ui';
 
-
 import { NotAvailable } from '../NotAvailable';
 import { Skeleton } from '../Skeleton';
 

+ 2 - 1
apps/app/src/components/Navbar/PageEditorModeManager.tsx

@@ -6,8 +6,9 @@ import { useTranslation } from 'next-i18next';
 
 import { useCreatePage } from '~/client/services/create-page';
 import { toastError } from '~/client/util/toastr';
+import { EditorMode, useEditorMode } from '~/stores-universal/ui';
 import { useIsNotFound } from '~/stores/page';
-import { EditorMode, useEditorMode, useIsDeviceLargerThanMd } from '~/stores/ui';
+import { useIsDeviceLargerThanMd } from '~/stores/ui';
 import { useCurrentPageYjsData } from '~/stores/yjs';
 
 import { shouldCreateWipPage } from '../../utils/should-create-wip-page';

+ 8 - 24
apps/app/src/components/Page/DisplaySwitcher.tsx

@@ -1,46 +1,30 @@
-import React from 'react';
-
 import dynamic from 'next/dynamic';
 
 import { useHashChangedEffect } from '~/client/services/side-effects/hash-changed';
-import { usePageUpdatedEffect } from '~/client/services/side-effects/page-updated';
-import { useCurrentPageYjsDataEffect } from '~/client/services/side-effects/yjs';
+import { EditorMode, useEditorMode } from '~/stores-universal/ui';
 import { useIsEditable } from '~/stores/context';
 import { useIsLatestRevision } from '~/stores/page';
-import { EditorMode, useEditorMode } from '~/stores/ui';
 
 import { LazyRenderer } from '../Common/LazyRenderer';
 
 const PageEditor = dynamic(() => import('../PageEditor'), { ssr: false });
 const PageEditorReadOnly = dynamic(() => import('../PageEditor/PageEditorReadOnly').then(mod => mod.PageEditorReadOnly), { ssr: false });
 
-type Props = {
-  pageView: JSX.Element,
-}
 
-export const DisplaySwitcher = (props: Props): JSX.Element => {
-  const { pageView } = props;
+export const DisplaySwitcher = (): JSX.Element => {
 
   const { data: editorMode = EditorMode.View } = useEditorMode();
   const { data: isEditable } = useIsEditable();
   const { data: isLatestRevision } = useIsLatestRevision();
 
-  usePageUpdatedEffect();
   useHashChangedEffect();
-  useCurrentPageYjsDataEffect();
 
   return (
-    <>
-      <div className="d-edit-none">
-        {pageView}
-      </div>
-
-      <LazyRenderer shouldRender={isEditable === true && editorMode === EditorMode.Editor}>
-        { isLatestRevision
-          ? <PageEditor />
-          : <PageEditorReadOnly />
-        }
-      </LazyRenderer>
-    </>
+    <LazyRenderer shouldRender={isEditable === true && editorMode === EditorMode.Editor}>
+      { isLatestRevision
+        ? <PageEditor />
+        : <PageEditorReadOnly />
+      }
+    </LazyRenderer>
   );
 };

+ 11 - 0
apps/app/src/components/Page/EditablePageEffects.tsx

@@ -0,0 +1,11 @@
+import { usePageUpdatedEffect } from '~/client/services/side-effects/page-updated';
+import { useCurrentPageYjsDataEffect } from '~/client/services/side-effects/yjs';
+
+export const EditablePageEffects = (): JSX.Element => {
+
+  usePageUpdatedEffect();
+  useCurrentPageYjsDataEffect();
+
+  return <></>;
+
+};

+ 1 - 1
apps/app/src/components/Page/RevisionLoader.tsx

@@ -9,7 +9,7 @@ import { useSWRxPageRevision } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 
 
-import RevisionRenderer from './RevisionRenderer';
+import RevisionRenderer from '../../components-universal/PageView/RevisionRenderer';
 
 export const ROOT_ELEM_ID = 'revision-loader' as const;
 

+ 1 - 2
apps/app/src/components/PageAttachment/DeleteAttachmentModal.tsx

@@ -2,7 +2,6 @@ import React, {
   useCallback, useMemo, useState,
 } from 'react';
 
-import type { IUser } from '@growi/core';
 import { UserPicture, LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import {
@@ -13,7 +12,7 @@ import { toastSuccess, toastError } from '~/client/util/toastr';
 import { useDeleteAttachmentModal } from '~/stores/modal';
 import loggerFactory from '~/utils/logger';
 
-import { Username } from '../User/Username';
+import { Username } from '../../components-universal/User/Username';
 
 import styles from './DeleteAttachmentModal.module.scss';
 

+ 2 - 2
apps/app/src/components/PageComment/Comment.tsx

@@ -12,10 +12,10 @@ import urljoin from 'url-join';
 import type { RendererOptions } from '~/interfaces/renderer-options';
 
 
+import RevisionRenderer from '../../components-universal/PageView/RevisionRenderer';
+import { Username } from '../../components-universal/User/Username';
 import type { ICommentHasId } from '../../interfaces/comment';
 import FormattedDistanceDate from '../FormattedDistanceDate';
-import RevisionRenderer from '../Page/RevisionRenderer';
-import { Username } from '../User/Username';
 
 import { CommentControl } from './CommentControl';
 import { CommentEditor } from './CommentEditor';

+ 1 - 1
apps/app/src/components/PageComment/CommentPreview.tsx

@@ -1,6 +1,6 @@
 import { useCommentPreviewOptions } from '~/stores/renderer';
 
-import RevisionRenderer from '../Page/RevisionRenderer';
+import RevisionRenderer from '../../components-universal/PageView/RevisionRenderer';
 
 
 import styles from './CommentPreview.module.scss';

+ 1 - 1
apps/app/src/components/PageComment/DeleteCommentModal.tsx

@@ -8,8 +8,8 @@ import {
   Button, Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 
+import { Username } from '../../components-universal/User/Username';
 import type { ICommentHasId } from '../../interfaces/comment';
-import { Username } from '../User/Username';
 
 import styles from './DeleteCommentModal.module.scss';
 

+ 4 - 1
apps/app/src/components/PageControls/PageControls.tsx

@@ -17,10 +17,13 @@ import {
   toggleLike, toggleSubscribe,
 } from '~/client/services/page-operation';
 import { toastError } from '~/client/util/toastr';
+import {
+  EditorMode, useEditorMode,
+} from '~/stores-universal/ui';
 import { useIsGuestUser, useIsReadOnlyUser, useIsSearchPage } from '~/stores/context';
 import { useTagEditModal, type IPageForPageDuplicateModal } from '~/stores/modal';
 import {
-  EditorMode, useEditorMode, useIsDeviceLargerThanMd, usePageControlsX,
+  useIsDeviceLargerThanMd, usePageControlsX,
 } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 

+ 3 - 5
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -18,11 +18,12 @@ import detectIndent from 'detect-indent';
 import { useTranslation } from 'next-i18next';
 import { throttle, debounce } from 'throttle-debounce';
 
-import { useShouldExpandContent } from '~/client/services/layout';
 import { useUpdateStateAfterSave } from '~/client/services/page-operation';
 import { updatePage, extractRemoteRevisionDataFromErrorObj } from '~/client/services/update-page';
 import { uploadAttachments } from '~/client/services/upload-attachments';
 import { toastError, toastSuccess, toastWarning } from '~/client/util/toastr';
+import { useShouldExpandContent } from '~/services/layout/use-should-expand-content';
+import { EditorMode, useEditorMode } from '~/stores-universal/ui';
 import { useNextThemes } from '~/stores-universal/use-next-themes';
 import {
   useDefaultIndentSize, useCurrentUser,
@@ -41,10 +42,7 @@ import {
 } from '~/stores/page';
 import { mutatePageTree } from '~/stores/page-listing';
 import { usePreviewOptions } from '~/stores/renderer';
-import {
-  EditorMode,
-  useEditorMode, useIsUntitledPage, useSelectedGrant,
-} from '~/stores/ui';
+import { useIsUntitledPage, useSelectedGrant } from '~/stores/ui';
 import { useEditingUsers } from '~/stores/use-editing-users';
 import loggerFactory from '~/utils/logger';
 

+ 1 - 1
apps/app/src/components/PageEditor/PageEditorReadOnly.tsx

@@ -4,7 +4,7 @@ import { GlobalCodeMirrorEditorKey } from '@growi/editor';
 import { CodeMirrorEditorReadOnly } from '@growi/editor/dist/client/components/CodeMirrorEditorReadOnly';
 import { throttle } from 'throttle-debounce';
 
-import { useShouldExpandContent } from '~/client/services/layout';
+import { useShouldExpandContent } from '~/services/layout/use-should-expand-content';
 import { useSWRxCurrentPage, useIsLatestRevision } from '~/stores/page';
 import { usePreviewOptions } from '~/stores/renderer';
 

+ 1 - 1
apps/app/src/components/PageEditor/Preview.tsx

@@ -5,7 +5,7 @@ import { useSlidesByFrontmatter } from '@growi/presentation/dist/services';
 import type { RendererOptions } from '~/interfaces/renderer-options';
 import { useIsEnabledMarp } from '~/stores/context';
 
-import RevisionRenderer from '../Page/RevisionRenderer';
+import RevisionRenderer from '../../components-universal/PageView/RevisionRenderer';
 import { SlideRenderer } from '../Page/SlideRenderer';
 
 import styles from './Preview.module.scss';

+ 1 - 1
apps/app/src/components/PageEditor/conflict.tsx

@@ -10,11 +10,11 @@ import { useUpdateStateAfterSave } from '~/client/services/page-operation';
 import { toastSuccess } from '~/client/util/toastr';
 import type { Save, SaveOptions } from '~/components/PageEditor/PageEditor';
 import { SocketEventName } from '~/interfaces/websocket';
+import { EditorMode, useEditorMode } from '~/stores-universal/ui';
 import { usePageStatusAlert } from '~/stores/alert';
 import { useConflictDiffModal } from '~/stores/modal';
 import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 import { type RemoteRevisionData, useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
-import { EditorMode, useEditorMode } from '~/stores/ui';
 
 
 export type ConflictHandler = (

+ 1 - 1
apps/app/src/components/PageHeader/PagePathHeader.tsx

@@ -14,7 +14,7 @@ import { ValidationTarget, useInputValidator } from '~/client/util/use-input-val
 import LinkedPagePath from '~/models/linked-page-path';
 import { usePageSelectModal } from '~/stores/modal';
 
-import { PagePathHierarchicalLink } from '../Common/PagePathHierarchicalLink';
+import { PagePathHierarchicalLink } from '../../components-universal/Common/PagePathHierarchicalLink';
 import { AutosizeSubmittableInput, getAdjustedMaxWidthForAutosizeInput } from '../Common/SubmittableInput';
 import { usePagePathRenameHandler } from '../PageEditor/page-path-rename-utils';
 import { PageSelectModal } from '../PageSelectModal/PageSelectModal';

+ 2 - 1
apps/app/src/components/PageHeader/PageTitleHeader.tsx

@@ -11,7 +11,8 @@ import { useTranslation } from 'next-i18next';
 
 import type { InputValidationResult } from '~/client/util/use-input-validator';
 import { ValidationTarget, useInputValidator } from '~/client/util/use-input-validator';
-import { EditorMode, useEditorMode, useIsUntitledPage } from '~/stores/ui';
+import { EditorMode, useEditorMode } from '~/stores-universal/ui';
+import { useIsUntitledPage } from '~/stores/ui';
 
 import { CopyDropdown } from '../Common/CopyDropdown';
 import { AutosizeSubmittableInput, getAdjustedMaxWidthForAutosizeInput } from '../Common/SubmittableInput';

+ 2 - 2
apps/app/src/components/PageHistory/Revision.tsx

@@ -7,8 +7,8 @@ import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
 import urljoin from 'url-join';
 
-import UserDate from '../User/UserDate';
-import { Username } from '../User/Username';
+import UserDate from '../../components-universal/User/UserDate';
+import { Username } from '../../components-universal/User/Username';
 
 import styles from './Revision.module.scss';
 

+ 1 - 1
apps/app/src/components/PageHistory/RevisionDiff.tsx

@@ -12,7 +12,7 @@ import urljoin from 'url-join';
 
 import { Themes, useNextThemes } from '~/stores-universal/use-next-themes';
 
-import UserDate from '../User/UserDate';
+import UserDate from '../../components-universal/User/UserDate';
 
 import styles from './RevisionDiff.module.scss';
 

+ 1 - 1
apps/app/src/components/PageList/PageListItemL.tsx

@@ -34,7 +34,7 @@ import { useIsDeviceLargerThanLg } from '~/stores/ui';
 import { useSWRMUTxPageInfo, useSWRxPageInfo } from '../../stores/page';
 import type { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import { PageItemControl } from '../Common/Dropdown/PageItemControl';
-import { PagePathHierarchicalLink } from '../Common/PagePathHierarchicalLink';
+import { PagePathHierarchicalLink } from '../../components-universal/Common/PagePathHierarchicalLink';
 
 type Props = {
   page: IPageWithSearchMeta | IPageWithMeta<IPageInfoForListing & IPageSearchMeta>,

+ 0 - 0
apps/app/src/components/Common/PagePathHierarchicalLink/CollapsedParentsDropdown.module.scss → apps/app/src/components/PagePathNavSticky/CollapsedParentsDropdown.module.scss


+ 0 - 0
apps/app/src/components/Common/PagePathHierarchicalLink/CollapsedParentsDropdown.tsx → apps/app/src/components/PagePathNavSticky/CollapsedParentsDropdown.tsx


+ 15 - 0
apps/app/src/components/PagePathNavSticky/PagePathNavSticky.module.scss

@@ -0,0 +1,15 @@
+@use '@growi/core-styles/scss/bootstrap/init' as bs;
+
+.grw-page-path-nav-sticky :global {
+  .sticky-inner-wrapper {
+    z-index: bs.$zindex-sticky;
+  }
+
+  // TODO:Responsive font size
+  // set smaller font-size when sticky
+  .sticky-inner-wrapper.active {
+    h1 {
+      font-size: 1.75rem !important;
+    }
+  }
+}

+ 114 - 0
apps/app/src/components/PagePathNavSticky/PagePathNavSticky.tsx

@@ -0,0 +1,114 @@
+import {
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from 'react';
+
+import { DevidedPagePath } from '@growi/core/dist/models';
+import { pagePathUtils } from '@growi/core/dist/utils';
+import Sticky from 'react-stickynode';
+
+import LinkedPagePath from '~/models/linked-page-path';
+import {
+  usePageControlsX, useCurrentProductNavWidth, useSidebarMode,
+} from '~/stores/ui';
+
+import { PagePathHierarchicalLink } from '../../components-universal/Common/PagePathHierarchicalLink';
+import type { PagePathNavLayoutProps } from '../../components-universal/Common/PagePathNav';
+import { PagePathNav, PagePathNavLayout, Separator } from '../../components-universal/Common/PagePathNav';
+
+import { CollapsedParentsDropdown } from './CollapsedParentsDropdown';
+
+import styles from './PagePathNavSticky.module.scss';
+
+const moduleClass = styles['grw-page-path-nav-sticky'];
+
+const { isTrashPage } = pagePathUtils;
+
+
+export const PagePathNavSticky = (props: PagePathNavLayoutProps): JSX.Element => {
+  const { pagePath } = props;
+
+  const { data: pageControlsX } = usePageControlsX();
+  const { data: sidebarWidth } = useCurrentProductNavWidth();
+  const { data: sidebarMode } = useSidebarMode();
+  const pagePathNavRef = useRef<HTMLDivElement>(null);
+
+  const [navMaxWidth, setNavMaxWidth] = useState<number | undefined>();
+
+  useEffect(() => {
+    if (pageControlsX == null || pagePathNavRef.current == null || sidebarWidth == null) {
+      return;
+    }
+    setNavMaxWidth(pageControlsX - pagePathNavRef.current.getBoundingClientRect().x - 10);
+  }, [pageControlsX, pagePathNavRef, sidebarWidth]);
+
+  useEffect(() => {
+    // wait for the end of the animation of the opening and closing of the sidebar
+    const timeout = setTimeout(() => {
+      if (pageControlsX == null || pagePathNavRef.current == null || sidebarMode == null) {
+        return;
+      }
+      setNavMaxWidth(pageControlsX - pagePathNavRef.current.getBoundingClientRect().x - 10);
+    }, 200);
+    return () => {
+      clearTimeout(timeout);
+    };
+  }, [pageControlsX, pagePathNavRef, sidebarMode]);
+
+  const latterLink = useMemo(() => {
+    const dPagePath = new DevidedPagePath(pagePath, false, true);
+
+    const isInTrash = isTrashPage(pagePath);
+
+    const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
+    const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
+
+    // not collapsed
+    if (dPagePath.isRoot || dPagePath.isFormerRoot) {
+      const linkedPagePath = new LinkedPagePath(pagePath);
+      return <PagePathHierarchicalLink linkedPagePath={linkedPagePath} isInTrash={isInTrash} />;
+    }
+
+    // collapsed
+    return (
+      <>
+        <CollapsedParentsDropdown linkedPagePath={linkedPagePathFormer} />
+        <Separator />
+        <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.former} isInTrash={isInTrash} />
+      </>
+    );
+  }, [pagePath]);
+
+  return (
+    // Controlling pointer-events
+    //  1. disable pointer-events with 'pe-none'
+    <div ref={pagePathNavRef}>
+      <Sticky className={`${moduleClass} mb-4`} innerClass="pe-none" innerActiveClass="active mt-1">
+        {({ status }) => {
+          const isCollapseParents = status === Sticky.STATUS_FIXED;
+          return (
+          // Controlling pointer-events
+          //  2. enable pointer-events with 'pe-auto' only against the children
+          //      which width is minimized by 'd-inline-block'
+          //
+            <div className="d-inline-block pe-auto">
+              { !isCollapseParents && <PagePathNav {...props} /> }
+              { isCollapseParents && (
+                <PagePathNavLayout
+                  {...props}
+                  latterLink={latterLink}
+                  latterLinkClassName="fs-3 text-truncate"
+                  maxWidth={isCollapseParents ? navMaxWidth : undefined}
+                />
+              ) }
+            </div>
+          );
+        }}
+      </Sticky>
+    </div>
+  );
+};
+
+PagePathNavSticky.displayName = 'PagePathNavSticky';

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

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

+ 2 - 3
apps/app/src/components/PageSideContents/PageSideContents.tsx

@@ -1,7 +1,6 @@
 import React, { Suspense, useCallback, useRef } from 'react';
 
-import type { IPagePopulatedToShowRevision } from '@growi/core';
-import { getIdForRef, type IPageInfoForOperation } from '@growi/core';
+import type { IPagePopulatedToShowRevision, IPageInfoForOperation } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
@@ -70,7 +69,7 @@ const Tags = (props: TagsProps): JSX.Element => {
 };
 
 
-export type PageSideContentsProps = {
+type PageSideContentsProps = {
   page: IPagePopulatedToShowRevision,
   isSharedUser?: boolean,
 }

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

@@ -2,13 +2,13 @@ import React, { useCallback } from 'react';
 
 import { useTranslation } from 'next-i18next';
 
+import { useEditorMode } from '~/stores-universal/ui';
 import { usePageStatusAlert } from '~/stores/alert';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useRemoteRevisionId, useRemoteRevisionLastUpdateUser } from '~/stores/remote-latest-page';
-import { useEditorMode } from '~/stores/ui';
 
-import { Username } from './User/Username';
+import { Username } from '../components-universal/User/Username';
 
 import styles from './PageStatusAlert.module.scss';
 

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

@@ -10,16 +10,14 @@ import {
   DropdownToggle, DropdownMenu, DropdownItem, Modal,
 } from 'reactstrap';
 
+import { useEditorMode } from '~/stores-universal/ui';
 import {
   useIsEditable, useIsAclEnabled,
   useIsSlackConfigured,
 } from '~/stores/context';
 import { useWaitingSaveProcessing, useSWRxSlackChannels, useIsSlackEnabled } from '~/stores/editor';
 import { useSWRxCurrentPage, useCurrentPagePath } from '~/stores/page';
-import {
-  useEditorMode, useIsDeviceLargerThanMd,
-
-} from '~/stores/ui';
+import { useIsDeviceLargerThanMd } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 
 import { GrantSelector } from './SavePageControls/GrantSelector';

+ 7 - 5
apps/app/src/components/SearchPage/SearchResultContent.tsx

@@ -11,11 +11,12 @@ import { animateScroll } from 'react-scroll';
 import { DropdownItem } from 'reactstrap';
 import { debounce } from 'throttle-debounce';
 
-import { useShouldExpandContent } from '~/client/services/layout';
 import { exportAsMarkdown, updateContentWidth } from '~/client/services/page-operation';
 import { toastSuccess } from '~/client/util/toastr';
+import { PagePathNav } from '~/components-universal/Common/PagePathNav';
 import type { IPageWithSearchMeta } from '~/interfaces/search';
 import type { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
+import { useShouldExpandContent } from '~/services/layout/use-should-expand-content';
 import { useCurrentUser } from '~/stores/context';
 import {
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal,
@@ -25,9 +26,7 @@ import { useSearchResultOptions } from '~/stores/renderer';
 import { mutateSearching } from '~/stores/search';
 
 import type { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
-import { PagePathNav } from '../Common/PagePathNav';
-import { type RevisionLoaderProps } from '../Page/RevisionLoader';
-import type { PageContentFooterProps } from '../PageContentFooter';
+import type { RevisionLoaderProps } from '../Page/RevisionLoader';
 
 import styles from './SearchResultContent.module.scss';
 
@@ -38,7 +37,10 @@ const _fluidLayoutClass = styles['fluid-layout'];
 const PageControls = dynamic(() => import('../PageControls').then(mod => mod.PageControls), { ssr: false });
 const RevisionLoader = dynamic<RevisionLoaderProps>(() => import('../Page/RevisionLoader').then(mod => mod.RevisionLoader), { ssr: false });
 const PageComment = dynamic(() => import('../PageComment').then(mod => mod.PageComment), { ssr: false });
-const PageContentFooter = dynamic<PageContentFooterProps>(() => import('../PageContentFooter').then(mod => mod.PageContentFooter), { ssr: false });
+const PageContentFooter = dynamic(
+  () => import('~/components-universal/PageView/PageContentFooter').then(mod => mod.PageContentFooter),
+  { ssr: false },
+);
 
 type AdditionalMenuItemsProps = AdditionalMenuItemsRendererProps & {
   pageId: string,

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

@@ -1,6 +1,6 @@
 import React from 'react';
 
-import RevisionRenderer from '~/components/Page/RevisionRenderer';
+import RevisionRenderer from '~/components-universal/PageView/RevisionRenderer';
 import { useSWRxPageByPath } from '~/stores/page';
 import { useCustomSidebarOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';

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

@@ -10,7 +10,7 @@ import { UserPicture } from '@growi/ui/dist/components';
 import { useTranslation } from 'react-i18next';
 
 import { useKeywordManager } from '~/client/services/search-operation';
-import { PagePathHierarchicalLink } from '~/components/Common/PagePathHierarchicalLink';
+import { PagePathHierarchicalLink } from '~/components-universal/Common/PagePathHierarchicalLink';
 import FormattedDistanceDate from '~/components/FormattedDistanceDate';
 import InfiniteScroll from '~/components/InfiniteScroll';
 import LinkedPagePath from '~/models/linked-page-path';

+ 3 - 13
apps/app/src/components/Sidebar/ResizableArea/ResizableArea.tsx

@@ -1,21 +1,11 @@
-import React, { memo, useCallback, useRef } from 'react';
+import { memo, useCallback, useRef } from 'react';
 
+import type { ResizableAreaProps } from './props';
 
 import styles from './ResizableArea.module.scss';
 
 
-type Props = {
-  className?: string,
-  width?: number,
-  minWidth?: number,
-  disabled?: boolean,
-  children?: React.ReactNode,
-  onResize?: (newWidth: number) => void,
-  onResizeDone?: (newWidth: number) => void,
-  onCollapsed?: () => void,
-}
-
-export const ResizableArea = memo((props: Props): JSX.Element => {
+export const ResizableArea = memo((props: ResizableAreaProps): JSX.Element => {
   const {
     className,
     width, minWidth = 0,

+ 25 - 0
apps/app/src/components/Sidebar/ResizableArea/ResizableAreaFallback.tsx

@@ -0,0 +1,25 @@
+import { memo } from 'react';
+
+
+type Props = {
+  className?: string,
+  width?: number,
+  children?: React.ReactNode,
+}
+
+export const ResizableAreaFallback = memo((props: Props): JSX.Element => {
+  const {
+    className = '',
+    width,
+    children,
+  } = props;
+
+  return (
+    <div
+      className={className}
+      style={{ width }}
+    >
+      {children}
+    </div>
+  );
+});

+ 10 - 0
apps/app/src/components/Sidebar/ResizableArea/props.d.ts

@@ -0,0 +1,10 @@
+export type ResizableAreaProps = {
+  className?: string,
+  width?: number,
+  minWidth?: number,
+  disabled?: boolean,
+  children?: React.ReactNode,
+  onResize?: (newWidth: number) => void,
+  onResizeDone?: (newWidth: number) => void,
+  onCollapsed?: () => void,
+}

+ 56 - 27
apps/app/src/components/Sidebar/Sidebar.tsx

@@ -1,10 +1,12 @@
-import React, {
+import {
   type FC,
   memo, useCallback, useEffect, useState,
   useRef,
 } from 'react';
 
+import withLoadingProps from 'next-dynamic-loading-props';
 import dynamic from 'next/dynamic';
+import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 
 import { SidebarMode } from '~/interfaces/ui';
 import { useIsSearchPage } from '~/stores/context';
@@ -20,7 +22,8 @@ import {
 import { DrawerToggler } from '../Common/DrawerToggler';
 
 import { AppTitleOnSidebarHead, AppTitleOnSubnavigation } from './AppTitle/AppTitle';
-import { ResizableArea } from './ResizableArea/ResizableArea';
+import { ResizableAreaFallback } from './ResizableArea/ResizableAreaFallback';
+import type { ResizableAreaProps } from './ResizableArea/props';
 import { SidebarHead } from './SidebarHead';
 import { SidebarNav, type SidebarNavProps } from './SidebarNav';
 
@@ -28,11 +31,32 @@ import styles from './Sidebar.module.scss';
 
 
 const SidebarContents = dynamic(() => import('./SidebarContents').then(mod => mod.SidebarContents), { ssr: false });
+const ResizableArea = withLoadingProps<ResizableAreaProps>(useLoadingProps => dynamic(
+  () => import('./ResizableArea').then(mod => mod.ResizableArea),
+  {
+    ssr: false,
+    loading: () => {
+      // eslint-disable-next-line react-hooks/rules-of-hooks
+      const { children, ...rest } = useLoadingProps();
+      return <ResizableAreaFallback {...rest}>{children}</ResizableAreaFallback>;
+    },
+  },
+));
 
 
 const resizableAreaMinWidth = 348;
 const sidebarNavCollapsedWidth = 48;
 
+const getWidthByMode = (isDrawerMode: boolean, isCollapsedMode: boolean, currentProductNavWidth: number | undefined): number | undefined => {
+  if (isDrawerMode) {
+    return undefined;
+  }
+  if (isCollapsedMode) {
+    return sidebarNavCollapsedWidth;
+  }
+  return currentProductNavWidth;
+};
+
 
 type ResizableContainerProps = {
   children?: React.ReactNode,
@@ -48,7 +72,10 @@ const ResizableContainer = memo((props: ResizableContainerProps): JSX.Element =>
   const { mutateAndSave: mutatePreferCollapsedMode } = usePreferCollapsedMode();
   const { mutate: mutateCollapsedContentsOpened } = useCollapsedContentsOpened();
 
-  const [resizableAreaWidth, setResizableAreaWidth] = useState<number|undefined>(undefined);
+  const [isClient, setClient] = useState(false);
+  const [resizableAreaWidth, setResizableAreaWidth] = useState<number|undefined>(
+    getWidthByMode(isDrawerMode(), isCollapsedMode(), currentProductNavWidth),
+  );
 
   const resizeHandler = useCallback((newWidth: number) => {
     setResizableAreaWidth(newWidth);
@@ -63,36 +90,38 @@ const ResizableContainer = memo((props: ResizableContainerProps): JSX.Element =>
     mutateCollapsedContentsOpened(false);
   }, [mutateCollapsedContentsOpened, mutatePreferCollapsedMode]);
 
+  useIsomorphicLayoutEffect(() => {
+    setClient(true);
+  }, []);
 
   // open/close resizable container when drawer mode
   useEffect(() => {
-    if (isDrawerMode()) {
-      setResizableAreaWidth(undefined);
-    }
-    else if (isCollapsedMode()) {
-      setResizableAreaWidth(sidebarNavCollapsedWidth);
-    }
-    else {
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      setResizableAreaWidth(currentProductNavWidth!);
-    }
-
+    setResizableAreaWidth(getWidthByMode(isDrawerMode(), isCollapsedMode(), currentProductNavWidth));
     mutateDrawerOpened(false);
   }, [currentProductNavWidth, isCollapsedMode, isDrawerMode, mutateDrawerOpened]);
 
-  return (
-    <ResizableArea
-      className="flex-expand-vert"
-      width={resizableAreaWidth}
-      minWidth={resizableAreaMinWidth}
-      disabled={!isDockMode()}
-      onResize={resizeHandler}
-      onResizeDone={resizeDoneHandler}
-      onCollapsed={collapsedByResizableAreaHandler}
-    >
-      {children}
-    </ResizableArea>
-  );
+  return !isClient
+    ? (
+      <ResizableAreaFallback
+        className="flex-expand-vert"
+        width={resizableAreaWidth}
+      >
+        {children}
+      </ResizableAreaFallback>
+    )
+    : (
+      <ResizableArea
+        className="flex-expand-vert"
+        width={resizableAreaWidth}
+        minWidth={resizableAreaMinWidth}
+        disabled={!isDockMode()}
+        onResize={resizeHandler}
+        onResizeDone={resizeDoneHandler}
+        onCollapsed={collapsedByResizableAreaHandler}
+      >
+        {children}
+      </ResizableArea>
+    );
 
 });
 

+ 1 - 1
apps/app/src/components/UsersHomepageFooter.tsx

@@ -8,7 +8,7 @@ import { useCurrentUser } from '~/stores/context';
 
 import { BookmarkFolderTree } from './Bookmarks/BookmarkFolderTree';
 
-export type UsersHomepageFooterProps = {
+type UsersHomepageFooterProps = {
   creatorId: string;
 };
 

+ 2 - 0
apps/app/src/pages/.eslintrc.js

@@ -8,6 +8,8 @@ module.exports = {
         '~/client/',
         '../components/',
         '~/components/',
+        '~/stores/',
+        'stores/',
       ],
     }],
   },

+ 20 - 15
apps/app/src/pages/[[...path]].page.tsx

@@ -20,10 +20,9 @@ import Head from 'next/head';
 import { useRouter } from 'next/router';
 import superjson from 'superjson';
 
-import { useEditorModeClassName } from '~/client/services/layout';
 import { BasicLayout } from '~/components-universal/Layout/BasicLayout';
+import { PageView } from '~/components-universal/PageView/PageView';
 import { DrawioViewerScript } from '~/components-universal/Script/DrawioViewerScript';
-import { PageView } from '~/components/Page/PageView';
 import { SupportedAction, type SupportedActionType } from '~/interfaces/activity';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
@@ -31,6 +30,7 @@ import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { CurrentPageYjsData } from '~/interfaces/yjs';
 import type { PageModel, PageDocument } from '~/server/models/page';
 import type { PageRedirectModel } from '~/server/models/page-redirect';
+import { useEditorModeClassName } from '~/services/layout/use-editor-mode-class-name';
 import {
   useCurrentUser,
   useIsForbidden, useIsSharedUser,
@@ -42,7 +42,7 @@ import {
   useIsSlackConfigured, useRendererConfig, useGrowiCloudUri,
   useIsAllReplyShown, useIsContainerFluid, useIsNotCreatable,
   useIsUploadAllFileAllowed, useIsUploadEnabled,
-} from '~/stores/context';
+} from '~/stores-universal/context';
 import { useEditingMarkdown } from '~/stores/editor';
 import {
   useSWRxCurrentPage, useSWRMUTxCurrentPage, useCurrentPageId,
@@ -55,7 +55,6 @@ import { useCurrentPageYjsData, useSWRMUTxCurrentPageYjsData } from '~/stores/yj
 import loggerFactory from '~/utils/logger';
 
 import GrowiContextualSubNavigationSubstance from '../components/Navbar/GrowiContextualSubNavigation';
-import { DisplaySwitcher } from '../components/Page/DisplaySwitcher';
 
 import type { NextPageWithLayout } from './_app.page';
 import type { CommonProps } from './utils/commons';
@@ -71,16 +70,22 @@ declare global {
 
 
 const GrowiPluginsActivator = dynamic(() => import('~/features/growi-plugin/client/components').then(mod => mod.GrowiPluginsActivator), { ssr: false });
-const DescendantsPageListModal = dynamic(() => import('../components/DescendantsPageListModal').then(mod => mod.DescendantsPageListModal), { ssr: false });
+
+const DisplaySwitcher = dynamic(() => import('../components/Page/DisplaySwitcher').then(mod => mod.DisplaySwitcher), { ssr: false });
+const PageStatusAlert = dynamic(() => import('../components/PageStatusAlert').then(mod => mod.PageStatusAlert), { ssr: false });
+
 const UnsavedAlertDialog = dynamic(() => import('../components/UnsavedAlertDialog'), { ssr: false });
+const DescendantsPageListModal = dynamic(() => import('../components/DescendantsPageListModal').then(mod => mod.DescendantsPageListModal), { ssr: false });
 const DrawioModal = dynamic(() => import('../components/PageEditor/DrawioModal').then(mod => mod.DrawioModal), { ssr: false });
 const HandsontableModal = dynamic(() => import('../components/PageEditor/HandsontableModal').then(mod => mod.HandsontableModal), { ssr: false });
 const TemplateModal = dynamic(() => import('../components/TemplateModal').then(mod => mod.TemplateModal), { ssr: false });
 const LinkEditModal = dynamic(() => import('../components/PageEditor/LinkEditModal').then(mod => mod.LinkEditModal), { ssr: false });
-const PageStatusAlert = dynamic(() => import('../components/PageStatusAlert').then(mod => mod.PageStatusAlert), { ssr: false });
-const QuestionnaireModalManager = dynamic(() => import('~/features/questionnaire/client/components/QuestionnaireModalManager'), { ssr: false });
 const TagEditModal = dynamic(() => import('../components/PageTags/TagEditModal').then(mod => mod.TagEditModal), { ssr: false });
 const ConflictDiffModal = dynamic(() => import('../components/PageEditor/ConflictDiffModal').then(mod => mod.ConflictDiffModal), { ssr: false });
+const QuestionnaireModalManager = dynamic(() => import('~/features/questionnaire/client/components/QuestionnaireModalManager'), { ssr: false });
+
+const EditablePageEffects = dynamic(() => import('../components/Page/EditablePageEffects').then(mod => mod.EditablePageEffects), { ssr: false });
+
 
 const logger = loggerFactory('growi:pages:all');
 
@@ -334,16 +339,16 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
 
         <GrowiContextualSubNavigation isLinkSharingDisabled={props.disableLinkSharing} />
 
-        <DisplaySwitcher
-          pageView={(
-            <PageView
-              pagePath={pagePath}
-              initialPage={pageWithMeta?.data}
-              rendererConfig={props.rendererConfig}
-            />
-          )}
+        <PageView
+          className="d-edit-none"
+          pagePath={pagePath}
+          initialPage={pageWithMeta?.data}
+          rendererConfig={props.rendererConfig}
         />
 
+        <EditablePageEffects />
+        <DisplaySwitcher />
+
         <PageStatusAlert />
       </div>
     </>

+ 1 - 1
apps/app/src/pages/_app.page.tsx

@@ -11,7 +11,7 @@ import * as nextI18nConfig from '^/config/next-i18next.config';
 import { GlobalFonts } from '~/components-universal/FontFamily/GlobalFonts';
 import {
   useAppTitle, useConfidential, useGrowiVersion, useSiteUrl, useIsDefaultLogo, useForcedColorScheme,
-} from '~/stores/context';
+} from '~/stores-universal/context';
 import { swrGlobalConfiguration } from '~/utils/swr-utils';
 
 import type { CommonProps } from './utils/commons';

+ 1 - 1
apps/app/src/pages/_private-legacy-pages.page.tsx

@@ -14,7 +14,7 @@ import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import {
   useCsrfToken, useCurrentUser, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig, useGrowiCloudUri, useIsEnabledMarp, useCurrentPathname,
-} from '~/stores/context';
+} from '~/stores-universal/context';
 import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 
 import type { CommonProps } from './utils/commons';

+ 1 - 1
apps/app/src/pages/_search.page.tsx

@@ -15,7 +15,7 @@ import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import {
   useCsrfToken, useCurrentUser, useIsContainerFluid, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig, useShowPageLimitationL, useGrowiCloudUri, useCurrentPathname,
-} from '~/stores/context';
+} from '~/stores-universal/context';
 import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 
 import type { NextPageWithLayout } from './_app.page';

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

@@ -5,7 +5,7 @@ import dynamic from 'next/dynamic';
 
 import AdminLayout from '~/components-universal/Layout/AdminLayout';
 import type { CommonProps } from '~/pages/utils/commons';
-import { useCurrentUser } from '~/stores/context';
+import { useCurrentUser } from '~/stores-universal/context';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';

+ 1 - 1
apps/app/src/pages/admin/app.page.tsx

@@ -11,7 +11,7 @@ import { Provider } from 'unstated';
 
 import type { CommonProps } from '~/pages/utils/commons';
 import { generateCustomTitle } from '~/pages/utils/commons';
-import { useCurrentUser } from '~/stores/context';
+import { useCurrentUser } from '~/stores-universal/context';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';

+ 1 - 1
apps/app/src/pages/admin/audit-log.page.tsx

@@ -11,7 +11,7 @@ import type { CommonProps } from '~/pages/utils/commons';
 import { generateCustomTitle } from '~/pages/utils/commons';
 import {
   useCurrentUser, useAuditLogEnabled, useAuditLogAvailableActions, useActivityExpirationSeconds,
-} from '~/stores/context';
+} from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 

+ 1 - 1
apps/app/src/pages/admin/customize.page.tsx

@@ -12,7 +12,7 @@ import { Provider } from 'unstated';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CommonProps } from '~/pages/utils/commons';
 import { generateCustomTitle } from '~/pages/utils/commons';
-import { useCustomizeTitle, useCurrentUser, useIsCustomizedLogoUploaded } from '~/stores/context';
+import { useCustomizeTitle, useCurrentUser, useIsCustomizedLogoUploaded } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 

+ 1 - 1
apps/app/src/pages/admin/data-transfer.page.tsx

@@ -10,7 +10,7 @@ import type { Container } from 'unstated';
 import { Provider } from 'unstated';
 
 import type { CommonProps } from '~/pages/utils/commons';
-import { useCurrentUser } from '~/stores/context';
+import { useCurrentUser } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 

+ 1 - 1
apps/app/src/pages/admin/export.page.tsx

@@ -11,7 +11,7 @@ import { Provider } from 'unstated';
 
 import type { CommonProps } from '~/pages/utils/commons';
 import { generateCustomTitle } from '~/pages/utils/commons';
-import { useCurrentUser } from '~/stores/context';
+import { useCurrentUser } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 

+ 1 - 1
apps/app/src/pages/admin/global-notification/[globalNotificationId].page.tsx

@@ -13,7 +13,7 @@ import { Provider } from 'unstated';
 
 import type { CommonProps } from '~/pages/utils/commons';
 import { generateCustomTitle } from '~/pages/utils/commons';
-import { useCurrentUser } from '~/stores/context';
+import { useCurrentUser } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../../utils/admin-page-util';
 

+ 1 - 1
apps/app/src/pages/admin/global-notification/new.page.tsx

@@ -10,7 +10,7 @@ import type { Container } from 'unstated';
 import { Provider } from 'unstated';
 
 import type { CommonProps } from '~/pages/utils/commons';
-import { useCurrentUser } from '~/stores/context';
+import { useCurrentUser } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../../utils/admin-page-util';
 

+ 1 - 1
apps/app/src/pages/admin/importer.page.tsx

@@ -11,7 +11,7 @@ import { Provider } from 'unstated';
 
 import type { CommonProps } from '~/pages/utils/commons';
 import { generateCustomTitle } from '~/pages/utils/commons';
-import { useCurrentUser } from '~/stores/context';
+import { useCurrentUser } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 

+ 1 - 1
apps/app/src/pages/admin/index.page.tsx

@@ -14,7 +14,7 @@ import type { CommonProps } from '~/pages/utils/commons';
 import { generateCustomTitle } from '~/pages/utils/commons';
 import {
   useCurrentUser, useGrowiCloudUri, useGrowiAppIdForGrowiCloud,
-} from '~/stores/context';
+} from '~/stores-universal/context';
 
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';

+ 1 - 1
apps/app/src/pages/admin/markdown.page.tsx

@@ -11,7 +11,7 @@ import { Provider } from 'unstated';
 
 import type { CommonProps } from '~/pages/utils/commons';
 import { generateCustomTitle } from '~/pages/utils/commons';
-import { useCurrentUser } from '~/stores/context';
+import { useCurrentUser } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 

+ 1 - 1
apps/app/src/pages/admin/notification.page.tsx

@@ -11,7 +11,7 @@ import { Provider } from 'unstated';
 
 import type { CommonProps } from '~/pages/utils/commons';
 import { generateCustomTitle } from '~/pages/utils/commons';
-import { useCurrentUser } from '~/stores/context';
+import { useCurrentUser } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 

+ 1 - 1
apps/app/src/pages/admin/plugins.page.tsx

@@ -12,7 +12,7 @@ import { Provider } from 'unstated';
 
 import type { CommonProps } from '~/pages/utils/commons';
 import { generateCustomTitle } from '~/pages/utils/commons';
-import { useCurrentUser } from '~/stores/context';
+import { useCurrentUser } from '~/stores-universal/context';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';

+ 1 - 1
apps/app/src/pages/admin/search.page.tsx

@@ -8,7 +8,7 @@ import Head from 'next/head';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CommonProps } from '~/pages/utils/commons';
 import { generateCustomTitle } from '~/pages/utils/commons';
-import { useIsSearchServiceReachable, useCurrentUser } from '~/stores/context';
+import { useIsSearchServiceReachable, useCurrentUser } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 

Некоторые файлы не были показаны из-за большого количества измененных файлов