Explorar o código

Merge pull request #8916 from weseek/imprv/ssr-performance

imprv: SSR performance
Yuki Takei hai 1 ano
pai
achega
e41db1b3a0
Modificáronse 100 ficheiros con 593 adicións e 502 borrados
  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. 14 0
      apps/app/src/components-universal/.eslintrc.js
  6. 0 0
      apps/app/src/components-universal/Admin/Common/AdminNavigation.module.scss
  7. 1 1
      apps/app/src/components-universal/Admin/Common/AdminNavigation.tsx
  8. 0 0
      apps/app/src/components-universal/Common/GrowiLogo.jsx
  9. 0 0
      apps/app/src/components-universal/Common/GrowiLogo.module.scss
  10. 0 0
      apps/app/src/components-universal/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.module.scss
  11. 1 1
      apps/app/src/components-universal/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.tsx
  12. 0 0
      apps/app/src/components-universal/Common/PagePathHierarchicalLink/index.ts
  13. 0 21
      apps/app/src/components-universal/Common/PagePathNav/PagePathNav.module.scss
  14. 69 0
      apps/app/src/components-universal/Common/PagePathNav/PagePathNav.tsx
  15. 62 0
      apps/app/src/components-universal/Common/PagePathNav/PagePathNavLayout.tsx
  16. 4 0
      apps/app/src/components-universal/Common/PagePathNav/Separator.module.scss
  17. 5 0
      apps/app/src/components-universal/Common/PagePathNav/Separator.tsx
  18. 3 0
      apps/app/src/components-universal/Common/PagePathNav/index.ts
  19. 5 0
      apps/app/src/components-universal/Common/PagePathNavTitle/PagePathNavTitle.module.scss
  20. 43 0
      apps/app/src/components-universal/Common/PagePathNavTitle/PagePathNavTitle.tsx
  21. 1 0
      apps/app/src/components-universal/Common/PagePathNavTitle/index.ts
  22. 0 0
      apps/app/src/components-universal/FontFamily/GlobalFonts.tsx
  23. 0 0
      apps/app/src/components-universal/FontFamily/types.d.ts
  24. 0 0
      apps/app/src/components-universal/FontFamily/use-growi-custom-icons.tsx
  25. 0 0
      apps/app/src/components-universal/FontFamily/use-lato.tsx
  26. 0 0
      apps/app/src/components-universal/FontFamily/use-material-symbols-outlined.tsx
  27. 0 0
      apps/app/src/components-universal/FontFamily/use-source-han-code-jp.tsx
  28. 0 0
      apps/app/src/components-universal/Layout/Admin.module.scss
  29. 6 4
      apps/app/src/components-universal/Layout/AdminLayout.tsx
  30. 0 0
      apps/app/src/components-universal/Layout/BasicLayout.module.scss
  31. 79 0
      apps/app/src/components-universal/Layout/BasicLayout.tsx
  32. 0 0
      apps/app/src/components-universal/Layout/NoLoginLayout.module.scss
  33. 3 2
      apps/app/src/components-universal/Layout/NoLoginLayout.tsx
  34. 0 0
      apps/app/src/components-universal/Layout/RawLayout.module.scss
  35. 1 1
      apps/app/src/components-universal/Layout/RawLayout.tsx
  36. 0 0
      apps/app/src/components-universal/Layout/SearchResultLayout.module.scss
  37. 1 1
      apps/app/src/components-universal/Layout/SearchResultLayout.tsx
  38. 33 0
      apps/app/src/components-universal/Layout/ShareLinkLayout.tsx
  39. 0 0
      apps/app/src/components-universal/Navbar/GroundGlassBar.module.scss
  40. 0 0
      apps/app/src/components-universal/Navbar/GroundGlassBar.tsx
  41. 4 3
      apps/app/src/components-universal/PageView/PageAlerts/FixPageGrantAlert.tsx
  42. 0 0
      apps/app/src/components-universal/PageView/PageAlerts/OldRevisionAlert.tsx
  43. 1 2
      apps/app/src/components-universal/PageView/PageAlerts/PageAlerts.tsx
  44. 0 0
      apps/app/src/components-universal/PageView/PageAlerts/PageGrantAlert.tsx
  45. 2 2
      apps/app/src/components-universal/PageView/PageAlerts/PageRedirectedAlert.tsx
  46. 2 2
      apps/app/src/components-universal/PageView/PageAlerts/PageStaleAlert.tsx
  47. 4 3
      apps/app/src/components-universal/PageView/PageAlerts/TrashPageAlert.tsx
  48. 7 4
      apps/app/src/components-universal/PageView/PageAlerts/WipPageAlert.tsx
  49. 1 0
      apps/app/src/components-universal/PageView/PageAlerts/index.ts
  50. 0 0
      apps/app/src/components-universal/PageView/PageContentFooter.module.scss
  51. 1 3
      apps/app/src/components-universal/PageView/PageContentFooter.tsx
  52. 0 0
      apps/app/src/components-universal/PageView/PageView.module.scss
  53. 20 24
      apps/app/src/components-universal/PageView/PageView.tsx
  54. 0 0
      apps/app/src/components-universal/PageView/PageViewLayout.module.scss
  55. 3 1
      apps/app/src/components-universal/PageView/PageViewLayout.tsx
  56. 0 0
      apps/app/src/components-universal/PageView/RevisionRenderer.tsx
  57. 1 0
      apps/app/src/components-universal/PageView/index.ts
  58. 0 0
      apps/app/src/components-universal/Script/DrawioViewerScript/DrawioViewerScript.tsx
  59. 0 0
      apps/app/src/components-universal/Script/DrawioViewerScript/index.ts
  60. 0 0
      apps/app/src/components-universal/Script/DrawioViewerScript/use-viewer-min-js-url.spec.ts
  61. 0 0
      apps/app/src/components-universal/Script/DrawioViewerScript/use-viewer-min-js-url.ts
  62. 0 0
      apps/app/src/components-universal/ShareLinkPageView/ShareLinkAlert.tsx
  63. 12 15
      apps/app/src/components-universal/ShareLinkPageView/ShareLinkPageView.tsx
  64. 1 0
      apps/app/src/components-universal/ShareLinkPageView/index.ts
  65. 0 0
      apps/app/src/components-universal/User/UserDate.jsx
  66. 0 0
      apps/app/src/components-universal/User/UserInfo.module.scss
  67. 0 0
      apps/app/src/components-universal/User/UserInfo.tsx
  68. 0 0
      apps/app/src/components-universal/User/Username.tsx
  69. 5 0
      apps/app/src/components/.eslintrc.js
  70. 1 1
      apps/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx
  71. 1 1
      apps/app/src/components/Admin/Customize/CustomizeSidebarSetting.tsx
  72. 54 48
      apps/app/src/components/Bookmarks/BookmarkFolderTree.tsx
  73. 1 1
      apps/app/src/components/Comments.tsx
  74. 0 174
      apps/app/src/components/Common/PagePathNav/PagePathNav.tsx
  75. 0 1
      apps/app/src/components/Common/PagePathNav/index.ts
  76. 1 3
      apps/app/src/components/CompleteUserRegistrationForm.tsx
  77. 1 1
      apps/app/src/components/Hotkeys/Subscribers/EditPage.jsx
  78. 1 1
      apps/app/src/components/InvitedForm.tsx
  79. 0 80
      apps/app/src/components/Layout/BasicLayout.tsx
  80. 0 36
      apps/app/src/components/Layout/ShareLinkLayout.tsx
  81. 59 0
      apps/app/src/components/Maintenance/Maintenance.tsx
  82. 1 0
      apps/app/src/components/Maintenance/index.ts
  83. 1 1
      apps/app/src/components/Me/ColorModeSettings.tsx
  84. 4 4
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  85. 1 2
      apps/app/src/components/Navbar/GrowiNavbarBottom.tsx
  86. 2 1
      apps/app/src/components/Navbar/PageEditorModeManager.tsx
  87. 8 24
      apps/app/src/components/Page/DisplaySwitcher.tsx
  88. 11 0
      apps/app/src/components/Page/EditablePageEffects.tsx
  89. 1 1
      apps/app/src/components/Page/RevisionLoader.tsx
  90. 1 2
      apps/app/src/components/PageAttachment/DeleteAttachmentModal.tsx
  91. 2 2
      apps/app/src/components/PageComment/Comment.tsx
  92. 5 4
      apps/app/src/components/PageComment/CommentEditor.tsx
  93. 1 1
      apps/app/src/components/PageComment/CommentPreview.tsx
  94. 1 1
      apps/app/src/components/PageComment/DeleteCommentModal.tsx
  95. 4 1
      apps/app/src/components/PageControls/PageControls.tsx
  96. 3 3
      apps/app/src/components/PageEditor/ConflictDiffModal.tsx
  97. 18 3
      apps/app/src/components/PageEditor/DrawioModal.tsx
  98. 2 1
      apps/app/src/components/PageEditor/HandsontableModal.tsx
  99. 1 1
      apps/app/src/components/PageEditor/LinkEditModal.tsx
  100. 7 10
      apps/app/src/components/PageEditor/PageEditor.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 => {

+ 14 - 0
apps/app/src/components-universal/.eslintrc.js

@@ -0,0 +1,14 @@
+module.exports = {
+  extends: '../../.eslintrc.js',
+  rules: {
+    // restrict importing from client/ and components/ directories
+    'no-restricted-imports': ['error', {
+      patterns: [
+        '~/client/',
+        'client/',
+        '~/components/',
+        'components/',
+      ],
+    }],
+  },
+};

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


+ 1 - 1
apps/app/src/components/Admin/Common/AdminNavigation.tsx → 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/Icons/GrowiLogo.jsx → apps/app/src/components-universal/Common/GrowiLogo.jsx


+ 0 - 0
apps/app/src/components/Icons/GrowiLogo.module.scss → apps/app/src/components-universal/Common/GrowiLogo.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';

+ 0 - 0
apps/app/src/components/FontFamily/GlobalFonts.tsx → apps/app/src/components-universal/FontFamily/GlobalFonts.tsx


+ 0 - 0
apps/app/src/components/FontFamily/types.d.ts → apps/app/src/components-universal/FontFamily/types.d.ts


+ 0 - 0
apps/app/src/components/FontFamily/use-growi-custom-icons.tsx → apps/app/src/components-universal/FontFamily/use-growi-custom-icons.tsx


+ 0 - 0
apps/app/src/components/FontFamily/use-lato.tsx → apps/app/src/components-universal/FontFamily/use-lato.tsx


+ 0 - 0
apps/app/src/components/FontFamily/use-material-symbols-outlined.tsx → apps/app/src/components-universal/FontFamily/use-material-symbols-outlined.tsx


+ 0 - 0
apps/app/src/components/FontFamily/use-source-han-code-jp.tsx → apps/app/src/components-universal/FontFamily/use-source-han-code-jp.tsx


+ 0 - 0
apps/app/src/components/Layout/Admin.module.scss → apps/app/src/components-universal/Layout/Admin.module.scss


+ 6 - 4
apps/app/src/components/Layout/AdminLayout.tsx → apps/app/src/components-universal/Layout/AdminLayout.tsx

@@ -4,17 +4,19 @@ import React from 'react';
 import dynamic from 'next/dynamic';
 import Link from 'next/link';
 
+import GrowiLogo from '~/components-universal/Common/GrowiLogo';
+
 import { AdminNavigation } from '../Admin/Common/AdminNavigation';
-import GrowiLogo from '../Icons/GrowiLogo';
 
 import { RawLayout } from './RawLayout';
 
+
 import styles from './Admin.module.scss';
 
 
-const PageCreateModal = dynamic(() => import('../PageCreateModal'), { ssr: false });
-const SystemVersion = dynamic(() => import('../SystemVersion'), { ssr: false });
-const HotkeysManager = dynamic(() => import('../Hotkeys/HotkeysManager'), { ssr: false });
+const PageCreateModal = dynamic(() => import('~/components/PageCreateModal'), { ssr: false });
+const SystemVersion = dynamic(() => import('~/components/SystemVersion'), { ssr: false });
+const HotkeysManager = dynamic(() => import('~/components/Hotkeys/HotkeysManager'), { ssr: false });
 
 
 type Props = {

+ 0 - 0
apps/app/src/components/Layout/BasicLayout.module.scss → apps/app/src/components-universal/Layout/BasicLayout.module.scss


+ 79 - 0
apps/app/src/components-universal/Layout/BasicLayout.tsx

@@ -0,0 +1,79 @@
+import type { ReactNode } from 'react';
+import React from 'react';
+
+import dynamic from 'next/dynamic';
+
+import { RawLayout } from './RawLayout';
+
+
+import styles from './BasicLayout.module.scss';
+
+const moduleClass = styles['grw-basic-layout'] ?? '';
+
+
+const Sidebar = dynamic(() => import('~/components/Sidebar').then(mod => mod.Sidebar), { ssr: false });
+
+const AlertSiteUrlUndefined = dynamic(() => import('~/components/AlertSiteUrlUndefined').then(mod => mod.AlertSiteUrlUndefined), { ssr: false });
+const DeleteAttachmentModal = dynamic(
+  () => import('~/components/PageAttachment/DeleteAttachmentModal').then(mod => mod.DeleteAttachmentModal), { ssr: false },
+);
+const HotkeysManager = dynamic(() => import('~/components/Hotkeys/HotkeysManager'), { ssr: false });
+const GrowiNavbarBottom = dynamic(() => import('~/components/Navbar/GrowiNavbarBottom').then(mod => mod.GrowiNavbarBottom), { ssr: false });
+const ShortcutsModal = dynamic(() => import('~/components/ShortcutsModal'), { ssr: false });
+const SystemVersion = dynamic(() => import('~/components/SystemVersion'), { ssr: false });
+const PutbackPageModal = dynamic(() => import('~/components/PutbackPageModal'), { ssr: false });
+// Page modals
+const PageCreateModal = dynamic(() => import('~/components/PageCreateModal'), { ssr: false });
+const PageDuplicateModal = dynamic(() => import('~/components/PageDuplicateModal'), { ssr: false });
+const PageDeleteModal = dynamic(() => import('~/components/PageDeleteModal'), { ssr: false });
+const PageRenameModal = dynamic(() => import('~/components/PageRenameModal'), { ssr: false });
+const PagePresentationModal = dynamic(() => import('~/components/PagePresentationModal'), { ssr: false });
+const PageAccessoriesModal = dynamic(() => import('~/components/PageAccessoriesModal').then(mod => mod.PageAccessoriesModal), { ssr: false });
+const GrantedGroupsInheritanceSelectModal = dynamic(() => import('~/components/GrantedGroupsInheritanceSelectModal'), { ssr: false });
+const DeleteBookmarkFolderModal = dynamic(
+  () => import('~/components/DeleteBookmarkFolderModal').then(mod => mod.DeleteBookmarkFolderModal), { ssr: false },
+);
+const SearchModal = dynamic(() => import('../../features/search/client/components/SearchModal'), { ssr: false });
+
+
+type Props = {
+  children?: ReactNode
+  className?: string
+}
+
+
+export const BasicLayout = ({ children, className }: Props): JSX.Element => {
+  return (
+    <RawLayout className={`${moduleClass} ${className ?? ''}`}>
+      <div className="page-wrapper flex-row">
+        <div className="z-2">
+          <Sidebar />
+        </div>
+
+        <div className="d-flex flex-grow-1 flex-column mw-0 z-1">{/* neccessary for nested {children} make expanded */}
+          <AlertSiteUrlUndefined />
+          {children}
+        </div>
+      </div>
+
+      <GrowiNavbarBottom />
+
+      <PageCreateModal />
+      <PageDuplicateModal />
+      <PageDeleteModal />
+      <PageRenameModal />
+      <PageAccessoriesModal />
+      <DeleteAttachmentModal />
+      <DeleteBookmarkFolderModal />
+      <PutbackPageModal />
+      <SearchModal />
+
+      <PagePresentationModal />
+      <HotkeysManager />
+
+      <ShortcutsModal />
+      <GrantedGroupsInheritanceSelectModal />
+      <SystemVersion showShortcutsButton />
+    </RawLayout>
+  );
+};

+ 0 - 0
apps/app/src/components/Layout/NoLoginLayout.module.scss → apps/app/src/components-universal/Layout/NoLoginLayout.module.scss


+ 3 - 2
apps/app/src/components/Layout/NoLoginLayout.tsx → apps/app/src/components-universal/Layout/NoLoginLayout.tsx

@@ -1,12 +1,13 @@
 import type { ReactNode } from 'react';
 import React from 'react';
 
-import { useAppTitle } from '~/stores/context';
+import { useAppTitle } from '~/stores-universal/context';
 
-import GrowiLogo from '../Icons/GrowiLogo';
+import GrowiLogo from '../Common/GrowiLogo';
 
 import { RawLayout } from './RawLayout';
 
+
 import commonStyles from './NoLoginLayout.module.scss';
 
 type Props = {

+ 0 - 0
apps/app/src/components/Layout/RawLayout.module.scss → apps/app/src/components-universal/Layout/RawLayout.module.scss


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

@@ -6,7 +6,7 @@ import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 
-import { useNextThemes, NextThemesProvider } from '~/stores/use-next-themes';
+import { useNextThemes, NextThemesProvider } from '~/stores-universal/use-next-themes';
 import loggerFactory from '~/utils/logger';
 
 import styles from './RawLayout.module.scss';

+ 0 - 0
apps/app/src/components/Layout/SearchResultLayout.module.scss → apps/app/src/components-universal/Layout/SearchResultLayout.module.scss


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

@@ -1,6 +1,6 @@
 import React, { type ReactNode } from 'react';
 
-import { BasicLayout } from '~/components/Layout/BasicLayout';
+import { BasicLayout } from './BasicLayout';
 
 import commonStyles from './SearchResultLayout.module.scss';
 

+ 33 - 0
apps/app/src/components-universal/Layout/ShareLinkLayout.tsx

@@ -0,0 +1,33 @@
+import type { ReactNode } from 'react';
+import React from 'react';
+
+import dynamic from 'next/dynamic';
+
+import { RawLayout } from './RawLayout';
+
+const PageCreateModal = dynamic(() => import('~/components/PageCreateModal'), { ssr: false });
+const GrowiNavbarBottom = dynamic(() => import('~/components/Navbar/GrowiNavbarBottom').then(mod => mod.GrowiNavbarBottom), { ssr: false });
+const ShortcutsModal = dynamic(() => import('~/components/ShortcutsModal'), { ssr: false });
+const SystemVersion = dynamic(() => import('~/components/SystemVersion'), { ssr: false });
+
+
+type Props = {
+  children?: ReactNode
+}
+
+export const ShareLinkLayout = ({ children }: Props): JSX.Element => {
+  return (
+    <RawLayout>
+
+      <div className="page-wrapper">
+        {children}
+      </div>
+
+      <GrowiNavbarBottom />
+
+      <ShortcutsModal />
+      <PageCreateModal />
+      <SystemVersion showShortcutsButton />
+    </RawLayout>
+  );
+};

+ 0 - 0
apps/app/src/components/Navbar/GroundGlassBar.module.scss → apps/app/src/components-universal/Navbar/GroundGlassBar.module.scss


+ 0 - 0
apps/app/src/components/Navbar/GroundGlassBar.tsx → apps/app/src/components-universal/Navbar/GroundGlassBar.tsx


+ 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/Script/DrawioViewerScript/DrawioViewerScript.tsx → apps/app/src/components-universal/Script/DrawioViewerScript/DrawioViewerScript.tsx


+ 0 - 0
apps/app/src/components/Script/DrawioViewerScript/index.ts → apps/app/src/components-universal/Script/DrawioViewerScript/index.ts


+ 0 - 0
apps/app/src/components/Script/DrawioViewerScript/use-viewer-min-js-url.spec.ts → apps/app/src/components-universal/Script/DrawioViewerScript/use-viewer-min-js-url.spec.ts


+ 0 - 0
apps/app/src/components/Script/DrawioViewerScript/use-viewer-min-js-url.ts → apps/app/src/components-universal/Script/DrawioViewerScript/use-viewer-min-js-url.ts


+ 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


+ 5 - 0
apps/app/src/components/.eslintrc.js

@@ -0,0 +1,5 @@
+module.exports = {
+  extends: '../../.eslintrc.js',
+  rules: {
+  },
+};

+ 1 - 1
apps/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx

@@ -6,8 +6,8 @@ import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 
 import { toastSuccess, toastError } from '~/client/util/toastr';
+import { useNextThemes } from '~/stores-universal/use-next-themes';
 import { useSWRxLayoutSetting } from '~/stores/admin/customize';
-import { useNextThemes } from '~/stores/use-next-themes';
 
 const useIsContainerFluid = () => {
   const { data: layoutSetting, update: updateLayoutSetting } = useSWRxLayoutSetting();

+ 1 - 1
apps/app/src/components/Admin/Customize/CustomizeSidebarSetting.tsx

@@ -5,8 +5,8 @@ import { useTranslation } from 'next-i18next';
 import { Card, CardBody } from 'reactstrap';
 
 import { toastSuccess, toastError } from '~/client/util/toastr';
+import { useNextThemes } from '~/stores-universal/use-next-themes';
 import { useSWRxSidebarConfig } from '~/stores/admin/sidebar-config';
-import { useNextThemes } from '~/stores/use-next-themes';
 
 const CustomizeSidebarsetting = (): JSX.Element => {
   const { t } = useTranslation(['admin', 'commons']);

+ 54 - 48
apps/app/src/components/Bookmarks/BookmarkFolderTree.tsx

@@ -4,6 +4,8 @@ import React, { useCallback } from 'react';
 import type { IPageToDeleteWithMeta } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
+import { DndProvider } from 'react-dnd';
+import { HTML5Backend } from 'react-dnd-html5-backend';
 
 import { toastSuccess } from '~/client/util/toastr';
 import type { OnDeletedFunction } from '~/interfaces/ui';
@@ -102,54 +104,58 @@ export const BookmarkFolderTree: React.FC<Props> = (props: Props) => {
   // };
 
   return (
-    <div className={`grw-folder-tree-container ${styles['grw-folder-tree-container']}`}>
-      <ul className={`grw-foldertree ${styles['grw-foldertree']} list-group py-2`}>
-        {bookmarkFolders?.map((bookmarkFolder) => {
-          return (
-            <BookmarkFolderItem
-              key={bookmarkFolder._id}
-              isReadOnlyUser={!!isReadOnlyUser}
-              isOperable={props.isOperable}
-              bookmarkFolder={bookmarkFolder}
-              isOpen={false}
-              level={0}
-              root={bookmarkFolder._id}
-              isUserHomepage={isUserHomepage}
-              onClickDeleteMenuItemHandler={onClickDeleteMenuItemHandler}
-              bookmarkFolderTreeMutation={bookmarkFolderTreeMutation}
-            />
-          );
-        })}
-        {userBookmarks?.map(userBookmark => (
-          <div key={userBookmark?._id} className="grw-foldertree-item-container grw-root-bookmarks">
-            <BookmarkItem
-              isReadOnlyUser={!!isReadOnlyUser}
-              isOperable={props.isOperable}
-              bookmarkedPage={userBookmark}
-              level={0}
-              parentFolder={null}
-              canMoveToRoot={false}
-              onClickDeleteMenuItemHandler={onClickDeleteMenuItemHandler}
-              bookmarkFolderTreeMutation={bookmarkFolderTreeMutation}
-            />
-          </div>
-        ))}
-      </ul>
-      {/* TODO: update in bookmarks folder v2. Also delete drop_item_here in translation.json, if don't need it. */}
-      {/* {bookmarkFolderData != null && bookmarkFolderData.length > 0 && (
-        <DragAndDropWrapper
-          useDropMode={true}
-          type={acceptedTypes}
-          onDropItem={itemDropHandler}
-          isDropable={isDroppable}
-        >
-          <div className="grw-drop-item-area">
-            <div className="d-flex flex-column align-items-center">
-              {t('bookmark_folder.drop_item_here')}
+    <DndProvider backend={HTML5Backend}>
+
+      <div className={`grw-folder-tree-container ${styles['grw-folder-tree-container']}`}>
+        <ul className={`grw-foldertree ${styles['grw-foldertree']} list-group py-2`}>
+          {bookmarkFolders?.map((bookmarkFolder) => {
+            return (
+              <BookmarkFolderItem
+                key={bookmarkFolder._id}
+                isReadOnlyUser={!!isReadOnlyUser}
+                isOperable={props.isOperable}
+                bookmarkFolder={bookmarkFolder}
+                isOpen={false}
+                level={0}
+                root={bookmarkFolder._id}
+                isUserHomepage={isUserHomepage}
+                onClickDeleteMenuItemHandler={onClickDeleteMenuItemHandler}
+                bookmarkFolderTreeMutation={bookmarkFolderTreeMutation}
+              />
+            );
+          })}
+          {userBookmarks?.map(userBookmark => (
+            <div key={userBookmark?._id} className="grw-foldertree-item-container grw-root-bookmarks">
+              <BookmarkItem
+                isReadOnlyUser={!!isReadOnlyUser}
+                isOperable={props.isOperable}
+                bookmarkedPage={userBookmark}
+                level={0}
+                parentFolder={null}
+                canMoveToRoot={false}
+                onClickDeleteMenuItemHandler={onClickDeleteMenuItemHandler}
+                bookmarkFolderTreeMutation={bookmarkFolderTreeMutation}
+              />
             </div>
-          </div>
-        </DragAndDropWrapper>
-      )} */}
-    </div>
+          ))}
+        </ul>
+        {/* TODO: update in bookmarks folder v2. Also delete drop_item_here in translation.json, if don't need it. */}
+        {/* {bookmarkFolderData != null && bookmarkFolderData.length > 0 && (
+          <DragAndDropWrapper
+            useDropMode={true}
+            type={acceptedTypes}
+            onDropItem={itemDropHandler}
+            isDropable={isDroppable}
+          >
+            <div className="grw-drop-item-area">
+              <div className="d-flex flex-column align-items-center">
+                {t('bookmark_folder.drop_item_here')}
+              </div>
+            </div>
+          </DragAndDropWrapper>
+        )} */}
+      </div>
+
+    </DndProvider>
   );
 };

+ 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 - 3
apps/app/src/components/CompleteUserRegistrationForm.tsx

@@ -25,7 +25,7 @@ interface Props {
   isEmailAuthenticationEnabled: boolean,
 }
 
-const CompleteUserRegistrationForm: React.FC<Props> = (props: Props) => {
+export const CompleteUserRegistrationForm: React.FC<Props> = (props: Props) => {
 
   const { t } = useTranslation();
   const {
@@ -206,5 +206,3 @@ const CompleteUserRegistrationForm: React.FC<Props> = (props: Props) => {
   );
 
 };
-
-export default CompleteUserRegistrationForm;

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

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

@@ -9,7 +9,7 @@ import { apiv3Post } from '~/client/util/apiv3-client';
 import { useCurrentUser } from '../stores/context';
 
 
-export type InvitedFormProps = {
+type InvitedFormProps = {
   invitedFormUsername: string,
   invitedFormName: string,
 }

+ 0 - 80
apps/app/src/components/Layout/BasicLayout.tsx

@@ -1,80 +0,0 @@
-import type { ReactNode } from 'react';
-import React from 'react';
-
-import dynamic from 'next/dynamic';
-import { DndProvider } from 'react-dnd';
-import { HTML5Backend } from 'react-dnd-html5-backend';
-
-import { Sidebar } from '../Sidebar';
-
-import { RawLayout } from './RawLayout';
-
-
-import styles from './BasicLayout.module.scss';
-
-const moduleClass = styles['grw-basic-layout'] ?? '';
-
-
-const AlertSiteUrlUndefined = dynamic(() => import('../AlertSiteUrlUndefined').then(mod => mod.AlertSiteUrlUndefined), { ssr: false });
-const DeleteAttachmentModal = dynamic(() => import('../PageAttachment/DeleteAttachmentModal').then(mod => mod.DeleteAttachmentModal), { ssr: false });
-const HotkeysManager = dynamic(() => import('../Hotkeys/HotkeysManager'), { ssr: false });
-const GrowiNavbarBottom = dynamic(() => import('../Navbar/GrowiNavbarBottom').then(mod => mod.GrowiNavbarBottom), { ssr: false });
-const ShortcutsModal = dynamic(() => import('../ShortcutsModal'), { ssr: false });
-const SystemVersion = dynamic(() => import('../SystemVersion'), { ssr: false });
-const PutbackPageModal = dynamic(() => import('../PutbackPageModal'), { ssr: false });
-// Page modals
-const PageCreateModal = dynamic(() => import('../PageCreateModal'), { ssr: false });
-const PageDuplicateModal = dynamic(() => import('../PageDuplicateModal'), { ssr: false });
-const PageDeleteModal = dynamic(() => import('../PageDeleteModal'), { ssr: false });
-const PageRenameModal = dynamic(() => import('../PageRenameModal'), { ssr: false });
-const PagePresentationModal = dynamic(() => import('../PagePresentationModal'), { ssr: false });
-const PageAccessoriesModal = dynamic(() => import('../PageAccessoriesModal').then(mod => mod.PageAccessoriesModal), { ssr: false });
-const GrantedGroupsInheritanceSelectModal = dynamic(() => import('../GrantedGroupsInheritanceSelectModal'), { ssr: false });
-const DeleteBookmarkFolderModal = dynamic(() => import('../DeleteBookmarkFolderModal').then(mod => mod.DeleteBookmarkFolderModal), { ssr: false });
-const SearchModal = dynamic(() => import('../../features/search/client/components/SearchModal'), { ssr: false });
-
-
-type Props = {
-  children?: ReactNode
-  className?: string
-}
-
-
-export const BasicLayout = ({ children, className }: Props): JSX.Element => {
-  return (
-    <RawLayout className={`${moduleClass} ${className ?? ''}`}>
-      <DndProvider backend={HTML5Backend}>
-
-        <div className="page-wrapper flex-row">
-          <div className="z-2">
-            <Sidebar />
-          </div>
-
-          <div className="d-flex flex-grow-1 flex-column mw-0 z-1">{/* neccessary for nested {children} make expanded */}
-            <AlertSiteUrlUndefined />
-            {children}
-          </div>
-        </div>
-
-        <GrowiNavbarBottom />
-
-        <PageCreateModal />
-        <PageDuplicateModal />
-        <PageDeleteModal />
-        <PageRenameModal />
-        <PageAccessoriesModal />
-        <DeleteAttachmentModal />
-        <DeleteBookmarkFolderModal />
-        <PutbackPageModal />
-        <SearchModal />
-      </DndProvider>
-
-      <PagePresentationModal />
-      <HotkeysManager />
-
-      <ShortcutsModal />
-      <GrantedGroupsInheritanceSelectModal />
-      <SystemVersion showShortcutsButton />
-    </RawLayout>
-  );
-};

+ 0 - 36
apps/app/src/components/Layout/ShareLinkLayout.tsx

@@ -1,36 +0,0 @@
-import React, { ReactNode } from 'react';
-
-import dynamic from 'next/dynamic';
-
-import { useEditorModeClassName } from '../../client/services/layout';
-
-import { RawLayout } from './RawLayout';
-
-const PageCreateModal = dynamic(() => import('../PageCreateModal'), { ssr: false });
-const GrowiNavbarBottom = dynamic(() => import('../Navbar/GrowiNavbarBottom').then(mod => mod.GrowiNavbarBottom), { ssr: false });
-const ShortcutsModal = dynamic(() => import('../ShortcutsModal'), { ssr: false });
-const SystemVersion = dynamic(() => import('../SystemVersion'), { ssr: false });
-
-
-type Props = {
-  children?: ReactNode
-}
-
-export const ShareLinkLayout = ({ children }: Props): JSX.Element => {
-  const className = useEditorModeClassName();
-
-  return (
-    <RawLayout className={className}>
-
-      <div className="page-wrapper">
-        {children}
-      </div>
-
-      <GrowiNavbarBottom />
-
-      <ShortcutsModal />
-      <PageCreateModal />
-      <SystemVersion showShortcutsButton />
-    </RawLayout>
-  );
-};

+ 59 - 0
apps/app/src/components/Maintenance/Maintenance.tsx

@@ -0,0 +1,59 @@
+import type { IUserHasId } from '@growi/core';
+import { useTranslation } from 'next-i18next';
+
+import { apiv3Post } from '~/client/util/apiv3-client';
+import { toastError } from '~/client/util/toastr';
+import { useCurrentUser } from '~/stores/context';
+
+
+type Props = {
+  currentUser: IUserHasId,
+};
+
+export const Maintenance = (props: Props): JSX.Element => {
+  const { t } = useTranslation();
+
+  useCurrentUser(props.currentUser ?? null);
+
+  const logoutHandler = async() => {
+    try {
+      await apiv3Post('/logout');
+      window.location.reload();
+    }
+    catch (err) {
+      toastError(err);
+    }
+  };
+
+  return (
+    <div className="text-center">
+      <h1><span className="material-symbols-outlined large">error</span></h1>
+      <h1 className="text-center">{ t('maintenance_mode.maintenance_mode') }</h1>
+      <h3>{ t('maintenance_mode.growi_is_under_maintenance') }</h3>
+      <hr />
+      <div className="text-start">
+        {props.currentUser?.admin
+              && (
+                <p>
+                  <span className="material-symbols-outlined">arrow_circle_right</span>
+                  <a className="btn btn-link" href="/admin">{ t('maintenance_mode.admin_page') }</a>
+                </p>
+              )}
+        {props.currentUser != null
+          ? (
+            <p>
+              <span className="material-symbols-outlined">arrow_circle_right</span>
+              <a className="btn btn-link" onClick={logoutHandler} id="maintanounse-mode-logout">{ t('maintenance_mode.logout') }</a>
+            </p>
+          )
+          : (
+            <p>
+              <span className="material-symbols-outlined">arrow_circle_right</span>
+              <a className="btn btn-link" href="/login">{ t('maintenance_mode.login') }</a>
+            </p>
+          )
+        }
+      </div>
+    </div>
+  );
+};

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

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

+ 1 - 1
apps/app/src/components/Me/ColorModeSettings.tsx

@@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
 
 import { useTranslation } from 'react-i18next';
 
-import { Themes, useNextThemes } from '~/stores/use-next-themes';
+import { Themes, useNextThemes } from '~/stores-universal/use-next-themes';
 
 
 type ColorModeSettingsButtonProps = {

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

@@ -14,9 +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,
@@ -30,7 +32,7 @@ import {
 } from '~/stores/page';
 import { mutatePageTree } from '~/stores/page-listing';
 import {
-  useEditorMode, useIsAbleToShowPageManagement,
+  useIsAbleToShowPageManagement,
   useIsAbleToChangeEditorMode,
   useIsDeviceLargerThanMd,
 } from '~/stores/ui';
@@ -38,8 +40,6 @@ import {
 import { NotAvailable } from '../NotAvailable';
 import { Skeleton } from '../Skeleton';
 
-import { GroundGlassBar } from './GroundGlassBar';
-
 import styles from './GrowiContextualSubNavigation.module.scss';
 import PageEditorModeManagerStyles from './PageEditorModeManager.module.scss';
 

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

@@ -2,14 +2,13 @@ import React, {
   useCallback,
 } from 'react';
 
+import { GroundGlassBar } from '~/components-universal/Navbar/GroundGlassBar';
 import { useSearchModal } from '~/features/search/client/stores/search';
 import { useIsSearchPage } from '~/stores/context';
 import { usePageCreateModal } from '~/stores/modal';
 import { useCurrentPagePath } from '~/stores/page';
 import { useDrawerOpened } from '~/stores/ui';
 
-import { GroundGlassBar } from './GroundGlassBar';
-
 import styles from './GrowiNavbarBottom.module.scss';
 
 

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

+ 5 - 4
apps/app/src/components/PageComment/CommentEditor.tsx

@@ -5,9 +5,9 @@ import React, {
 } from 'react';
 
 import { GlobalCodeMirrorEditorKey } from '@growi/editor';
-import {
-  CodeMirrorEditorComment, useCodeMirrorEditorIsolated, useResolvedThemeForEditor,
-} from '@growi/editor/dist/client';
+import { CodeMirrorEditorComment } from '@growi/editor/dist/client/components/CodeMirrorEditorComment';
+import { useCodeMirrorEditorIsolated } from '@growi/editor/dist/client/stores/codemirror-editor';
+import { useResolvedThemeForEditor } from '@growi/editor/dist/client/stores/use-resolved-theme';
 import { UserPicture } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
@@ -15,8 +15,10 @@ import {
   TabContent, TabPane,
 } from 'reactstrap';
 
+
 import { uploadAttachments } from '~/client/services/upload-attachments';
 import { toastError } from '~/client/util/toastr';
+import { useNextThemes } from '~/stores-universal/use-next-themes';
 import { useSWRxPageComment } from '~/stores/comment';
 import {
   useCurrentUser, useIsSlackConfigured, useAcceptedUploadFileType,
@@ -26,7 +28,6 @@ import {
 } from '~/stores/editor';
 import { useCurrentPagePath } from '~/stores/page';
 import { useCommentEditorDirtyMap } from '~/stores/ui';
-import { useNextThemes } from '~/stores/use-next-themes';
 import loggerFactory from '~/utils/logger';
 
 import { NotAvailableForGuest } from '../NotAvailableForGuest';

+ 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 - 3
apps/app/src/components/PageEditor/ConflictDiffModal.tsx

@@ -4,9 +4,9 @@ import React, {
 
 import type { IUser } from '@growi/core';
 import { GlobalCodeMirrorEditorKey } from '@growi/editor';
-import {
-  MergeViewer, CodeMirrorEditorDiff, useCodeMirrorEditorIsolated,
-} from '@growi/editor/dist/client';
+import { CodeMirrorEditorDiff } from '@growi/editor/dist/client/components/diff/CodeMirrorEditorDiff';
+import { MergeViewer } from '@growi/editor/dist/client/components/diff/MergeViewer';
+import { useCodeMirrorEditorIsolated } from '@growi/editor/dist/client/stores/codemirror-editor';
 import { UserPicture } from '@growi/ui/dist/components';
 import { format } from 'date-fns/format';
 import { useTranslation } from 'next-i18next';

+ 18 - 3
apps/app/src/components/PageEditor/DrawioModal.tsx

@@ -4,14 +4,15 @@ import React, {
   useMemo,
 } from 'react';
 
-import { useCodeMirrorEditorIsolated, useDrawioModalForEditor } from '@growi/editor/dist/client';
+import { Lang } from '@growi/core';
+import { useCodeMirrorEditorIsolated } from '@growi/editor/dist/client/stores/codemirror-editor';
+import { useDrawioModalForEditor } from '@growi/editor/dist/client/stores/use-drawio';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import {
   Modal,
   ModalBody,
 } from 'reactstrap';
 
-import { getDiagramsNetLangCode } from '~/client/util/locale-utils';
 import { replaceFocusedDrawioWithEditor, getMarkdownDrawioMxfile } from '~/components/PageEditor/markdown-drawio-util-for-editor';
 import { useRendererConfig } from '~/stores/context';
 import { useDrawioModal } from '~/stores/modal';
@@ -23,6 +24,20 @@ import { type DrawioConfig, DrawioCommunicationHelper } from './DrawioCommunicat
 
 const logger = loggerFactory('growi:components:DrawioModal');
 
+
+// https://docs.google.com/spreadsheets/d/1FoYdyEraEQuWofzbYCDPKN7EdKgS_2ZrsDrOA8scgwQ
+const DIAGRAMS_NET_LANG_MAP = {
+  en_US: 'en',
+  ja_JP: 'ja',
+  zh_CN: 'zh',
+  fr_FR: 'fr',
+};
+
+export const getDiagramsNetLangCode = (lang: Lang): string => {
+  return DIAGRAMS_NET_LANG_MAP[lang];
+};
+
+
 const headerColor = '#334455';
 const fontFamily = "-apple-system, BlinkMacSystemFont, 'Hiragino Kaku Gothic ProN', Meiryo, sans-serif";
 
@@ -75,7 +90,7 @@ export const DrawioModal = (): JSX.Element => {
     // refs: https://desk.draw.io/support/solutions/articles/16000042546-what-url-parameters-are-supported-
     url.searchParams.append('spin', '1');
     url.searchParams.append('embed', '1');
-    url.searchParams.append('lang', getDiagramsNetLangCode(personalSettingsInfo?.lang || 'en'));
+    url.searchParams.append('lang', getDiagramsNetLangCode(personalSettingsInfo?.lang ?? Lang.en_US));
     url.searchParams.append('ui', 'atlas');
     url.searchParams.append('configure', '1');
 

+ 2 - 1
apps/app/src/components/PageEditor/HandsontableModal.tsx

@@ -1,7 +1,7 @@
 import React, { useState } from 'react';
 
 import { MarkdownTable } from '@growi/editor';
-import { useHandsontableModalForEditor } from '@growi/editor/dist/client';
+import { useHandsontableModalForEditor } from '@growi/editor/dist/client/stores/use-handsontable';
 import { HotTable } from '@handsontable/react';
 import type Handsontable from 'handsontable';
 import { useTranslation } from 'next-i18next';
@@ -11,6 +11,7 @@ import {
 } from 'reactstrap';
 import { debounce } from 'throttle-debounce';
 
+
 import { replaceFocusedMarkdownTableWithEditor, getMarkdownTable } from '~/components/PageEditor/markdown-table-util-for-editor';
 import { useHandsontableModal } from '~/stores/modal';
 

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

@@ -3,7 +3,7 @@ import React, { useEffect, useState, useCallback } from 'react';
 import path from 'path';
 
 import { Linker } from '@growi/editor';
-import { useLinkEditModal } from '@growi/editor/dist/client';
+import { useLinkEditModal } from '@growi/editor/dist/client/stores/use-link-edit-modal';
 import { useTranslation } from 'next-i18next';
 import {
   Modal,

+ 7 - 10
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -10,20 +10,21 @@ import nodePath from 'path';
 import { type IPageHasId, Origin } from '@growi/core';
 import { pathUtils } from '@growi/core/dist/utils';
 import { GlobalCodeMirrorEditorKey } from '@growi/editor';
-import {
-  CodeMirrorEditorMain,
-  useCodeMirrorEditorIsolated, useResolvedThemeForEditor,
-} from '@growi/editor/dist/client';
+import { CodeMirrorEditorMain } from '@growi/editor/dist/client/components/CodeMirrorEditorMain';
+import { useCodeMirrorEditorIsolated } from '@growi/editor/dist/client/stores/codemirror-editor';
+import { useResolvedThemeForEditor } from '@growi/editor/dist/client/stores/use-resolved-theme';
 import { useRect } from '@growi/ui/dist/utils';
 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,
   useCurrentPathname, useIsEnabledAttachTitleHeader,
@@ -41,12 +42,8 @@ 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 { useNextThemes } from '~/stores/use-next-themes';
 import loggerFactory from '~/utils/logger';
 
 import { EditorNavbar } from './EditorNavbar';

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio