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

configure biome for app components dir

Futa Arai 6 месяцев назад
Родитель
Сommit
ec2e84e1eb
37 измененных файлов с 1408 добавлено и 801 удалено
  1. 272 94
      apps/app/src/components/Admin/Common/AdminNavigation.tsx
  2. 95 72
      apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.tsx
  3. 23 12
      apps/app/src/components/Common/PagePathNav/PagePathNav.tsx
  4. 30 26
      apps/app/src/components/Common/PagePathNav/PagePathNavLayout.tsx
  5. 8 3
      apps/app/src/components/Common/PagePathNav/Separator.tsx
  6. 36 22
      apps/app/src/components/Common/PagePathNavTitle/PagePathNavTitle.tsx
  7. 1 1
      apps/app/src/components/FontFamily/GlobalFonts.tsx
  8. 27 26
      apps/app/src/components/Layout/AdminLayout.tsx
  9. 111 37
      apps/app/src/components/Layout/BasicLayout.tsx
  10. 15 19
      apps/app/src/components/Layout/NoLoginLayout.tsx
  11. 16 11
      apps/app/src/components/Layout/RawLayout.tsx
  12. 4 5
      apps/app/src/components/Layout/SearchResultLayout.tsx
  13. 24 14
      apps/app/src/components/Layout/ShareLinkLayout.tsx
  14. 4 1
      apps/app/src/components/Navbar/GroundGlassBar.tsx
  15. 193 114
      apps/app/src/components/PageView/PageAlerts/FixPageGrantAlert.tsx
  16. 12 6
      apps/app/src/components/PageView/PageAlerts/FullTextSearchNotCoverAlert.tsx
  17. 16 6
      apps/app/src/components/PageView/PageAlerts/OldRevisionAlert.tsx
  18. 21 9
      apps/app/src/components/PageView/PageAlerts/PageAlerts.tsx
  19. 10 10
      apps/app/src/components/PageView/PageAlerts/PageGrantAlert.tsx
  20. 15 9
      apps/app/src/components/PageView/PageAlerts/PageRedirectedAlert.tsx
  21. 10 10
      apps/app/src/components/PageView/PageAlerts/PageStaleAlert.tsx
  22. 57 19
      apps/app/src/components/PageView/PageAlerts/TrashPageAlert.tsx
  23. 10 10
      apps/app/src/components/PageView/PageAlerts/WipPageAlert.tsx
  24. 26 14
      apps/app/src/components/PageView/PageContentFooter.tsx
  25. 104 54
      apps/app/src/components/PageView/PageView.tsx
  26. 36 31
      apps/app/src/components/PageView/PageViewLayout.tsx
  27. 23 20
      apps/app/src/components/PageView/RevisionRenderer.tsx
  28. 45 25
      apps/app/src/components/ReactMarkdownComponents/CodeBlock.tsx
  29. 36 18
      apps/app/src/components/ReactMarkdownComponents/NextLink.tsx
  30. 2 3
      apps/app/src/components/Script/DrawioViewerScript/DrawioViewerScript.tsx
  31. 14 11
      apps/app/src/components/Script/DrawioViewerScript/use-viewer-min-js-url.spec.ts
  32. 18 15
      apps/app/src/components/ShareLinkPageView/ShareLinkAlert.tsx
  33. 69 44
      apps/app/src/components/ShareLinkPageView/ShareLinkPageView.tsx
  34. 1 5
      apps/app/src/components/User/UserDate.jsx
  35. 18 19
      apps/app/src/components/User/UserInfo.tsx
  36. 6 5
      apps/app/src/components/User/Username.tsx
  37. 0 1
      biome.json

+ 272 - 94
apps/app/src/components/Admin/Common/AdminNavigation.tsx

@@ -1,37 +1,122 @@
-import React, { useCallback, type JSX } from 'react';
-
 import { pathUtils } from '@growi/core/dist/utils';
-import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
+import { useTranslation } from 'next-i18next';
+import React, { type JSX, useCallback } from 'react';
 import urljoin from 'url-join';
 
-import { useGrowiCloudUri, useGrowiAppIdForGrowiCloud } from '~/stores-universal/context';
+import {
+  useGrowiAppIdForGrowiCloud,
+  useGrowiCloudUri,
+} from '~/stores-universal/context';
 
 import styles from './AdminNavigation.module.scss';
 
 const moduleClass = styles['admin-navigation'];
 
-
 // eslint-disable-next-line react/prop-types
 const MenuLabel = ({ menu }: { menu: string }) => {
   const { t } = useTranslation(['admin', 'commons']);
 
   switch (menu) {
     /* eslint-disable no-multi-spaces, max-len */
-    case 'app':                      return <><span className="material-symbols-outlined me-1">settings</span>{         t('headers.app_settings', { ns: 'commons' }) }</>;
-    case 'security':                 return <><span className="material-symbols-outlined me-1">shield</span>{           t('security_settings.security_settings') }</>;
-    case 'markdown':                 return <><span className="material-symbols-outlined me-1">note</span>{             t('markdown_settings.markdown_settings') }</>;
-    case 'customize':                return <><span className="material-symbols-outlined me-1">construction</span>{     t('customize_settings.customize_settings') }</>;
-    case 'importer':                 return <><span className="material-symbols-outlined me-1">cloud_upload</span>{     t('importer_management.import_data') }</>;
-    case 'export':                   return <><span className="material-symbols-outlined me-1">cloud_download</span>{   t('export_management.export_archive_data') }</>;
-    case 'data-transfer':            return <><span className="material-symbols-outlined me-1">flight</span>{           t('g2g_data_transfer.data_transfer', { ns: 'commons' })}</>;
-    case 'notification':             return <><span className="material-symbols-outlined me-1">notifications</span>{    t('external_notification.external_notification')}</>;
-    case 'slack-integration':        return <><span className="material-symbols-outlined me-1">shuffle</span>{          t('slack_integration.slack_integration') }</>;
-    case 'slack-integration-legacy': return <><span className="material-symbols-outlined me-1">shuffle</span>{          t('slack_integration_legacy.slack_integration_legacy')}</>;
-    case 'users':                    return <><span className="material-symbols-outlined me-1">person</span>{           t('user_management.user_management') }</>;
-    case 'user-groups':              return <><span className="material-symbols-outlined me-1">group</span>{            t('user_group_management.user_group_management') }</>;
-    case 'audit-log':                return <><span className="material-symbols-outlined me-1">feed</span>{             t('audit_log_management.audit_log')}</>;
-    case 'plugins':                  return <><span className="material-symbols-outlined me-1">extension</span>{        t('plugins.plugins')}</>;
+    case 'app':
+      return (
+        <>
+          <span className="material-symbols-outlined me-1">settings</span>
+          {t('headers.app_settings', { ns: 'commons' })}
+        </>
+      );
+    case 'security':
+      return (
+        <>
+          <span className="material-symbols-outlined me-1">shield</span>
+          {t('security_settings.security_settings')}
+        </>
+      );
+    case 'markdown':
+      return (
+        <>
+          <span className="material-symbols-outlined me-1">note</span>
+          {t('markdown_settings.markdown_settings')}
+        </>
+      );
+    case 'customize':
+      return (
+        <>
+          <span className="material-symbols-outlined me-1">construction</span>
+          {t('customize_settings.customize_settings')}
+        </>
+      );
+    case 'importer':
+      return (
+        <>
+          <span className="material-symbols-outlined me-1">cloud_upload</span>
+          {t('importer_management.import_data')}
+        </>
+      );
+    case 'export':
+      return (
+        <>
+          <span className="material-symbols-outlined me-1">cloud_download</span>
+          {t('export_management.export_archive_data')}
+        </>
+      );
+    case 'data-transfer':
+      return (
+        <>
+          <span className="material-symbols-outlined me-1">flight</span>
+          {t('g2g_data_transfer.data_transfer', { ns: 'commons' })}
+        </>
+      );
+    case 'notification':
+      return (
+        <>
+          <span className="material-symbols-outlined me-1">notifications</span>
+          {t('external_notification.external_notification')}
+        </>
+      );
+    case 'slack-integration':
+      return (
+        <>
+          <span className="material-symbols-outlined me-1">shuffle</span>
+          {t('slack_integration.slack_integration')}
+        </>
+      );
+    case 'slack-integration-legacy':
+      return (
+        <>
+          <span className="material-symbols-outlined me-1">shuffle</span>
+          {t('slack_integration_legacy.slack_integration_legacy')}
+        </>
+      );
+    case 'users':
+      return (
+        <>
+          <span className="material-symbols-outlined me-1">person</span>
+          {t('user_management.user_management')}
+        </>
+      );
+    case 'user-groups':
+      return (
+        <>
+          <span className="material-symbols-outlined me-1">group</span>
+          {t('user_group_management.user_group_management')}
+        </>
+      );
+    case 'audit-log':
+      return (
+        <>
+          <span className="material-symbols-outlined me-1">feed</span>
+          {t('audit_log_management.audit_log')}
+        </>
+      );
+    case 'plugins':
+      return (
+        <>
+          <span className="material-symbols-outlined me-1">extension</span>
+          {t('plugins.plugins')}
+        </>
+      );
     // Temporarily hiding
     // case 'ai-integration':           return (
     //   <>{/* TODO: unify sizing of growi-custom-icons so that simplify code -- 2024.10.09 Yuki Takei */}
@@ -46,24 +131,44 @@ const MenuLabel = ({ menu }: { menu: string }) => {
     //     {t('ai_integration.ai_integration')}
     //   </>
     // );
-    case 'search':                   return <><span className="material-symbols-outlined me-1">search</span>{           t('full_text_search_management.full_text_search_management') }</>;
-    case 'cloud':                    return <><span className="material-symbols-outlined me-1">share</span>{            t('cloud_setting_management.to_cloud_settings')} </>;
-    default:                         return <><span className="material-symbols-outlined me-1">home</span>{             t('wiki_management_homepage') }</>;
-      /* eslint-enable no-multi-spaces, max-len */
+    case 'search':
+      return (
+        <>
+          <span className="material-symbols-outlined me-1">search</span>
+          {t('full_text_search_management.full_text_search_management')}
+        </>
+      );
+    case 'cloud':
+      return (
+        <>
+          <span className="material-symbols-outlined me-1">share</span>
+          {t('cloud_setting_management.to_cloud_settings')}{' '}
+        </>
+      );
+    default:
+      return (
+        <>
+          <span className="material-symbols-outlined me-1">home</span>
+          {t('wiki_management_homepage')}
+        </>
+      );
+    /* eslint-enable no-multi-spaces, max-len */
   }
 };
 
 type MenuLinkProps = {
-  menu: string,
-  isListGroupItems: boolean,
-  isRoot?: boolean,
-  isActive?: boolean,
-}
+  menu: string;
+  isListGroupItems: boolean;
+  isRoot?: boolean;
+  isActive?: boolean;
+};
 
 const MenuLink = ({
-  menu, isRoot, isListGroupItems, isActive,
+  menu,
+  isRoot,
+  isListGroupItems,
+  isActive,
 }: MenuLinkProps) => {
-
   const pageTransitionClassName = isListGroupItems
     ? 'list-group-item list-group-item-action rounded border-0'
     : 'dropdown-item px-3 py-2';
@@ -86,57 +191,122 @@ export const AdminNavigation = (): JSX.Element => {
   const { data: growiCloudUri } = useGrowiCloudUri();
   const { data: growiAppIdForGrowiCloud } = useGrowiAppIdForGrowiCloud();
 
-  const isActiveMenu = useCallback((path: string | string[]) => {
-    const paths = Array.isArray(path) ? path : [path];
-
-    return paths.some((path) => {
-      const basisPath = pathUtils.normalizePath(urljoin('/admin', path));
-      const basisParentPath = pathUtils.addTrailingSlash(basisPath);
-
-      return (
-        pathname === basisPath
-        || pathname.startsWith(basisParentPath)
-      );
-    });
-
-  }, [pathname]);
-
-  const getListGroupItemOrDropdownItemList = useCallback((isListGroupItems: boolean) => {
-    return (
-      <>
-        {/* eslint-disable no-multi-spaces */}
-        <MenuLink menu="home" isListGroupItems={isListGroupItems} isActive={pathname === '/admin'} isRoot />
-        <MenuLink menu="app" isListGroupItems={isListGroupItems} isActive={isActiveMenu('/app')} />
-        <MenuLink menu="security" isListGroupItems={isListGroupItems} isActive={isActiveMenu('/security')} />
-        <MenuLink menu="markdown" isListGroupItems={isListGroupItems} isActive={isActiveMenu('/markdown')} />
-        <MenuLink menu="customize" isListGroupItems={isListGroupItems} isActive={isActiveMenu('/customize')} />
-        <MenuLink menu="importer" isListGroupItems={isListGroupItems} isActive={isActiveMenu('/importer')} />
-        <MenuLink menu="export" isListGroupItems={isListGroupItems} isActive={isActiveMenu('/export')} />
-        <MenuLink menu="data-transfer" isListGroupItems={isListGroupItems} isActive={isActiveMenu('/data-transfer')} />
-        <MenuLink menu="notification" isListGroupItems={isListGroupItems} isActive={isActiveMenu(['/notification', '/global-notification'])} />
-        <MenuLink menu="slack-integration" isListGroupItems={isListGroupItems} isActive={isActiveMenu('/slack-integration')} />
-        <MenuLink menu="slack-integration-legacy" isListGroupItems={isListGroupItems} isActive={isActiveMenu('/slack-integration-legacy')} />
-        <MenuLink menu="users" isListGroupItems={isListGroupItems} isActive={isActiveMenu('/users')} />
-        <MenuLink menu="user-groups" isListGroupItems={isListGroupItems} isActive={isActiveMenu(['/user-groups', 'user-group-detail'])} />
-        <MenuLink menu="audit-log" isListGroupItems={isListGroupItems} isActive={isActiveMenu('/audit-log')} />
-        <MenuLink menu="plugins" isListGroupItems={isListGroupItems} isActive={isActiveMenu('/plugins')} />
-        {/* Temporarily hiding */}
-        {/* <MenuLink menu="ai-integration" isListGroupItems={isListGroupItems} isActive={isActiveMenu('/aai-integration')} /> */}
-        <MenuLink menu="search" isListGroupItems={isListGroupItems} isActive={isActiveMenu('/search')} />
-        {growiCloudUri != null && growiAppIdForGrowiCloud != null
-          && (
+  const isActiveMenu = useCallback(
+    (path: string | string[]) => {
+      const paths = Array.isArray(path) ? path : [path];
+
+      return paths.some((path) => {
+        const basisPath = pathUtils.normalizePath(urljoin('/admin', path));
+        const basisParentPath = pathUtils.addTrailingSlash(basisPath);
+
+        return pathname === basisPath || pathname.startsWith(basisParentPath);
+      });
+    },
+    [pathname],
+  );
+
+  const getListGroupItemOrDropdownItemList = useCallback(
+    (isListGroupItems: boolean) => {
+      return (
+        <>
+          {/* eslint-disable no-multi-spaces */}
+          <MenuLink
+            menu="home"
+            isListGroupItems={isListGroupItems}
+            isActive={pathname === '/admin'}
+            isRoot
+          />
+          <MenuLink
+            menu="app"
+            isListGroupItems={isListGroupItems}
+            isActive={isActiveMenu('/app')}
+          />
+          <MenuLink
+            menu="security"
+            isListGroupItems={isListGroupItems}
+            isActive={isActiveMenu('/security')}
+          />
+          <MenuLink
+            menu="markdown"
+            isListGroupItems={isListGroupItems}
+            isActive={isActiveMenu('/markdown')}
+          />
+          <MenuLink
+            menu="customize"
+            isListGroupItems={isListGroupItems}
+            isActive={isActiveMenu('/customize')}
+          />
+          <MenuLink
+            menu="importer"
+            isListGroupItems={isListGroupItems}
+            isActive={isActiveMenu('/importer')}
+          />
+          <MenuLink
+            menu="export"
+            isListGroupItems={isListGroupItems}
+            isActive={isActiveMenu('/export')}
+          />
+          <MenuLink
+            menu="data-transfer"
+            isListGroupItems={isListGroupItems}
+            isActive={isActiveMenu('/data-transfer')}
+          />
+          <MenuLink
+            menu="notification"
+            isListGroupItems={isListGroupItems}
+            isActive={isActiveMenu(['/notification', '/global-notification'])}
+          />
+          <MenuLink
+            menu="slack-integration"
+            isListGroupItems={isListGroupItems}
+            isActive={isActiveMenu('/slack-integration')}
+          />
+          <MenuLink
+            menu="slack-integration-legacy"
+            isListGroupItems={isListGroupItems}
+            isActive={isActiveMenu('/slack-integration-legacy')}
+          />
+          <MenuLink
+            menu="users"
+            isListGroupItems={isListGroupItems}
+            isActive={isActiveMenu('/users')}
+          />
+          <MenuLink
+            menu="user-groups"
+            isListGroupItems={isListGroupItems}
+            isActive={isActiveMenu(['/user-groups', 'user-group-detail'])}
+          />
+          <MenuLink
+            menu="audit-log"
+            isListGroupItems={isListGroupItems}
+            isActive={isActiveMenu('/audit-log')}
+          />
+          <MenuLink
+            menu="plugins"
+            isListGroupItems={isListGroupItems}
+            isActive={isActiveMenu('/plugins')}
+          />
+          {/* Temporarily hiding */}
+          {/* <MenuLink menu="ai-integration" isListGroupItems={isListGroupItems} isActive={isActiveMenu('/aai-integration')} /> */}
+          <MenuLink
+            menu="search"
+            isListGroupItems={isListGroupItems}
+            isActive={isActiveMenu('/search')}
+          />
+          {growiCloudUri != null && growiAppIdForGrowiCloud != null && (
             <a
               href={`${growiCloudUri}/my/apps/${growiAppIdForGrowiCloud}`}
               className="list-group-item list-group-item-action border-0 round-corner"
             >
               <MenuLabel menu="cloud" />
             </a>
-          )
-        }
-        {/* eslint-enable no-multi-spaces */}
-      </>
-    );
-  }, [growiAppIdForGrowiCloud, growiCloudUri, isActiveMenu, pathname]);
+          )}
+          {/* eslint-enable no-multi-spaces */}
+        </>
+      );
+    },
+    [growiAppIdForGrowiCloud, growiCloudUri, isActiveMenu, pathname],
+  );
 
   return (
     <React.Fragment>
@@ -158,33 +328,41 @@ export const AdminNavigation = (): JSX.Element => {
         >
           <span className="float-start">
             {/* eslint-disable no-multi-spaces */}
-            {pathname === '/admin'                  && <MenuLabel menu="home" />}
-            {isActiveMenu('/app')                   && <MenuLabel menu="app" />}
-            {isActiveMenu('/security')              && <MenuLabel menu="security" />}
-            {isActiveMenu('/markdown')              && <MenuLabel menu="markdown" />}
-            {isActiveMenu('/customize')             && <MenuLabel menu="customize" />}
-            {isActiveMenu('/importer')              && <MenuLabel menu="importer" />}
-            {isActiveMenu('/export')                && <MenuLabel menu="export" />}
-            {(isActiveMenu(['/notification', '/global-notification']))
-                                                    && <MenuLabel menu="notification" />}
-            {isActiveMenu('/slack-integration')     && <MenuLabel menu="slack-integration" />}
-            {isActiveMenu('/users')                 && <MenuLabel menu="users" />}
-            {isActiveMenu(['/user-groups', 'user-group-detail'])
-                                                    && <MenuLabel menu="user-groups" />}
-            {isActiveMenu('/search')                && <MenuLabel menu="search" />}
-            {isActiveMenu('/audit-log')             && <MenuLabel menu="audit-log" />}
-            {isActiveMenu('/plugins')               && <MenuLabel menu="plugins" />}
-            {isActiveMenu('/data-transfer')         && <MenuLabel menu="data-transfer" />}
+            {pathname === '/admin' && <MenuLabel menu="home" />}
+            {isActiveMenu('/app') && <MenuLabel menu="app" />}
+            {isActiveMenu('/security') && <MenuLabel menu="security" />}
+            {isActiveMenu('/markdown') && <MenuLabel menu="markdown" />}
+            {isActiveMenu('/customize') && <MenuLabel menu="customize" />}
+            {isActiveMenu('/importer') && <MenuLabel menu="importer" />}
+            {isActiveMenu('/export') && <MenuLabel menu="export" />}
+            {isActiveMenu(['/notification', '/global-notification']) && (
+              <MenuLabel menu="notification" />
+            )}
+            {isActiveMenu('/slack-integration') && (
+              <MenuLabel menu="slack-integration" />
+            )}
+            {isActiveMenu('/users') && <MenuLabel menu="users" />}
+            {isActiveMenu(['/user-groups', 'user-group-detail']) && (
+              <MenuLabel menu="user-groups" />
+            )}
+            {isActiveMenu('/search') && <MenuLabel menu="search" />}
+            {isActiveMenu('/audit-log') && <MenuLabel menu="audit-log" />}
+            {isActiveMenu('/plugins') && <MenuLabel menu="plugins" />}
+            {isActiveMenu('/data-transfer') && (
+              <MenuLabel menu="data-transfer" />
+            )}
             {/* Temporarily hiding */}
             {/* {isActiveMenu('/ai-integration')                && <MenuLabel menu="ai-integration" />} */}
             {/* eslint-enable no-multi-spaces */}
           </span>
         </button>
-        <div className="dropdown-menu" aria-labelledby="dropdown-admin-navigation">
+        <div
+          className="dropdown-menu"
+          aria-labelledby="dropdown-admin-navigation"
+        >
           {getListGroupItemOrDropdownItemList(false)}
         </div>
       </div>
-
     </React.Fragment>
   );
 };

+ 95 - 72
apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.tsx

@@ -1,100 +1,123 @@
-import React, { memo, useCallback, type JSX } from 'react';
-
 import Link from 'next/link';
+import React, { type JSX, memo, useCallback } from 'react';
 import urljoin from 'url-join';
 
 import type LinkedPagePath from '~/models/linked-page-path';
 
 import styles from './PagePathHierarchicalLink.module.scss';
 
-
 type PagePathHierarchicalLinkProps = {
-  linkedPagePath: LinkedPagePath,
-  linkedPagePathByHtml?: LinkedPagePath,
-  basePath?: string,
-  isInTrash?: boolean,
-  isIconHidden?: boolean,
+  linkedPagePath: LinkedPagePath;
+  linkedPagePathByHtml?: LinkedPagePath;
+  basePath?: string;
+  isInTrash?: boolean;
+  isIconHidden?: boolean;
 
   // !!INTERNAL USE ONLY!!
-  isInnerElem?: boolean,
+  isInnerElem?: boolean;
 };
 
-export const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkProps): JSX.Element => {
-  const {
-    linkedPagePath, linkedPagePathByHtml, basePath, isInTrash, isInnerElem,
-  } = props;
-
-  const isIconHidden = props.isIconHidden ?? false;
-
-  // eslint-disable-next-line react/prop-types
-  const RootElm = useCallback(({ children }) => {
-    return isInnerElem
-      ? <>{children}</>
-      : <span className="text-break" id="grw-page-path-hierarchical-link">{children}</span>;
-  }, [isInnerElem]);
-
-  // render root element
-  if (linkedPagePath.isRoot) {
-    if (basePath != null || isIconHidden) {
-      return <></>;
-    }
-
-    return isInTrash
-      ? (
+export const PagePathHierarchicalLink = memo(
+  (props: PagePathHierarchicalLinkProps): JSX.Element => {
+    const {
+      linkedPagePath,
+      linkedPagePathByHtml,
+      basePath,
+      isInTrash,
+      isInnerElem,
+    } = props;
+
+    const isIconHidden = props.isIconHidden ?? false;
+
+    // eslint-disable-next-line react/prop-types
+    const RootElm = useCallback(
+      ({ children }) => {
+        return isInnerElem ? (
+          <>{children}</>
+        ) : (
+          <span className="text-break" id="grw-page-path-hierarchical-link">
+            {children}
+          </span>
+        );
+      },
+      [isInnerElem],
+    );
+
+    // render root element
+    if (linkedPagePath.isRoot) {
+      if (basePath != null || isIconHidden) {
+        return <></>;
+      }
+
+      return isInTrash ? (
         <RootElm>
           <span className="path-segment">
             <Link href="/trash" prefetch={false}>
-              <span className={`material-symbols-outlined ${styles['material-symbols-outlined']}`}>delete</span>
+              <span
+                className={`material-symbols-outlined ${styles['material-symbols-outlined']}`}
+              >
+                delete
+              </span>
             </Link>
           </span>
-          <span className={`separator ${styles.separator}`}><a href="/">/</a></span>
+          <span className={`separator ${styles.separator}`}>
+            <a href="/">/</a>
+          </span>
         </RootElm>
-      )
-      : (
+      ) : (
         <RootElm>
           <span className="path-segment">
             <Link href="/" prefetch={false}>
-              <span className={`material-symbols-outlined ${styles['material-symbols-outlined']}`}>home</span>
+              <span
+                className={`material-symbols-outlined ${styles['material-symbols-outlined']}`}
+              >
+                home
+              </span>
               <span className={`separator ${styles.separator}`}>/</span>
             </Link>
           </span>
         </RootElm>
       );
-  }
-
-  const isParentExists = linkedPagePath.parent != null;
-  const isParentRoot = linkedPagePath.parent?.isRoot;
-  const isSeparatorRequired = isParentExists && !isParentRoot;
-
-  const shouldDangerouslySetInnerHTML = linkedPagePathByHtml != null;
-
-  const href = encodeURI(urljoin(basePath || '/', linkedPagePath.href));
-
-  return (
-    <RootElm>
-      { isParentExists && (
-        <PagePathHierarchicalLink
-          linkedPagePath={linkedPagePath.parent}
-          linkedPagePathByHtml={linkedPagePathByHtml?.parent}
-          basePath={basePath}
-          isInTrash={isInTrash || linkedPagePath.isInTrash}
-          isInnerElem
-          isIconHidden={isIconHidden}
-        />
-      ) }
-      { isSeparatorRequired && (
-        <span className={`separator ${styles.separator}`}>/</span>
-      ) }
+    }
 
-      <Link href={href} prefetch={false} legacyBehavior>
-        {
-          shouldDangerouslySetInnerHTML
+    const isParentExists = linkedPagePath.parent != null;
+    const isParentRoot = linkedPagePath.parent?.isRoot;
+    const isSeparatorRequired = isParentExists && !isParentRoot;
+
+    const shouldDangerouslySetInnerHTML = linkedPagePathByHtml != null;
+
+    const href = encodeURI(urljoin(basePath || '/', linkedPagePath.href));
+
+    return (
+      <RootElm>
+        {isParentExists && (
+          <PagePathHierarchicalLink
+            linkedPagePath={linkedPagePath.parent}
+            linkedPagePathByHtml={linkedPagePathByHtml?.parent}
+            basePath={basePath}
+            isInTrash={isInTrash || linkedPagePath.isInTrash}
+            isInnerElem
+            isIconHidden={isIconHidden}
+          />
+        )}
+        {isSeparatorRequired && (
+          <span className={`separator ${styles.separator}`}>/</span>
+        )}
+
+        <Link href={href} prefetch={false} legacyBehavior>
+          {shouldDangerouslySetInnerHTML ? (
             // eslint-disable-next-line react/no-danger
-            ? <a className="page-segment" dangerouslySetInnerHTML={{ __html: linkedPagePathByHtml.pathName }}></a>
-            : <a className="page-segment">{linkedPagePath.pathName}</a>
-        }
-      </Link>
-
-    </RootElm>
-  );
-});
+            <a
+              className="page-segment"
+              dangerouslySetInnerHTML={{
+                __html: linkedPagePathByHtml.pathName,
+              }}
+            ></a>
+          ) : (
+            <a className="page-segment">{linkedPagePath.pathName}</a>
+          )}
+        </Link>
+      </RootElm>
+    );
+  },
+);

+ 23 - 12
apps/app/src/components/Common/PagePathNav/PagePathNav.tsx

@@ -1,23 +1,22 @@
-import { useMemo, type JSX } from 'react';
-
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { pagePathUtils } from '@growi/core/dist/utils';
+import { type JSX, useMemo } from 'react';
 
 import LinkedPagePath from '~/models/linked-page-path';
 
 import { PagePathHierarchicalLink } from '../PagePathHierarchicalLink';
-
+import styles from './PagePathNav.module.scss';
 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>;
+const Separator = ({ className }: { className?: string }): JSX.Element => {
+  return (
+    <span className={`separator ${className ?? ''} ${styles['grw-mx-02em']}`}>
+      /
+    </span>
+  );
 };
 
 export const PagePathNav = (props: PagePathNavLayoutProps): JSX.Element => {
@@ -37,7 +36,10 @@ export const PagePathNav = (props: PagePathNavLayoutProps): JSX.Element => {
     const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
     return (
       <>
-        <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} isInTrash={isInTrash} />
+        <PagePathHierarchicalLink
+          linkedPagePath={linkedPagePathFormer}
+          isInTrash={isInTrash}
+        />
         <Separator />
       </>
     );
@@ -49,13 +51,22 @@ export const PagePathNav = (props: PagePathNavLayoutProps): JSX.Element => {
     // one line
     if (dPagePath.isRoot || dPagePath.isFormerRoot) {
       const linkedPagePath = new LinkedPagePath(pagePath);
-      return <PagePathHierarchicalLink linkedPagePath={linkedPagePath} isInTrash={isInTrash} />;
+      return (
+        <PagePathHierarchicalLink
+          linkedPagePath={linkedPagePath}
+          isInTrash={isInTrash}
+        />
+      );
     }
 
     // two line
     const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
     return (
-      <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.former} isInTrash={isInTrash} />
+      <PagePathHierarchicalLink
+        linkedPagePath={linkedPagePathLatter}
+        basePath={dPagePath.former}
+        isInTrash={isInTrash}
+      />
     );
   }, [isInTrash, pagePath]);
 

+ 30 - 26
apps/app/src/components/Common/PagePathNav/PagePathNavLayout.tsx

@@ -1,6 +1,5 @@
-import type { ReactNode, JSX } from 'react';
-
 import dynamic from 'next/dynamic';
+import type { JSX, ReactNode } from 'react';
 
 import { useIsNotFound } from '~/stores/page';
 
@@ -9,28 +8,36 @@ import styles from './PagePathNav.module.scss';
 const moduleClass = styles['grw-page-path-nav-layout'] ?? '';
 
 export type PagePathNavLayoutProps = {
-  pagePath: string,
-  inline?: boolean,
-  className?: string,
-  pageId?: string | null,
-  isWipPage?: boolean,
-  maxWidth?: number,
-  formerLinkClassName?: string,
-  latterLinkClassName?: string,
-}
+  pagePath: string;
+  inline?: boolean;
+  className?: string;
+  pageId?: string | null;
+  isWipPage?: boolean;
+  maxWidth?: number;
+  formerLinkClassName?: string;
+  latterLinkClassName?: string;
+};
 
 type Props = PagePathNavLayoutProps & {
-  formerLink?: ReactNode,
-  latterLink?: ReactNode,
-}
+  formerLink?: ReactNode;
+  latterLink?: ReactNode;
+};
 
-const CopyDropdown = dynamic(() => import('~/client/components/Common/CopyDropdown').then(mod => mod.CopyDropdown), { ssr: false });
+const CopyDropdown = dynamic(
+  () =>
+    import('~/client/components/Common/CopyDropdown').then(
+      (mod) => mod.CopyDropdown,
+    ),
+  { ssr: false },
+);
 
 export const PagePathNavLayout = (props: Props): JSX.Element => {
   const {
     className = '',
     inline = false,
-    pageId, pagePath, isWipPage,
+    pageId,
+    pagePath,
+    isWipPage,
     formerLink,
     formerLinkClassName = '',
     latterLink,
@@ -45,12 +52,11 @@ export const PagePathNavLayout = (props: Props): JSX.Element => {
   const containerLayoutClass = inline ? '' : 'd-flex align-items-center';
 
   return (
-    <div
-      className={`${className} ${moduleClass}`}
-      style={{ maxWidth }}
-    >
+    <div className={`${className} ${moduleClass}`} style={{ maxWidth }}>
       {formerLink && (
-        <span className={`${formerLinkClassName ?? ''} ${styles['grw-former-link']} mb-2 d-block`}>
+        <span
+          className={`${formerLinkClassName ?? ''} ${styles['grw-former-link']} mb-2 d-block`}
+        >
           {formerLink}
         </span>
       )}
@@ -58,11 +64,9 @@ export const PagePathNavLayout = (props: Props): JSX.Element => {
         <h1 className={`m-0 d-inline align-bottom ${latterLinkClassName}`}>
           {latterLink}
         </h1>
-        { pageId != null && !isNotFound && (
+        {pageId != null && !isNotFound && (
           <span className="d-inline-flex align-items-center align-bottom ms-2 gap-2">
-            { isWipPage && (
-              <span className="badge text-bg-secondary">WIP</span>
-            )}
+            {isWipPage && <span className="badge text-bg-secondary">WIP</span>}
             <span className="grw-page-path-nav-copydropdown">
               <CopyDropdown
                 pageId={pageId}
@@ -75,7 +79,7 @@ export const PagePathNavLayout = (props: Props): JSX.Element => {
               </CopyDropdown>
             </span>
           </span>
-        ) }
+        )}
       </div>
     </div>
   );

+ 8 - 3
apps/app/src/components/Common/PagePathNav/Separator.tsx

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

+ 36 - 22
apps/app/src/components/Common/PagePathNavTitle/PagePathNavTitle.tsx

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

+ 1 - 1
apps/app/src/components/FontFamily/GlobalFonts.tsx

@@ -1,4 +1,4 @@
-import { memo, type JSX } from 'react';
+import { type JSX, memo } from 'react';
 
 import { useGrowiCustomIcon } from './use-growi-custom-icons';
 import { useLatoFontFamily } from './use-lato';

+ 27 - 26
apps/app/src/components/Layout/AdminLayout.tsx

@@ -1,37 +1,41 @@
-import type { ReactNode, JSX } from 'react';
-import React from 'react';
-
 import dynamic from 'next/dynamic';
 import Link from 'next/link';
+import type { JSX, ReactNode } from 'react';
+import React from 'react';
 
 import GrowiLogo from '~/components/Common/GrowiLogo';
-
-import { RawLayout } from './RawLayout';
-
-
 import styles from './Admin.module.scss';
+import { RawLayout } from './RawLayout';
 
-
-const AdminNavigation = dynamic(() => import('../Admin/Common/AdminNavigation').then(mod => mod.AdminNavigation), { ssr: false });
-const PageCreateModal = dynamic(() => import('~/client/components/PageCreateModal'), { ssr: false });
-const SystemVersion = dynamic(() => import('~/client/components/SystemVersion'), { ssr: false });
-const HotkeysManager = dynamic(() => import('~/client/components/Hotkeys/HotkeysManager'), { ssr: false });
-
+const AdminNavigation = dynamic(
+  () =>
+    import('../Admin/Common/AdminNavigation').then(
+      (mod) => mod.AdminNavigation,
+    ),
+  { ssr: false },
+);
+const PageCreateModal = dynamic(
+  () => import('~/client/components/PageCreateModal'),
+  { ssr: false },
+);
+const SystemVersion = dynamic(
+  () => import('~/client/components/SystemVersion'),
+  { ssr: false },
+);
+const HotkeysManager = dynamic(
+  () => import('~/client/components/Hotkeys/HotkeysManager'),
+  { ssr: false },
+);
 
 type Props = {
-  componentTitle?: string
-  children?: ReactNode
-}
-
-
-const AdminLayout = ({
-  children, componentTitle,
-}: Props): JSX.Element => {
+  componentTitle?: string;
+  children?: ReactNode;
+};
 
+const AdminLayout = ({ children, componentTitle }: Props): JSX.Element => {
   return (
     <RawLayout>
       <div className={`admin-page ${styles['admin-page']}`}>
-
         <header className="py-0 container">
           <h1 className="p-3 fs-2 d-flex align-items-center">
             <Link href="/" className="d-block mb-1 me-2">
@@ -46,9 +50,7 @@ const AdminLayout = ({
               <div className="col-lg-3">
                 <AdminNavigation />
               </div>
-              <div className="col-lg-9 mb-5">
-                {children}
-              </div>
+              <div className="col-lg-9 mb-5">{children}</div>
             </div>
           </div>
         </div>
@@ -58,7 +60,6 @@ const AdminLayout = ({
       </div>
 
       <HotkeysManager />
-
     </RawLayout>
   );
 };

+ 111 - 37
apps/app/src/components/Layout/BasicLayout.tsx

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

+ 15 - 19
apps/app/src/components/Layout/NoLoginLayout.tsx

@@ -1,26 +1,19 @@
-import type { ReactNode, JSX } from 'react';
-import React from 'react';
-
 import Image from 'next/image';
+import type { JSX, ReactNode } from 'react';
+import React from 'react';
 
 import { useAppTitle } from '~/stores-universal/context';
 
 import GrowiLogo from '../Common/GrowiLogo';
-
-import { RawLayout } from './RawLayout';
-
-
 import commonStyles from './NoLoginLayout.module.scss';
+import { RawLayout } from './RawLayout';
 
 type Props = {
-  className?: string,
-  children?: ReactNode,
-}
-
-export const NoLoginLayout = ({
-  children, className,
-}: Props): JSX.Element => {
+  className?: string;
+  children?: ReactNode;
+};
 
+export const NoLoginLayout = ({ children, className }: Props): JSX.Element => {
   const { data: appTitle } = useAppTitle();
 
   const classNames: string[] = [''];
@@ -32,23 +25,26 @@ export const NoLoginLayout = ({
     <RawLayout className={`nologin ${commonStyles.nologin} ${classNames}`}>
       <div className="d-flex align-items-center vh-100 mt-0 flex-row">
         <div className="main container-fluid">
-
           <div className="row">
-
             <div className="col-md-12 position-relative">
               <div className="nologin-header mx-auto rounded-4 rounded-bottom-0">
                 <div className="d-flex justify-content-center align-items-center">
                   <GrowiLogo />
-                  <Image width={128.48} height={28} src="/images/growi-brand-logo-login.svg" alt="GROWI" className="growi-logo-type my-3" />
+                  <Image
+                    width={128.48}
+                    height={28}
+                    src="/images/growi-brand-logo-login.svg"
+                    alt="GROWI"
+                    className="growi-logo-type my-3"
+                  />
                 </div>
                 {appTitle !== 'GROWI' ? (
-                  <h2 className="fs-4 text-center text-white">{ appTitle }</h2>
+                  <h2 className="fs-4 text-center text-white">{appTitle}</h2>
                 ) : null}
                 <div className="noLogin-form-errors px-3"></div>
               </div>
               {children}
             </div>
-
           </div>
         </div>
       </div>

+ 16 - 11
apps/app/src/components/Layout/RawLayout.tsx

@@ -1,12 +1,14 @@
-import type { ReactNode, JSX } from 'react';
-import React, { useState } from 'react';
-
 import type { ColorScheme } from '@growi/core';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import type { JSX, ReactNode } from 'react';
+import React, { useState } from 'react';
 import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 
-import { useNextThemes, NextThemesProvider } from '~/stores-universal/use-next-themes';
+import {
+  NextThemesProvider,
+  useNextThemes,
+} from '~/stores-universal/use-next-themes';
 import loggerFactory from '~/utils/logger';
 
 import styles from './RawLayout.module.scss';
@@ -15,14 +17,15 @@ const toastContainerClass = styles['grw-toast-container'] ?? '';
 
 const logger = loggerFactory('growi:cli:RawLayout');
 
-
-const ToastContainer = dynamic(() => import('react-toastify').then(mod => mod.ToastContainer), { ssr: false });
-
+const ToastContainer = dynamic(
+  () => import('react-toastify').then((mod) => mod.ToastContainer),
+  { ssr: false },
+);
 
 type Props = {
-  className?: string,
-  children?: ReactNode,
-}
+  className?: string;
+  children?: ReactNode;
+};
 
 export const RawLayout = ({ children, className }: Props): JSX.Element => {
   const classNames: string[] = ['layout-root', 'growi'];
@@ -32,7 +35,9 @@ export const RawLayout = ({ children, className }: Props): JSX.Element => {
   // get color scheme from next-themes
   const { resolvedTheme, resolvedThemeByAttributes } = useNextThemes();
 
-  const [colorScheme, setColorScheme] = useState<ColorScheme|undefined>(undefined);
+  const [colorScheme, setColorScheme] = useState<ColorScheme | undefined>(
+    undefined,
+  );
 
   // set colorScheme in CSR
   useIsomorphicLayoutEffect(() => {

+ 4 - 5
apps/app/src/components/Layout/SearchResultLayout.tsx

@@ -1,18 +1,17 @@
-import React, { type ReactNode, type JSX } from 'react';
+import React, { type JSX, type ReactNode } from 'react';
 
 import { BasicLayout } from './BasicLayout';
 
 import commonStyles from './SearchResultLayout.module.scss';
 
 type Props = {
-  children?: ReactNode,
-}
+  children?: ReactNode;
+};
 
 const SearchResultLayout = ({ children }: Props): JSX.Element => {
-
   return (
     <BasicLayout className={`on-search ${commonStyles['on-search']}`}>
-      { children }
+      {children}
     </BasicLayout>
   );
 };

+ 24 - 14
apps/app/src/components/Layout/ShareLinkLayout.tsx

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

+ 4 - 1
apps/app/src/components/Navbar/GroundGlassBar.tsx

@@ -4,7 +4,10 @@ import styles from './GroundGlassBar.module.scss';
 
 const moduleClass = styles['ground-glass-bar'];
 
-type Props = DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
+type Props = DetailedHTMLProps<
+  React.HTMLAttributes<HTMLDivElement>,
+  HTMLDivElement
+>;
 
 export const GroundGlassBar = (props: Props): JSX.Element => {
   const { className, children, ...rest } = props;

+ 193 - 114
apps/app/src/components/PageView/PageAlerts/FixPageGrantAlert.tsx

@@ -1,42 +1,57 @@
-import React, {
-  useEffect, useState, useCallback, type JSX,
-} from 'react';
-
-import { PageGrant, GroupType } from '@growi/core';
+import { GroupType, PageGrant } from '@growi/core';
+import React, { type JSX, useCallback, useEffect, useState } from 'react';
 import { useTranslation } from 'react-i18next';
-import {
-  Modal, ModalHeader, ModalBody, ModalFooter,
-} from 'reactstrap';
+import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
 
-import { UserGroupPageGrantStatus, type IPageGrantData } from '~/interfaces/page';
-import type { PopulatedGrantedGroup, IRecordApplicableGrant, IResGrantData } from '~/interfaces/page-grant';
+import {
+  type IPageGrantData,
+  UserGroupPageGrantStatus,
+} from '~/interfaces/page';
+import type {
+  IRecordApplicableGrant,
+  IResGrantData,
+  PopulatedGrantedGroup,
+} from '~/interfaces/page-grant';
+import {
+  useSWRxApplicableGrant,
+  useSWRxCurrentGrantData,
+  useSWRxCurrentPage,
+} from '~/stores/page';
 import { useCurrentUser } from '~/stores-universal/context';
-import { useSWRxApplicableGrant, useSWRxCurrentGrantData, useSWRxCurrentPage } from '~/stores/page';
 
 type ModalProps = {
-  isOpen: boolean
-  pageId: string
-  dataApplicableGrant: IRecordApplicableGrant
-  currentAndParentPageGrantData: IResGrantData
-  close(): void
-}
+  isOpen: boolean;
+  pageId: string;
+  dataApplicableGrant: IRecordApplicableGrant;
+  currentAndParentPageGrantData: IResGrantData;
+  close(): void;
+};
 
 const FixPageGrantModal = (props: ModalProps): JSX.Element => {
   const { t } = useTranslation();
 
   const {
-    isOpen, pageId, dataApplicableGrant, currentAndParentPageGrantData, close,
+    isOpen,
+    pageId,
+    dataApplicableGrant,
+    currentAndParentPageGrantData,
+    close,
   } = props;
 
-  const [selectedGrant, setSelectedGrant] = useState<PageGrant>(PageGrant.GRANT_RESTRICTED);
+  const [selectedGrant, setSelectedGrant] = useState<PageGrant>(
+    PageGrant.GRANT_RESTRICTED,
+  );
 
   const [isGroupSelectModalShown, setIsGroupSelectModalShown] = useState(false);
-  const [selectedGroups, setSelectedGroups] = useState<PopulatedGrantedGroup[]>([]);
+  const [selectedGroups, setSelectedGroups] = useState<PopulatedGrantedGroup[]>(
+    [],
+  );
 
   // Alert message state
   const [shouldShowModalAlert, setShowModalAlert] = useState<boolean>(false);
 
-  const applicableGroups = dataApplicableGrant[PageGrant.GRANT_USER_GROUP]?.applicableGroups;
+  const applicableGroups =
+    dataApplicableGrant[PageGrant.GRANT_USER_GROUP]?.applicableGroups;
 
   // Reset state when opened
   useEffect(() => {
@@ -48,17 +63,21 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
   }, [isOpen]);
 
   const groupListItemClickHandler = (group: PopulatedGrantedGroup) => {
-    if (selectedGroups.find(g => g.item._id === group.item._id) != null) {
-      setSelectedGroups(selectedGroups.filter(g => g.item._id !== group.item._id));
-    }
-    else {
+    if (selectedGroups.find((g) => g.item._id === group.item._id) != null) {
+      setSelectedGroups(
+        selectedGroups.filter((g) => g.item._id !== group.item._id),
+      );
+    } else {
       setSelectedGroups([...selectedGroups, group]);
     }
   };
 
-  const submit = async() => {
+  const submit = async () => {
     // Validate input values
-    if (selectedGrant === PageGrant.GRANT_USER_GROUP && selectedGroups.length === 0) {
+    if (
+      selectedGrant === PageGrant.GRANT_USER_GROUP &&
+      selectedGroups.length === 0
+    ) {
       setShowModalAlert(true);
       return;
     }
@@ -69,69 +88,89 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
       const apiv3Put = (await import('~/client/util/apiv3-client')).apiv3Put;
       await apiv3Put(`/page/${pageId}/grant`, {
         grant: selectedGrant,
-        userRelatedGrantedGroups: selectedGroups.length !== 0 ? selectedGroups.map((g) => {
-          return { item: g.item._id, type: g.type };
-        }) : null,
+        userRelatedGrantedGroups:
+          selectedGroups.length !== 0
+            ? selectedGroups.map((g) => {
+                return { item: g.item._id, type: g.type };
+              })
+            : null,
       });
 
       const toastSuccess = (await import('~/client/util/toastr')).toastSuccess;
       toastSuccess(t('Successfully updated'));
-    }
-    catch (err) {
+    } catch (err) {
       const toastError = (await import('~/client/util/toastr')).toastError;
       toastError(t('Failed to update'));
     }
   };
 
-  const getGrantLabel = useCallback((isForbidden: boolean, grantData?: IPageGrantData): string => {
-
-    if (isForbidden) {
-      return t('fix_page_grant.modal.grant_label.isForbidden');
-    }
+  const getGrantLabel = useCallback(
+    (isForbidden: boolean, grantData?: IPageGrantData): string => {
+      if (isForbidden) {
+        return t('fix_page_grant.modal.grant_label.isForbidden');
+      }
 
-    if (grantData == null) {
-      return t('fix_page_grant.modal.grant_label.isForbidden');
-    }
+      if (grantData == null) {
+        return t('fix_page_grant.modal.grant_label.isForbidden');
+      }
 
-    if (grantData.grant === 1) {
-      return t('fix_page_grant.modal.grant_label.public');
-    }
+      if (grantData.grant === 1) {
+        return t('fix_page_grant.modal.grant_label.public');
+      }
 
-    if (grantData.grant === 4) {
-      return t('fix_page_grant.modal.radio_btn.only_me');
-    }
+      if (grantData.grant === 4) {
+        return t('fix_page_grant.modal.radio_btn.only_me');
+      }
 
-    if (grantData.grant === 5) {
-      const groupGrantData = grantData.groupGrantData;
-      if (groupGrantData != null) {
-        const userRelatedGrantedGroups = groupGrantData.userRelatedGroups.filter(group => group.status === UserGroupPageGrantStatus.isGranted);
-        if (userRelatedGrantedGroups.length > 0) {
-          const grantedGroupNames = [
-            ...userRelatedGrantedGroups.map(group => group.name),
-            ...groupGrantData.nonUserRelatedGrantedGroups.map(group => group.name),
-          ];
-          return `${t('fix_page_grant.modal.radio_btn.grant_group')} (${grantedGroupNames.join(', ')})`;
+      if (grantData.grant === 5) {
+        const groupGrantData = grantData.groupGrantData;
+        if (groupGrantData != null) {
+          const userRelatedGrantedGroups =
+            groupGrantData.userRelatedGroups.filter(
+              (group) => group.status === UserGroupPageGrantStatus.isGranted,
+            );
+          if (userRelatedGrantedGroups.length > 0) {
+            const grantedGroupNames = [
+              ...userRelatedGrantedGroups.map((group) => group.name),
+              ...groupGrantData.nonUserRelatedGrantedGroups.map(
+                (group) => group.name,
+              ),
+            ];
+            return `${t('fix_page_grant.modal.radio_btn.grant_group')} (${grantedGroupNames.join(', ')})`;
+          }
         }
-      }
 
-      return t('fix_page_grant.modal.grant_label.isForbidden');
-    }
+        return t('fix_page_grant.modal.grant_label.isForbidden');
+      }
 
-    throw Error('cannot get grant label'); // this error can't be throwed
-  }, [t]);
+      throw Error('cannot get grant label'); // this error can't be throwed
+    },
+    [t],
+  );
 
   const renderGrantDataLabel = useCallback(() => {
-    const { isForbidden, currentPageGrant, parentPageGrant } = currentAndParentPageGrantData;
+    const { isForbidden, currentPageGrant, parentPageGrant } =
+      currentAndParentPageGrantData;
 
     const currentGrantLabel = getGrantLabel(false, currentPageGrant);
     const parentGrantLabel = getGrantLabel(isForbidden, parentPageGrant);
 
     return (
       <>
-        <p className="mt-3">{ t('fix_page_grant.modal.grant_label.parentPageGrantLabel') + parentGrantLabel }</p>
-        <p>{ t('fix_page_grant.modal.grant_label.currentPageGrantLabel') + currentGrantLabel }</p>
+        <p className="mt-3">
+          {t('fix_page_grant.modal.grant_label.parentPageGrantLabel') +
+            parentGrantLabel}
+        </p>
+        <p>
+          {t('fix_page_grant.modal.grant_label.currentPageGrantLabel') +
+            currentGrantLabel}
+        </p>
         {/* eslint-disable-next-line react/no-danger */}
-        <p dangerouslySetInnerHTML={{ __html: t('fix_page_grant.modal.grant_label.docLink') }} />
+        <p
+          dangerouslySetInnerHTML={{
+            __html: t('fix_page_grant.modal.grant_label.docLink'),
+          }}
+        />
       </>
     );
   }, [t, currentAndParentPageGrantData, getGrantLabel]);
@@ -141,9 +180,7 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
 
     if (!isGrantAvailable) {
       return (
-        <p className="m-5">
-          { t('fix_page_grant.modal.no_grant_available') }
-        </p>
+        <p className="m-5">{t('fix_page_grant.modal.no_grant_available')}</p>
       );
     }
 
@@ -152,7 +189,12 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
         <ModalBody>
           <div>
             {/* eslint-disable-next-line react/no-danger */}
-            <p className="mb-2" dangerouslySetInnerHTML={{ __html: t('fix_page_grant.modal.need_to_fix_grant') }} />
+            <p
+              className="mb-2"
+              dangerouslySetInnerHTML={{
+                __html: t('fix_page_grant.modal.need_to_fix_grant'),
+              }}
+            />
 
             {/* grant data label */}
             {renderGrantDataLabel()}
@@ -164,12 +206,17 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
                   name="grantRestricted"
                   id="grantRestricted"
                   type="radio"
-                  disabled={!(PageGrant.GRANT_RESTRICTED in dataApplicableGrant)}
+                  disabled={
+                    !(PageGrant.GRANT_RESTRICTED in dataApplicableGrant)
+                  }
                   checked={selectedGrant === PageGrant.GRANT_RESTRICTED}
                   onChange={() => setSelectedGrant(PageGrant.GRANT_RESTRICTED)}
                 />
-                <label className="form-label form-check-label" htmlFor="grantRestricted">
-                  { t('fix_page_grant.modal.radio_btn.restrected') }
+                <label
+                  className="form-label form-check-label"
+                  htmlFor="grantRestricted"
+                >
+                  {t('fix_page_grant.modal.radio_btn.restrected')}
                 </label>
               </div>
               <div className="form-check mb-3">
@@ -182,8 +229,11 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
                   checked={selectedGrant === PageGrant.GRANT_OWNER}
                   onChange={() => setSelectedGrant(PageGrant.GRANT_OWNER)}
                 />
-                <label className="form-label form-check-label" htmlFor="grantUser">
-                  { t('fix_page_grant.modal.radio_btn.only_me') }
+                <label
+                  className="form-label form-check-label"
+                  htmlFor="grantUser"
+                >
+                  {t('fix_page_grant.modal.radio_btn.only_me')}
                 </label>
               </div>
               <div className="form-check d-flex mb-3">
@@ -192,12 +242,17 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
                   name="grantUserGroup"
                   id="grantUserGroup"
                   type="radio"
-                  disabled={!(PageGrant.GRANT_USER_GROUP in dataApplicableGrant)}
+                  disabled={
+                    !(PageGrant.GRANT_USER_GROUP in dataApplicableGrant)
+                  }
                   checked={selectedGrant === PageGrant.GRANT_USER_GROUP}
                   onChange={() => setSelectedGrant(PageGrant.GRANT_USER_GROUP)}
                 />
-                <label className="form-label form-check-label" htmlFor="grantUserGroup">
-                  { t('fix_page_grant.modal.radio_btn.grant_group') }
+                <label
+                  className="form-label form-check-label"
+                  htmlFor="grantUserGroup"
+                >
+                  {t('fix_page_grant.modal.radio_btn.grant_group')}
                 </label>
                 <div className="dropdown ms-2">
                   <button
@@ -207,28 +262,24 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
                     onClick={() => setIsGroupSelectModalShown(true)}
                   >
                     <span className="float-start ms-2">
-                      {
-                        selectedGroups.length === 0
-                          ? t('fix_page_grant.modal.select_group_default_text')
-                          : selectedGroups.map(g => g.item.name).join(', ')
-                      }
+                      {selectedGroups.length === 0
+                        ? t('fix_page_grant.modal.select_group_default_text')
+                        : selectedGroups.map((g) => g.item.name).join(', ')}
                     </span>
                   </button>
                 </div>
               </div>
-              {
-                shouldShowModalAlert && (
-                  <p className="alert alert-warning">
-                    {t('fix_page_grant.modal.alert_message')}
-                  </p>
-                )
-              }
+              {shouldShowModalAlert && (
+                <p className="alert alert-warning">
+                  {t('fix_page_grant.modal.alert_message')}
+                </p>
+              )}
             </div>
           </div>
         </ModalBody>
         <ModalFooter>
           <button type="button" className="btn btn-primary" onClick={submit}>
-            { t('fix_page_grant.modal.btn_label') }
+            {t('fix_page_grant.modal.btn_label')}
           </button>
         </ModalFooter>
       </>
@@ -239,7 +290,7 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
     <>
       <Modal size="lg" isOpen={isOpen} toggle={close}>
         <ModalHeader tag="h4" toggle={close}>
-          { t('fix_page_grant.modal.title') }
+          {t('fix_page_grant.modal.title')}
         </ModalHeader>
         {renderModalBodyAndFooter()}
       </Modal>
@@ -248,13 +299,18 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
           isOpen={isGroupSelectModalShown}
           toggle={() => setIsGroupSelectModalShown(false)}
         >
-          <ModalHeader tag="h4" toggle={() => setIsGroupSelectModalShown(false)}>
+          <ModalHeader
+            tag="h4"
+            toggle={() => setIsGroupSelectModalShown(false)}
+          >
             {t('user_group.select_group')}
           </ModalHeader>
           <ModalBody>
             <>
-              { applicableGroups.map((group) => {
-                const groupIsGranted = selectedGroups?.find(g => g.item._id === group.item._id) != null;
+              {applicableGroups.map((group) => {
+                const groupIsGranted =
+                  selectedGroups?.find((g) => g.item._id === group.item._id) !=
+                  null;
                 const activeClass = groupIsGranted ? 'active' : '';
 
                 return (
@@ -264,14 +320,26 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
                     key={group.item._id}
                     onClick={() => groupListItemClickHandler(group)}
                   >
-                    <span className="align-middle"><input type="checkbox" checked={groupIsGranted} /></span>
+                    <span className="align-middle">
+                      <input type="checkbox" checked={groupIsGranted} />
+                    </span>
                     <h5 className="d-inline-block ml-3">{group.item.name}</h5>
-                    {group.type === GroupType.externalUserGroup && <span className="ml-2 badge badge-pill badge-info">{group.item.provider}</span>}
+                    {group.type === GroupType.externalUserGroup && (
+                      <span className="ml-2 badge badge-pill badge-info">
+                        {group.item.provider}
+                      </span>
+                    )}
                     {/* TODO: Replace <div className="small">(TBD) List group members</div> */}
                   </button>
                 );
-              }) }
-              <button type="button" className="btn btn-primary mt-2 float-right" onClick={() => setIsGroupSelectModalShown(false)}>{t('Done')}</button>
+              })}
+              <button
+                type="button"
+                className="btn btn-primary mt-2 float-right"
+                onClick={() => setIsGroupSelectModalShown(false)}
+              >
+                {t('Done')}
+              </button>
             </>
           </ModalBody>
         </Modal>
@@ -290,8 +358,12 @@ export const FixPageGrantAlert = (): JSX.Element => {
 
   const [isOpen, setOpen] = useState<boolean>(false);
 
-  const { data: dataIsGrantNormalized } = useSWRxCurrentGrantData(currentUser != null ? pageId : null);
-  const { data: dataApplicableGrant } = useSWRxApplicableGrant(currentUser != null ? pageId : null);
+  const { data: dataIsGrantNormalized } = useSWRxCurrentGrantData(
+    currentUser != null ? pageId : null,
+  );
+  const { data: dataApplicableGrant } = useSWRxApplicableGrant(
+    currentUser != null ? pageId : null,
+  );
 
   // Dependencies
   if (pageData == null) {
@@ -301,7 +373,10 @@ export const FixPageGrantAlert = (): JSX.Element => {
   if (!hasParent) {
     return <></>;
   }
-  if (dataIsGrantNormalized?.isGrantNormalized == null || dataIsGrantNormalized.isGrantNormalized) {
+  if (
+    dataIsGrantNormalized?.isGrantNormalized == null ||
+    dataIsGrantNormalized.isGrantNormalized
+  ) {
     return <></>;
   }
 
@@ -309,27 +384,31 @@ export const FixPageGrantAlert = (): JSX.Element => {
     <>
       <div className="alert alert-warning py-3 ps-4 d-flex flex-column flex-lg-row">
         <div className="flex-grow-1 d-flex align-items-center">
-          <span className="material-symbols-outlined mx-1" aria-hidden="true">error</span>
+          <span className="material-symbols-outlined mx-1" aria-hidden="true">
+            error
+          </span>
           {t('fix_page_grant.alert.description')}
         </div>
         <div className="d-flex align-items-end align-items-lg-center">
-          <button type="button" className="btn btn-info btn-sm rounded-pill px-3" onClick={() => setOpen(true)}>
+          <button
+            type="button"
+            className="btn btn-info btn-sm rounded-pill px-3"
+            onClick={() => setOpen(true)}
+          >
             {t('fix_page_grant.alert.btn_label')}
           </button>
         </div>
       </div>
 
-      {
-        pageId != null && dataApplicableGrant != null && (
-          <FixPageGrantModal
-            isOpen={isOpen}
-            pageId={pageId}
-            dataApplicableGrant={dataApplicableGrant}
-            currentAndParentPageGrantData={dataIsGrantNormalized.grantData}
-            close={() => setOpen(false)}
-          />
-        )
-      }
+      {pageId != null && dataApplicableGrant != null && (
+        <FixPageGrantModal
+          isOpen={isOpen}
+          pageId={pageId}
+          dataApplicableGrant={dataApplicableGrant}
+          currentAndParentPageGrantData={dataIsGrantNormalized.grantData}
+          close={() => setOpen(false)}
+        />
+      )}
     </>
   );
 };

+ 12 - 6
apps/app/src/components/PageView/PageAlerts/FullTextSearchNotCoverAlert.tsx

@@ -1,26 +1,32 @@
 import type { JSX } from 'react';
 
 import { useTranslation } from 'react-i18next';
-
-import { useElasticsearchMaxBodyLengthToIndex } from '~/stores-universal/context';
 import { useSWRxCurrentPage } from '~/stores/page';
-
+import { useElasticsearchMaxBodyLengthToIndex } from '~/stores-universal/context';
 
 export const FullTextSearchNotCoverAlert = (): JSX.Element => {
   const { t } = useTranslation();
 
-  const { data: elasticsearchMaxBodyLengthToIndex } = useElasticsearchMaxBodyLengthToIndex();
+  const { data: elasticsearchMaxBodyLengthToIndex } =
+    useElasticsearchMaxBodyLengthToIndex();
   const { data } = useSWRxCurrentPage();
 
   const markdownLength = data?.revision?.body?.length;
 
-  if (markdownLength == null || elasticsearchMaxBodyLengthToIndex == null || markdownLength <= elasticsearchMaxBodyLengthToIndex) {
+  if (
+    markdownLength == null ||
+    elasticsearchMaxBodyLengthToIndex == null ||
+    markdownLength <= elasticsearchMaxBodyLengthToIndex
+  ) {
     return <></>;
   }
 
   return (
     <div className="alert alert-warning">
-      <strong>{t('Warning')}: {t('page_page.notice.not_indexed1')}</strong><br />
+      <strong>
+        {t('Warning')}: {t('page_page.notice.not_indexed1')}
+      </strong>
+      <br />
       <small
         // eslint-disable-next-line react/no-danger
         dangerouslySetInnerHTML={{

+ 16 - 6
apps/app/src/components/PageView/PageAlerts/OldRevisionAlert.tsx

@@ -1,10 +1,13 @@
-import React, { useCallback, type JSX } from 'react';
-
 import { returnPathForURL } from '@growi/core/dist/utils/path-utils';
 import { useRouter } from 'next/router';
+import React, { type JSX, useCallback } from 'react';
 import { useTranslation } from 'react-i18next';
 
-import { useSWRxCurrentPage, useSWRMUTxCurrentPage, useIsLatestRevision } from '~/stores/page';
+import {
+  useIsLatestRevision,
+  useSWRMUTxCurrentPage,
+  useSWRxCurrentPage,
+} from '~/stores/page';
 
 export const OldRevisionAlert = (): JSX.Element => {
   const router = useRouter();
@@ -14,7 +17,7 @@ export const OldRevisionAlert = (): JSX.Element => {
   const { data: page } = useSWRxCurrentPage();
   const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
 
-  const onClickShowLatestButton = useCallback(async() => {
+  const onClickShowLatestButton = useCallback(async () => {
     if (page == null) {
       return;
     }
@@ -31,8 +34,15 @@ export const OldRevisionAlert = (): JSX.Element => {
   return (
     <div className="alert alert-warning">
       <strong>{t('Warning')}: </strong> {t('page_page.notice.version')}
-      <button type="button" className="btn btn-outline-natural-secondary" onClick={onClickShowLatestButton}>
-        <span className="material-symbols-outlined me-1">arrow_circle_right</span>{t('Show latest')}
+      <button
+        type="button"
+        className="btn btn-outline-natural-secondary"
+        onClick={onClickShowLatestButton}
+      >
+        <span className="material-symbols-outlined me-1">
+          arrow_circle_right
+        </span>
+        {t('Show latest')}
       </button>
     </div>
   );

+ 21 - 9
apps/app/src/components/PageView/PageAlerts/PageAlerts.tsx

@@ -1,6 +1,5 @@
-import type { JSX } from 'react';
-
 import dynamic from 'next/dynamic';
+import type { JSX } from 'react';
 
 import { useIsNotFound } from '~/stores/page';
 
@@ -9,21 +8,34 @@ import { PageGrantAlert } from './PageGrantAlert';
 import { PageStaleAlert } from './PageStaleAlert';
 import { WipPageAlert } from './WipPageAlert';
 
-
-const FullTextSearchNotCoverAlert = dynamic(() => import('./FullTextSearchNotCoverAlert').then(mod => mod.FullTextSearchNotCoverAlert), { ssr: false });
-const PageRedirectedAlert = dynamic(() => import('./PageRedirectedAlert').then(mod => mod.PageRedirectedAlert), { ssr: false });
-const FixPageGrantAlert = dynamic(() => import('./FixPageGrantAlert').then(mod => mod.FixPageGrantAlert), { ssr: false });
-const TrashPageAlert = dynamic(() => import('./TrashPageAlert').then(mod => mod.TrashPageAlert), { ssr: false });
+const FullTextSearchNotCoverAlert = dynamic(
+  () =>
+    import('./FullTextSearchNotCoverAlert').then(
+      (mod) => mod.FullTextSearchNotCoverAlert,
+    ),
+  { ssr: false },
+);
+const PageRedirectedAlert = dynamic(
+  () => import('./PageRedirectedAlert').then((mod) => mod.PageRedirectedAlert),
+  { ssr: false },
+);
+const FixPageGrantAlert = dynamic(
+  () => import('./FixPageGrantAlert').then((mod) => mod.FixPageGrantAlert),
+  { ssr: false },
+);
+const TrashPageAlert = dynamic(
+  () => import('./TrashPageAlert').then((mod) => mod.TrashPageAlert),
+  { ssr: false },
+);
 
 export const PageAlerts = (): JSX.Element => {
-
   const { data: isNotFound } = useIsNotFound();
 
   return (
     <div className="row d-edit-none">
       <div className="col-sm-12">
         {/* alerts */}
-        { !isNotFound && <FixPageGrantAlert /> }
+        {!isNotFound && <FixPageGrantAlert />}
         <FullTextSearchNotCoverAlert />
         <WipPageAlert />
         <PageGrantAlert />

+ 10 - 10
apps/app/src/components/PageView/PageAlerts/PageGrantAlert.tsx

@@ -1,11 +1,9 @@
-import React, { type JSX } from 'react';
-
 import { isPopulated } from '@growi/core';
+import React, { type JSX } from 'react';
 import { useTranslation } from 'react-i18next';
 
 import { useSWRxCurrentPage } from '~/stores/page';
 
-
 export const PageGrantAlert = (): JSX.Element => {
   const { t } = useTranslation();
   const { data: pageData } = useSWRxCurrentPage();
@@ -15,7 +13,7 @@ export const PageGrantAlert = (): JSX.Element => {
   }
 
   const populatedGrantedGroups = () => {
-    return pageData.grantedGroups.filter(group => isPopulated(group.item));
+    return pageData.grantedGroups.filter((group) => isPopulated(group.item));
   };
 
   const renderAlertContent = () => {
@@ -23,14 +21,16 @@ export const PageGrantAlert = (): JSX.Element => {
       if (pageData.grant === 2) {
         return (
           <>
-            <span className="material-symbols-outlined me-1">link</span><strong>{t('Anyone with the link')}</strong>
+            <span className="material-symbols-outlined me-1">link</span>
+            <strong>{t('Anyone with the link')}</strong>
           </>
         );
       }
       if (pageData.grant === 4) {
         return (
           <>
-            <span className="material-symbols-outlined me-1">lock</span><strong>{t('Only me')}</strong>
+            <span className="material-symbols-outlined me-1">lock</span>
+            <strong>{t('Only me')}</strong>
           </>
         );
       }
@@ -38,9 +38,10 @@ export const PageGrantAlert = (): JSX.Element => {
         return (
           <>
             <span className="material-symbols-outlined me-1">account_tree</span>
-            <strong>{
-              populatedGrantedGroups().map(g => g.item.name).join(', ')
-            }
+            <strong>
+              {populatedGrantedGroups()
+                .map((g) => g.item.name)
+                .join(', ')}
             </strong>
           </>
         );
@@ -53,7 +54,6 @@ export const PageGrantAlert = (): JSX.Element => {
     );
   };
 
-
   return (
     <p data-testid="page-grant-alert" className="alert alert-primary py-3 px-4">
       {renderAlertContent()}

+ 15 - 9
apps/app/src/components/PageView/PageAlerts/PageRedirectedAlert.tsx

@@ -1,6 +1,5 @@
-import React, { useState, useCallback, type JSX } from 'react';
-
 import { useTranslation } from 'next-i18next';
+import React, { type JSX, useCallback, useState } from 'react';
 
 import { useCurrentPagePath } from '~/stores/page';
 import { useRedirectFrom } from '~/stores/page-redirect';
@@ -12,7 +11,7 @@ export const PageRedirectedAlert = React.memo((): JSX.Element => {
 
   const [isUnlinked, setIsUnlinked] = useState(false);
 
-  const unlinkButtonClickHandler = useCallback(async() => {
+  const unlinkButtonClickHandler = useCallback(async () => {
     if (currentPagePath == null) {
       return;
     }
@@ -20,8 +19,7 @@ export const PageRedirectedAlert = React.memo((): JSX.Element => {
       const unlink = (await import('~/client/services/page-operation')).unlink;
       await unlink(currentPagePath);
       setIsUnlinked(true);
-    }
-    catch (err) {
+    } catch (err) {
       const toastError = (await import('~/client/util/toastr')).toastError;
       toastError(err);
     }
@@ -34,7 +32,7 @@ export const PageRedirectedAlert = React.memo((): JSX.Element => {
   if (isUnlinked) {
     return (
       <div className="alert alert-info d-edit-none py-3 px-4">
-        <strong>{ t('Unlinked') }: </strong> { t('page_page.notice.unlinked') }
+        <strong>{t('Unlinked')}: </strong> {t('page_page.notice.unlinked')}
       </div>
     );
   }
@@ -42,10 +40,18 @@ export const PageRedirectedAlert = React.memo((): JSX.Element => {
   return (
     <div className="alert alert-pink d-edit-none py-3 px-4 d-flex align-items-center justify-content-between">
       <span>
-        <strong>{ t('Redirected') }:</strong> { t('page_page.notice.redirected')} <code>{redirectFrom}</code> {t('page_page.notice.redirected_period')}
+        <strong>{t('Redirected')}:</strong> {t('page_page.notice.redirected')}{' '}
+        <code>{redirectFrom}</code> {t('page_page.notice.redirected_period')}
       </span>
-      <button type="button" id="unlink-page-button" onClick={unlinkButtonClickHandler} className="btn btn-outline-dark btn-sm float-end">
-        <span className="material-symbols-outlined" aria-hidden="true">link_off</span>
+      <button
+        type="button"
+        id="unlink-page-button"
+        onClick={unlinkButtonClickHandler}
+        className="btn btn-outline-dark btn-sm float-end"
+      >
+        <span className="material-symbols-outlined" aria-hidden="true">
+          link_off
+        </span>
         {t('unlink_redirection')}
       </button>
     </div>

+ 10 - 10
apps/app/src/components/PageView/PageAlerts/PageStaleAlert.tsx

@@ -1,22 +1,22 @@
-import type { JSX } from 'react';
-
 import { isIPageInfoForEntity } from '@growi/core';
 import { useTranslation } from 'next-i18next';
-
-
-import { useIsEnabledStaleNotification } from '~/stores-universal/context';
+import type { JSX } from 'react';
 import { useSWRxCurrentPage, useSWRxPageInfo } from '~/stores/page';
+import { useIsEnabledStaleNotification } from '~/stores-universal/context';
 
-
-export const PageStaleAlert = ():JSX.Element => {
+export const PageStaleAlert = (): JSX.Element => {
   const { t } = useTranslation();
   const { data: isEnabledStaleNotification } = useIsEnabledStaleNotification();
 
   // Todo: determine if it should fetch or not like useSWRxPageInfo below after https://redmine.weseek.co.jp/issues/96788
   const { data: pageData } = useSWRxCurrentPage();
-  const { data: pageInfo } = useSWRxPageInfo(isEnabledStaleNotification ? pageData?._id : null);
+  const { data: pageInfo } = useSWRxPageInfo(
+    isEnabledStaleNotification ? pageData?._id : null,
+  );
 
-  const contentAge = isIPageInfoForEntity(pageInfo) ? pageInfo.contentAge : null;
+  const contentAge = isIPageInfoForEntity(pageInfo)
+    ? pageInfo.contentAge
+    : null;
 
   if (!isEnabledStaleNotification) {
     return <></>;
@@ -41,7 +41,7 @@ export const PageStaleAlert = ():JSX.Element => {
   return (
     <div className={`alert ${alertClass}`}>
       <span className="material-symbols-outlined me-1">hourglass</span>
-      <strong>{ t('page_page.notice.stale', { count: contentAge }) }</strong>
+      <strong>{t('page_page.notice.stale', { count: contentAge })}</strong>
     </div>
   );
 };

+ 57 - 19
apps/app/src/components/PageView/PageAlerts/TrashPageAlert.tsx

@@ -1,18 +1,20 @@
-import React, { useCallback, type JSX } from 'react';
-
 import { UserPicture } from '@growi/ui/dist/components';
 import { format } from 'date-fns/format';
 import { useRouter } from 'next/router';
+import React, { type JSX, useCallback } from 'react';
 import { useTranslation } from 'react-i18next';
 
 import { usePageDeleteModal, usePutBackPageModal } from '~/stores/modal';
 import {
-  useCurrentPagePath, useSWRxPageInfo, useSWRxCurrentPage, useIsTrashPage, useSWRMUTxCurrentPage,
+  useCurrentPagePath,
+  useIsTrashPage,
+  useSWRMUTxCurrentPage,
+  useSWRxCurrentPage,
+  useSWRxPageInfo,
 } from '~/stores/page';
 import { mutateRecentlyUpdated } from '~/stores/page-listing';
 import { useIsAbleToShowTrashPageManagementButtons } from '~/stores/ui';
 
-
 const onDeletedHandler = (pathOrPathsToDelete) => {
   if (typeof pathOrPathsToDelete !== 'string') {
     return;
@@ -25,7 +27,8 @@ export const TrashPageAlert = (): JSX.Element => {
   const { t } = useTranslation();
   const router = useRouter();
 
-  const { data: isAbleToShowTrashPageManagementButtons } = useIsAbleToShowTrashPageManagementButtons();
+  const { data: isAbleToShowTrashPageManagementButtons } =
+    useIsAbleToShowTrashPageManagementButtons();
   const { data: pageData } = useSWRxCurrentPage();
   const { data: isTrashPage } = useIsTrashPage();
   const pageId = pageData?._id;
@@ -39,7 +42,9 @@ export const TrashPageAlert = (): JSX.Element => {
   const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
 
   const deleteUser = pageData?.deleteUser;
-  const deletedAt = pageData?.deletedAt ? format(new Date(pageData?.deletedAt), 'yyyy/MM/dd HH:mm') : '';
+  const deletedAt = pageData?.deletedAt
+    ? format(new Date(pageData?.deletedAt), 'yyyy/MM/dd HH:mm')
+    : '';
   const revisionId = pageData?.revision?._id;
   const isEmptyPage = pageId == null || revisionId == null || pagePath == null;
 
@@ -48,25 +53,36 @@ export const TrashPageAlert = (): JSX.Element => {
     if (isEmptyPage) {
       return;
     }
-    const putBackedHandler = async() => {
+    const putBackedHandler = async () => {
       if (currentPagePath == null) {
         return;
       }
       try {
-        const unlink = (await import('~/client/services/page-operation')).unlink;
+        const unlink = (await import('~/client/services/page-operation'))
+          .unlink;
         unlink(currentPagePath);
 
         router.push(`/${pageId}`);
         mutateCurrentPage();
         mutateRecentlyUpdated();
-      }
-      catch (err) {
+      } catch (err) {
         const toastError = (await import('~/client/util/toastr')).toastError;
         toastError(err);
       }
     };
-    openPutBackPageModal({ pageId, path: pagePath }, { onPutBacked: putBackedHandler });
-  }, [currentPagePath, mutateCurrentPage, openPutBackPageModal, pageId, pagePath, router, isEmptyPage]);
+    openPutBackPageModal(
+      { pageId, path: pagePath },
+      { onPutBacked: putBackedHandler },
+    );
+  }, [
+    currentPagePath,
+    mutateCurrentPage,
+    openPutBackPageModal,
+    pageId,
+    pagePath,
+    router,
+    isEmptyPage,
+  ]);
 
   const openPageDeleteModalHandler = useCallback(() => {
     // User cannot operate empty page.
@@ -93,7 +109,10 @@ export const TrashPageAlert = (): JSX.Element => {
           onClick={openPutbackPageModalHandler}
           data-testid="put-back-button"
         >
-          <span className="material-symbols-outlined" aria-hidden="true">undo</span> {t('Put Back')}
+          <span className="material-symbols-outlined" aria-hidden="true">
+            undo
+          </span>{' '}
+          {t('Put Back')}
         </button>
         <button
           type="button"
@@ -101,11 +120,19 @@ export const TrashPageAlert = (): JSX.Element => {
           disabled={!(pageInfo?.isAbleToDeleteCompletely ?? false)}
           onClick={openPageDeleteModalHandler}
         >
-          <span className="material-symbols-outlined" aria-hidden="true">delete_forever</span> {t('Delete Completely')}
+          <span className="material-symbols-outlined" aria-hidden="true">
+            delete_forever
+          </span>{' '}
+          {t('Delete Completely')}
         </button>
       </>
     );
-  }, [openPageDeleteModalHandler, openPutbackPageModalHandler, pageInfo?.isAbleToDeleteCompletely, t]);
+  }, [
+    openPageDeleteModalHandler,
+    openPutbackPageModalHandler,
+    pageInfo?.isAbleToDeleteCompletely,
+    t,
+  ]);
 
   // Show this alert only for non-empty pages in trash.
   if (!isTrashPage || isEmptyPage) {
@@ -114,17 +141,28 @@ export const TrashPageAlert = (): JSX.Element => {
 
   return (
     <>
-      <div className="alert alert-warning py-3 ps-4 d-flex flex-column flex-lg-row" data-testid="trash-page-alert">
+      <div
+        className="alert alert-warning py-3 ps-4 d-flex flex-column flex-lg-row"
+        data-testid="trash-page-alert"
+      >
         <div className="flex-grow-1">
-          This page is in the trash <span className="material-symbols-outlined" aria-hidden="true">delete</span>.
+          This page is in the trash{' '}
+          <span className="material-symbols-outlined" aria-hidden="true">
+            delete
+          </span>
+          .
           <br />
           <UserPicture user={deleteUser} />
           <span className="ms-2">
-            Deleted by {deleteUser?.name} at <span data-vrt-blackout-datetime>{deletedAt ?? pageData?.updatedAt}</span>
+            Deleted by {deleteUser?.name} at{' '}
+            <span data-vrt-blackout-datetime>
+              {deletedAt ?? pageData?.updatedAt}
+            </span>
           </span>
         </div>
         <div className="pt-1 d-flex align-items-end align-items-lg-center">
-          {isAbleToShowTrashPageManagementButtons && renderTrashPageManagementButtons()}
+          {isAbleToShowTrashPageManagementButtons &&
+            renderTrashPageManagementButtons()}
         </div>
       </div>
     </>

+ 10 - 10
apps/app/src/components/PageView/PageAlerts/WipPageAlert.tsx

@@ -1,16 +1,15 @@
-import React, { useCallback, type JSX } from 'react';
+import React, { type JSX, useCallback } from 'react';
 
 import { useTranslation } from 'react-i18next';
 
 import { useSWRMUTxCurrentPage, useSWRxCurrentPage } from '~/stores/page';
 
-
 export const WipPageAlert = (): JSX.Element => {
   const { t } = useTranslation();
   const { data: currentPage } = useSWRxCurrentPage();
   const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
 
-  const clickPagePublishButton = useCallback(async() => {
+  const clickPagePublishButton = useCallback(async () => {
     const pageId = currentPage?._id;
 
     if (pageId == null) {
@@ -18,27 +17,28 @@ export const WipPageAlert = (): JSX.Element => {
     }
 
     try {
-      const publish = (await import('~/client/services/page-operation')).publish;
+      const publish = (await import('~/client/services/page-operation'))
+        .publish;
       await publish(pageId);
 
       await mutateCurrentPage();
 
-      const mutatePageTree = (await import('~/stores/page-listing')).mutatePageTree;
+      const mutatePageTree = (await import('~/stores/page-listing'))
+        .mutatePageTree;
       await mutatePageTree();
 
-      const mutateRecentlyUpdated = (await import('~/stores/page-listing')).mutateRecentlyUpdated;
+      const mutateRecentlyUpdated = (await import('~/stores/page-listing'))
+        .mutateRecentlyUpdated;
       await mutateRecentlyUpdated();
 
       const toastSuccess = (await import('~/client/util/toastr')).toastSuccess;
       toastSuccess(t('wip_page.success_publish_page'));
-    }
-    catch {
+    } catch {
       const toastError = (await import('~/client/util/toastr')).toastError;
       toastError(t('wip_page.fail_publish_page'));
     }
   }, [currentPage?._id, mutateCurrentPage, t]);
 
-
   if (!currentPage?.wip) {
     return <></>;
   }
@@ -52,7 +52,7 @@ export const WipPageAlert = (): JSX.Element => {
         className="btn btn-outline-secondary ms-auto"
         onClick={clickPagePublishButton}
       >
-        {t('wip_page.publish_page') }
+        {t('wip_page.publish_page')}
       </button>
     </p>
   );

+ 26 - 14
apps/app/src/components/PageView/PageContentFooter.tsx

@@ -1,34 +1,46 @@
-import type { JSX } from 'react';
-
 import type { IPage, IPagePopulatedToShowRevision } from '@growi/core';
 import dynamic from 'next/dynamic';
+import type { JSX } from 'react';
 
 import styles from './PageContentFooter.module.scss';
 
-
-const AuthorInfo = dynamic(() => import('~/client/components/AuthorInfo').then(mod => mod.AuthorInfo), { ssr: false });
+const AuthorInfo = dynamic(
+  () => import('~/client/components/AuthorInfo').then((mod) => mod.AuthorInfo),
+  { ssr: false },
+);
 
 export type PageContentFooterProps = {
-  page: IPage | IPagePopulatedToShowRevision,
-}
-
-export const PageContentFooter = (props: PageContentFooterProps): JSX.Element => {
+  page: IPage | IPagePopulatedToShowRevision;
+};
 
+export const PageContentFooter = (
+  props: PageContentFooterProps,
+): JSX.Element => {
   const { page } = props;
 
-  const {
-    creator, lastUpdateUser, createdAt, updatedAt,
-  } = page;
+  const { creator, lastUpdateUser, createdAt, updatedAt } = page;
 
   if (page.isEmpty) {
     return <></>;
   }
 
   return (
-    <div className={`${styles['page-content-footer']} my-4 pt-4 d-edit-none d-print-none}`}>
+    <div
+      className={`${styles['page-content-footer']} my-4 pt-4 d-edit-none d-print-none}`}
+    >
       <div className="page-meta">
-        <AuthorInfo user={creator} date={createdAt} mode="create" locate="footer" />
-        <AuthorInfo user={lastUpdateUser} date={updatedAt} mode="update" locate="footer" />
+        <AuthorInfo
+          user={creator}
+          date={createdAt}
+          mode="create"
+          locate="footer"
+        />
+        <AuthorInfo
+          user={lastUpdateUser}
+          date={updatedAt}
+          mode="update"
+          locate="footer"
+        />
       </div>
     </div>
   );

+ 104 - 54
apps/app/src/components/PageView/PageView.tsx

@@ -1,21 +1,20 @@
-import React, {
-  useEffect, useMemo, useRef, useState, type JSX,
-} from 'react';
-
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import { isUsersHomepage } from '@growi/core/dist/utils/page-path-utils';
 import { useSlidesByFrontmatter } from '@growi/presentation/dist/services';
 import dynamic from 'next/dynamic';
+import React, { type JSX, useEffect, useMemo, useRef, useState } from 'react';
 
 import { PagePathNavTitle } from '~/components/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 { useIsNotFound, useSWRxCurrentPage } from '~/stores/page';
+import { useViewOptions } from '~/stores/renderer';
 import {
-  useIsForbidden, useIsIdenticalPath, useIsNotCreatable,
+  useIsForbidden,
+  useIsIdenticalPath,
+  useIsNotCreatable,
 } from '~/stores-universal/context';
-import { useSWRxCurrentPage, useIsNotFound } from '~/stores/page';
-import { useViewOptions } from '~/stores/renderer';
 
 import { UserInfo } from '../User/UserInfo';
 
@@ -24,33 +23,71 @@ import { PageContentFooter } from './PageContentFooter';
 import { PageViewLayout } from './PageViewLayout';
 import RevisionRenderer from './RevisionRenderer';
 
-
-const NotCreatablePage = dynamic(() => import('~/client/components/NotCreatablePage').then(mod => mod.NotCreatablePage), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/client/components/ForbiddenPage'), { ssr: false });
-const NotFoundPage = dynamic(() => import('~/client/components/NotFoundPage'), { ssr: false });
-const PageSideContents = dynamic(() => import('~/client/components/PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
-const PageContentsUtilities = dynamic(() => import('~/client/components/Page/PageContentsUtilities').then(mod => mod.PageContentsUtilities), { ssr: false });
-const Comments = dynamic(() => import('~/client/components/Comments').then(mod => mod.Comments), { ssr: false });
-const UsersHomepageFooter = dynamic(() => import('~/client/components/UsersHomepageFooter')
-  .then(mod => mod.UsersHomepageFooter), { ssr: false });
-const IdenticalPathPage = dynamic(() => import('~/client/components/IdenticalPathPage').then(mod => mod.IdenticalPathPage), { ssr: false });
-const SlideRenderer = dynamic(() => import('~/client/components/Page/SlideRenderer').then(mod => mod.SlideRenderer), { ssr: false });
-
+const NotCreatablePage = dynamic(
+  () =>
+    import('~/client/components/NotCreatablePage').then(
+      (mod) => mod.NotCreatablePage,
+    ),
+  { ssr: false },
+);
+const ForbiddenPage = dynamic(
+  () => import('~/client/components/ForbiddenPage'),
+  { ssr: false },
+);
+const NotFoundPage = dynamic(() => import('~/client/components/NotFoundPage'), {
+  ssr: false,
+});
+const PageSideContents = dynamic(
+  () =>
+    import('~/client/components/PageSideContents').then(
+      (mod) => mod.PageSideContents,
+    ),
+  { ssr: false },
+);
+const PageContentsUtilities = dynamic(
+  () =>
+    import('~/client/components/Page/PageContentsUtilities').then(
+      (mod) => mod.PageContentsUtilities,
+    ),
+  { ssr: false },
+);
+const Comments = dynamic(
+  () => import('~/client/components/Comments').then((mod) => mod.Comments),
+  { ssr: false },
+);
+const UsersHomepageFooter = dynamic(
+  () =>
+    import('~/client/components/UsersHomepageFooter').then(
+      (mod) => mod.UsersHomepageFooter,
+    ),
+  { ssr: false },
+);
+const IdenticalPathPage = dynamic(
+  () =>
+    import('~/client/components/IdenticalPathPage').then(
+      (mod) => mod.IdenticalPathPage,
+    ),
+  { ssr: false },
+);
+const SlideRenderer = dynamic(
+  () =>
+    import('~/client/components/Page/SlideRenderer').then(
+      (mod) => mod.SlideRenderer,
+    ),
+  { ssr: false },
+);
 
 type Props = {
-  pagePath: string,
-  rendererConfig: RendererConfig,
-  initialPage?: IPagePopulatedToShowRevision,
-  className?: string,
-}
+  pagePath: string;
+  rendererConfig: RendererConfig;
+  initialPage?: IPagePopulatedToShowRevision;
+  className?: string;
+};
 
 export const PageView = (props: Props): JSX.Element => {
-
   const commentsContainerRef = useRef<HTMLDivElement>(null);
 
-  const {
-    pagePath, initialPage, rendererConfig, className,
-  } = props;
+  const { pagePath, initialPage, rendererConfig, className } = props;
 
   const { data: isIdenticalPathPage } = useIsIdenticalPath();
   const { data: isForbidden } = useIsForbidden();
@@ -66,11 +103,15 @@ export const PageView = (props: Props): JSX.Element => {
 
   const shouldExpandContent = useShouldExpandContent(page);
 
-
   const markdown = page?.revision?.body;
-  const isSlide = useSlidesByFrontmatter(markdown, rendererConfig.isEnabledMarp);
+  const isSlide = useSlidesByFrontmatter(
+    markdown,
+    rendererConfig.isEnabledMarp,
+  );
 
-  const [currentPageId, setCurrentPageId] = useState<string | undefined>(page?._id);
+  const [currentPageId, setCurrentPageId] = useState<string | undefined>(
+    page?._id,
+  );
 
   useEffect(() => {
     if (page?._id !== undefined) {
@@ -90,7 +131,9 @@ export const PageView = (props: Props): JSX.Element => {
       return;
     }
 
-    const contentContainer = document.getElementById('page-view-content-container');
+    const contentContainer = document.getElementById(
+      'page-view-content-container',
+    );
     if (contentContainer == null) return;
 
     const targetId = decodeURIComponent(hash.slice(1));
@@ -127,24 +170,26 @@ export const PageView = (props: Props): JSX.Element => {
     }
   }, [isForbidden, isIdenticalPathPage, isNotCreatable]);
 
-  const headerContents = <PagePathNavTitle pageId={page?._id} pagePath={pagePath} isWipPage={page?.wip} />;
+  const headerContents = (
+    <PagePathNavTitle
+      pageId={page?._id}
+      pagePath={pagePath}
+      isWipPage={page?.wip}
+    />
+  );
 
-  const sideContents = !isNotFound && !isNotCreatable
-    ? (
-      <PageSideContents page={page} />
-    )
-    : null;
+  const sideContents =
+    !isNotFound && !isNotCreatable ? <PageSideContents page={page} /> : null;
 
-  const footerContents = !isIdenticalPathPage && !isNotFound
-    ? (
+  const footerContents =
+    !isIdenticalPathPage && !isNotFound ? (
       <>
-        {(isUsersHomepagePath && page.creator != null) && (
+        {isUsersHomepagePath && page.creator != null && (
           <UsersHomepageFooter creatorId={page.creator._id} />
         )}
         <PageContentFooter page={page} />
       </>
-    )
-    : null;
+    ) : null;
 
   const Contents = () => {
     if (isNotFound || page?.revision == null) {
@@ -152,20 +197,24 @@ export const PageView = (props: Props): JSX.Element => {
     }
 
     const markdown = page.revision.body;
-    const rendererOptions = viewOptions ?? generateSSRViewOptions(rendererConfig, pagePath);
+    const rendererOptions =
+      viewOptions ?? generateSSRViewOptions(rendererConfig, pagePath);
 
     return (
       <>
         <PageContentsUtilities />
 
         <div className="flex-expand-vert justify-content-between">
-
-          { isSlide != null
-            ? <SlideRenderer marp={isSlide.marp} markdown={markdown} />
-            : <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
-          }
-
-          { !isIdenticalPathPage && !isNotFound && (
+          {isSlide != null ? (
+            <SlideRenderer marp={isSlide.marp} markdown={markdown} />
+          ) : (
+            <RevisionRenderer
+              rendererOptions={rendererOptions}
+              markdown={markdown}
+            />
+          )}
+
+          {!isIdenticalPathPage && !isNotFound && (
             <div id="comments-container" ref={commentsContainerRef}>
               <Comments
                 pageId={page._id}
@@ -173,7 +222,7 @@ export const PageView = (props: Props): JSX.Element => {
                 revision={page.revision}
               />
             </div>
-          ) }
+          )}
         </div>
       </>
     );
@@ -192,13 +241,14 @@ export const PageView = (props: Props): JSX.Element => {
       {specialContents}
       {specialContents == null && (
         <>
-          {(isUsersHomepagePath && page?.creator != null) && <UserInfo author={page.creator} />}
+          {isUsersHomepagePath && page?.creator != null && (
+            <UserInfo author={page.creator} />
+          )}
           <div id="page-view-content-container" className="flex-expand-vert">
             <Contents />
           </div>
         </>
       )}
-
     </PageViewLayout>
   );
 };

+ 36 - 31
apps/app/src/components/PageView/PageViewLayout.tsx

@@ -1,4 +1,4 @@
-import type { ReactNode, JSX } from 'react';
+import type { JSX, ReactNode } from 'react';
 
 import styles from './PageViewLayout.module.scss';
 
@@ -6,18 +6,21 @@ const pageViewLayoutClass = styles['page-view-layout'] ?? '';
 const _fluidLayoutClass = styles['fluid-layout'] ?? '';
 
 type Props = {
-  className?: string,
-  children?: ReactNode,
-  headerContents?: ReactNode,
-  sideContents?: ReactNode,
-  footerContents?: ReactNode,
-  expandContentWidth?: boolean,
-}
+  className?: string;
+  children?: ReactNode;
+  headerContents?: ReactNode;
+  sideContents?: ReactNode;
+  footerContents?: ReactNode;
+  expandContentWidth?: boolean;
+};
 
 export const PageViewLayout = (props: Props): JSX.Element => {
   const {
     className,
-    children, headerContents, sideContents, footerContents,
+    children,
+    headerContents,
+    sideContents,
+    footerContents,
     expandContentWidth,
   } = props;
 
@@ -25,36 +28,38 @@ export const PageViewLayout = (props: Props): JSX.Element => {
 
   return (
     <>
-      <div className={`main ${className} ${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
-            ? (
-              <div className="flex-expand-horiz gap-3 z-0">
-                <div className="flex-expand-vert flex-basis-0 mw-0">
-                  {children}
-                </div>
-                <div className="grw-side-contents-container col-lg-3  d-edit-none d-print-none" data-vrt-blackout-side-contents>
-                  <div className="grw-side-contents-sticky-container">
-                    {sideContents}
-                  </div>
-                </div>
-              </div>
-            )
-            : (
-              <div className="z-0">
+          {headerContents != null && headerContents}
+          {sideContents != null ? (
+            <div className="flex-expand-horiz gap-3 z-0">
+              <div className="flex-expand-vert flex-basis-0 mw-0">
                 {children}
               </div>
-            )
-          }
+              <div
+                className="grw-side-contents-container col-lg-3  d-edit-none d-print-none"
+                data-vrt-blackout-side-contents
+              >
+                <div className="grw-side-contents-sticky-container">
+                  {sideContents}
+                </div>
+              </div>
+            </div>
+          ) : (
+            <div className="z-0">{children}</div>
+          )}
         </div>
       </div>
 
-      { footerContents != null && (
-        <footer className={`footer d-edit-none container-lg wide-gutter-x-lg ${fluidLayoutClass}`}>
+      {footerContents != null && (
+        <footer
+          className={`footer d-edit-none container-lg wide-gutter-x-lg ${fluidLayoutClass}`}
+        >
           {footerContents}
         </footer>
-      ) }
+      )}
     </>
   );
 };

+ 23 - 20
apps/app/src/components/PageView/RevisionRenderer.tsx

@@ -9,31 +9,35 @@ import loggerFactory from '~/utils/logger';
 
 import 'katex/dist/katex.min.css';
 
-
 const logger = loggerFactory('components:Page:RevisionRenderer');
 
 type Props = {
-  rendererOptions: RendererOptions,
-  markdown: string,
-  additionalClassName?: string,
-}
-
-const ErrorFallback: React.FC<FallbackProps> = React.memo(({ error, resetErrorBoundary }) => {
-  return (
-    <div role="alert">
-      <p>Something went wrong:</p>
-      <pre>{error.message}</pre>
-      <button type="button" className="btn btn-primary" onClick={resetErrorBoundary}>Reload</button>
-    </div>
-  );
-});
+  rendererOptions: RendererOptions;
+  markdown: string;
+  additionalClassName?: string;
+};
+
+const ErrorFallback: React.FC<FallbackProps> = React.memo(
+  ({ error, resetErrorBoundary }) => {
+    return (
+      <div role="alert">
+        <p>Something went wrong:</p>
+        <pre>{error.message}</pre>
+        <button
+          type="button"
+          className="btn btn-primary"
+          onClick={resetErrorBoundary}
+        >
+          Reload
+        </button>
+      </div>
+    );
+  },
+);
 ErrorFallback.displayName = 'ErrorFallback';
 
 const RevisionRenderer = React.memo((props: Props): JSX.Element => {
-
-  const {
-    rendererOptions, markdown, additionalClassName,
-  } = props;
+  const { rendererOptions, markdown, additionalClassName } = props;
 
   return (
     <ErrorBoundary FallbackComponent={ErrorFallback}>
@@ -45,7 +49,6 @@ const RevisionRenderer = React.memo((props: Props): JSX.Element => {
       </ReactMarkdown>
     </ErrorBoundary>
   );
-
 });
 RevisionRenderer.displayName = 'RevisionRenderer';
 

+ 45 - 25
apps/app/src/components/ReactMarkdownComponents/CodeBlock.tsx

@@ -1,4 +1,4 @@
-import type { ReactNode, JSX } from 'react';
+import type { JSX, ReactNode } from 'react';
 
 import { PrismAsyncLight } from 'react-syntax-highlighter';
 import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
@@ -12,20 +12,21 @@ Object.entries<object>(oneDark).forEach(([key, value]) => {
   }
 });
 
-
 type InlineCodeBlockProps = {
-  children: ReactNode,
-  className?: string,
-}
+  children: ReactNode;
+  className?: string;
+};
 
 const InlineCodeBlockSubstance = (props: InlineCodeBlockProps): JSX.Element => {
   const { children, className, ...rest } = props;
-  return <code className={`code-inline ${className ?? ''}`} {...rest}>{children}</code>;
+  return (
+    <code className={`code-inline ${className ?? ''}`} {...rest}>
+      {children}
+    </code>
+  );
 };
 
-
 function extractChildrenToIgnoreReactNode(children: ReactNode): ReactNode {
-
   if (children == null) {
     return children;
   }
@@ -37,30 +38,46 @@ function extractChildrenToIgnoreReactNode(children: ReactNode): ReactNode {
 
   // Multiple element array
   if (Array.isArray(children) && children.length > 1) {
-    return children.map(node => extractChildrenToIgnoreReactNode(node)).join('');
+    return children
+      .map((node) => extractChildrenToIgnoreReactNode(node))
+      .join('');
   }
 
   // object
   if (typeof children === 'object') {
-    const grandChildren = (children as any).children ?? (children as any).props.children;
+    const grandChildren =
+      (children as any).children ?? (children as any).props.children;
     return extractChildrenToIgnoreReactNode(grandChildren);
   }
 
   return String(children).replace(/\n$/, '');
 }
 
-function CodeBlockSubstance({ lang, children }: { lang: string, children: ReactNode }): JSX.Element {
+function CodeBlockSubstance({
+  lang,
+  children,
+}: {
+  lang: string;
+  children: ReactNode;
+}): JSX.Element {
   // return alternative element
   //   in order to fix "CodeBlock string is be [object Object] if searched"
   // see: https://github.com/growilabs/growi/pull/7484
   //
   // Note: You can also remove this code if the user requests to see the code highlighted in Prism as-is.
 
-  const isSimpleString = typeof children === 'string' || (Array.isArray(children) && children.length === 1 && typeof children[0] === 'string');
+  const isSimpleString =
+    typeof children === 'string' ||
+    (Array.isArray(children) &&
+      children.length === 1 &&
+      typeof children[0] === 'string');
   if (!isSimpleString) {
     return (
       <div style={oneDark['pre[class*="language-"]']}>
-        <code className={`language-${lang}`} style={oneDark['code[class*="language-"]']}>
+        <code
+          className={`language-${lang}`}
+          style={oneDark['code[class*="language-"]']}
+        >
           {children}
         </code>
       </div>
@@ -68,28 +85,27 @@ function CodeBlockSubstance({ lang, children }: { lang: string, children: ReactN
   }
 
   return (
-    <PrismAsyncLight
-      PreTag="div"
-      style={oneDark}
-      language={lang}
-    >
+    <PrismAsyncLight PreTag="div" style={oneDark} language={lang}>
       {extractChildrenToIgnoreReactNode(children)}
     </PrismAsyncLight>
   );
 }
 
 type CodeBlockProps = {
-  children: ReactNode,
-  className?: string,
-  inline?: true,
-}
+  children: ReactNode;
+  className?: string;
+  inline?: true;
+};
 
 export const CodeBlock = (props: CodeBlockProps): JSX.Element => {
-
   // TODO: set border according to the value of 'customize:highlightJsStyleBorder'
   const { className, children, inline } = props;
   if (inline) {
-    return <InlineCodeBlockSubstance className={`code-inline ${className ?? ''}`}>{children}</InlineCodeBlockSubstance>;
+    return (
+      <InlineCodeBlockSubstance className={`code-inline ${className ?? ''}`}>
+        {children}
+      </InlineCodeBlockSubstance>
+    );
   }
 
   const match = /language-(\w+)(:?.+)?/.exec(className || '');
@@ -99,7 +115,11 @@ export const CodeBlock = (props: CodeBlockProps): JSX.Element => {
   return (
     <>
       {name != null && (
-        <cite className={`code-highlighted-title ${styles['code-highlighted-title']}`}>{name}</cite>
+        <cite
+          className={`code-highlighted-title ${styles['code-highlighted-title']}`}
+        >
+          {name}
+        </cite>
       )}
       <CodeBlockSubstance lang={lang}>{children}</CodeBlockSubstance>
     </>

+ 36 - 18
apps/app/src/components/ReactMarkdownComponents/NextLink.tsx

@@ -1,13 +1,11 @@
-import type { JSX } from 'react';
-
 import { pagePathUtils } from '@growi/core/dist/utils';
 import type { LinkProps } from 'next/link';
 import Link from 'next/link';
+import type { JSX } from 'react';
 
 import { useSiteUrl } from '~/stores-universal/context';
 import loggerFactory from '~/utils/logger';
 
-
 const logger = loggerFactory('growi:components:NextLink');
 
 const isAnchorLink = (href: string): boolean => {
@@ -19,8 +17,7 @@ const isExternalLink = (href: string, siteUrl: string | undefined): boolean => {
     const baseUrl = new URL(siteUrl ?? 'https://example.com');
     const hrefUrl = new URL(href, baseUrl);
     return baseUrl.host !== hrefUrl.host;
-  }
-  catch (err) {
+  } catch (err) {
     logger.debug(err);
     return false;
   }
@@ -31,24 +28,21 @@ const isCreatablePage = (href: string) => {
     const url = new URL(href, 'http://example.com');
     const pathName = url.pathname;
     return pagePathUtils.isCreatablePage(pathName);
-  }
-  catch (err) {
+  } catch (err) {
     logger.debug(err);
     return false;
   }
 };
 
 type Props = Omit<LinkProps, 'href'> & {
-  children: React.ReactNode,
-  id?: string,
-  href?: string,
-  className?: string,
+  children: React.ReactNode;
+  id?: string;
+  href?: string;
+  className?: string;
 };
 
 export const NextLink = (props: Props): JSX.Element => {
-  const {
-    id, href, children, className, onClick, ...rest
-  } = props;
+  const { id, href, children, className, onClick, ...rest } = props;
 
   const { data: siteUrl } = useSiteUrl();
 
@@ -63,8 +57,17 @@ export const NextLink = (props: Props): JSX.Element => {
 
   if (isExternalLink(href, siteUrl)) {
     return (
-      <a id={id} href={href} className={className} target="_blank" onClick={onClick} rel="noopener noreferrer" {...dataAttributes}>
-        {children}&nbsp;<span className="growi-custom-icons">external_link</span>
+      <a
+        id={id}
+        href={href}
+        className={className}
+        target="_blank"
+        onClick={onClick}
+        rel="noopener noreferrer"
+        {...dataAttributes}
+      >
+        {children}&nbsp;
+        <span className="growi-custom-icons">external_link</span>
       </a>
     );
   }
@@ -72,13 +75,28 @@ export const NextLink = (props: Props): JSX.Element => {
   // when href is an anchor link or not-creatable path
   if (isAnchorLink(href) || !isCreatablePage(href)) {
     return (
-      <a id={id} href={href} className={className} onClick={onClick} {...dataAttributes}>{children}</a>
+      <a
+        id={id}
+        href={href}
+        className={className}
+        onClick={onClick}
+        {...dataAttributes}
+      >
+        {children}
+      </a>
     );
   }
 
   return (
     <Link {...rest} href={href} prefetch={false} legacyBehavior>
-      <a href={href} className={className} {...dataAttributes} onClick={onClick}>{children}</a>
+      <a
+        href={href}
+        className={className}
+        {...dataAttributes}
+        onClick={onClick}
+      >
+        {children}
+      </a>
     </Link>
   );
 };

+ 2 - 3
apps/app/src/components/Script/DrawioViewerScript/DrawioViewerScript.tsx

@@ -1,7 +1,6 @@
-import { useCallback, type JSX } from 'react';
-
 import type { IGraphViewerGlobal } from '@growi/remark-drawio';
 import Head from 'next/head';
+import { type JSX, useCallback } from 'react';
 
 import { useViewerMinJsUrl } from './use-viewer-min-js-url';
 
@@ -12,7 +11,7 @@ declare global {
 
 type Props = {
   drawioUri: string;
-}
+};
 
 export const DrawioViewerScript = ({ drawioUri }: Props): JSX.Element => {
   const viewerMinJsSrc = useViewerMinJsUrl(drawioUri);

+ 14 - 11
apps/app/src/components/Script/DrawioViewerScript/use-viewer-min-js-url.spec.ts

@@ -2,16 +2,19 @@ import { useViewerMinJsUrl } from './use-viewer-min-js-url';
 
 describe('useViewerMinJsUrl', () => {
   it.each`
-    drawioUri                                     | expected
-    ${'http://localhost:8080'}                    | ${'http://localhost:8080/js/viewer-static.min.js'}
-    ${'http://example.com'}                       | ${'http://example.com/js/viewer-static.min.js'}
-    ${'http://example.com/drawio'}                | ${'http://example.com/drawio/js/viewer-static.min.js'}
-    ${'http://example.com/?offline=1&https=0'}    | ${'http://example.com/js/viewer-static.min.js?offline=1&https=0'}
-  `('should return the expected URL "$expected" when drawioUri is "$drawioUrk"', ({ drawioUri, expected }: {drawioUri: string, expected: string}) => {
-    // Act
-    const url = useViewerMinJsUrl(drawioUri);
+    drawioUri                                  | expected
+    ${'http://localhost:8080'}                 | ${'http://localhost:8080/js/viewer-static.min.js'}
+    ${'http://example.com'}                    | ${'http://example.com/js/viewer-static.min.js'}
+    ${'http://example.com/drawio'}             | ${'http://example.com/drawio/js/viewer-static.min.js'}
+    ${'http://example.com/?offline=1&https=0'} | ${'http://example.com/js/viewer-static.min.js?offline=1&https=0'}
+  `(
+    'should return the expected URL "$expected" when drawioUri is "$drawioUrk"',
+    ({ drawioUri, expected }: { drawioUri: string; expected: string }) => {
+      // Act
+      const url = useViewerMinJsUrl(drawioUri);
 
-    // Assert
-    expect(url).toBe(expected);
-  });
+      // Assert
+      expect(url).toBe(expected);
+    },
+  );
 });

+ 18 - 15
apps/app/src/components/ShareLinkPageView/ShareLinkAlert.tsx

@@ -1,10 +1,10 @@
+import { useTranslation } from 'next-i18next';
 import type { FC } from 'react';
 import React from 'react';
 
-import { useTranslation } from 'next-i18next';
-
 const generateRatio = (expiredAt: Date, createdAt: Date): number => {
-  const wholeTime = new Date(expiredAt).getTime() - new Date(createdAt).getTime();
+  const wholeTime =
+    new Date(expiredAt).getTime() - new Date(createdAt).getTime();
   const remainingTime = new Date(expiredAt).getTime() - new Date().getTime();
   return remainingTime / wholeTime;
 };
@@ -14,23 +14,20 @@ const getAlertColor = (ratio: number): string => {
 
   if (ratio >= 0.75) {
     color = 'success';
-  }
-  else if (ratio < 0.75 && ratio >= 0.5) {
+  } else if (ratio < 0.75 && ratio >= 0.5) {
     color = 'info';
-  }
-  else if (ratio < 0.5 && ratio >= 0.25) {
+  } else if (ratio < 0.5 && ratio >= 0.25) {
     color = 'warning';
-  }
-  else {
+  } else {
     color = 'danger';
   }
   return color;
 };
 
 type Props = {
-  createdAt: Date,
-  expiredAt?: Date,
-}
+  createdAt: Date;
+  expiredAt?: Date;
+};
 
 const ShareLinkAlert: FC<Props> = (props: Props) => {
   const { t } = useTranslation();
@@ -42,9 +39,15 @@ const ShareLinkAlert: FC<Props> = (props: Props) => {
   return (
     <p className={`alert alert-${alertColor} px-4 d-edit-none`}>
       <span className="material-symbols-outlined me-1">link</span>
-      {(expiredAt == null ? <span>{t('page_page.notice.no_deadline')}</span>
-      // eslint-disable-next-line react/no-danger
-        : <span dangerouslySetInnerHTML={{ __html: t('page_page.notice.expiration', { expiredAt }) }} />
+      {expiredAt == null ? (
+        <span>{t('page_page.notice.no_deadline')}</span>
+      ) : (
+        // eslint-disable-next-line react/no-danger
+        <span
+          dangerouslySetInnerHTML={{
+            __html: t('page_page.notice.expiration', { expiredAt }),
+          }}
+        />
       )}
     </p>
   );

+ 69 - 44
apps/app/src/components/ShareLinkPageView/ShareLinkPageView.tsx

@@ -1,8 +1,7 @@
-import { useMemo, type JSX } from 'react';
-
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import { useSlidesByFrontmatter } from '@growi/presentation/dist/services';
 import dynamic from 'next/dynamic';
+import { type JSX, useMemo } from 'react';
 
 import { PagePathNavTitle } from '~/components/Common/PagePathNavTitle';
 import type { RendererConfig } from '~/interfaces/services/renderer';
@@ -19,28 +18,44 @@ import RevisionRenderer from '../PageView/RevisionRenderer';
 
 import ShareLinkAlert from './ShareLinkAlert';
 
-
 const logger = loggerFactory('growi:Page');
 
-
-const PageSideContents = dynamic(() => import('~/client/components/PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/client/components/ForbiddenPage'), { ssr: false });
-const SlideRenderer = dynamic(() => import('~/client/components/Page/SlideRenderer').then(mod => mod.SlideRenderer), { ssr: false });
+const PageSideContents = dynamic(
+  () =>
+    import('~/client/components/PageSideContents').then(
+      (mod) => mod.PageSideContents,
+    ),
+  { ssr: false },
+);
+const ForbiddenPage = dynamic(
+  () => import('~/client/components/ForbiddenPage'),
+  { ssr: false },
+);
+const SlideRenderer = dynamic(
+  () =>
+    import('~/client/components/Page/SlideRenderer').then(
+      (mod) => mod.SlideRenderer,
+    ),
+  { ssr: false },
+);
 
 type Props = {
-  pagePath: string,
-  rendererConfig: RendererConfig,
-  page?: IPagePopulatedToShowRevision,
-  shareLink?: IShareLinkHasId,
-  isExpired: boolean,
-  disableLinkSharing: boolean,
-}
+  pagePath: string;
+  rendererConfig: RendererConfig;
+  page?: IPagePopulatedToShowRevision;
+  shareLink?: IShareLinkHasId;
+  isExpired: boolean;
+  disableLinkSharing: boolean;
+};
 
 export const ShareLinkPageView = (props: Props): JSX.Element => {
   const {
-    pagePath, rendererConfig,
-    page, shareLink,
-    isExpired, disableLinkSharing,
+    pagePath,
+    rendererConfig,
+    page,
+    shareLink,
+    isExpired,
+    disableLinkSharing,
   } = props;
 
   const { data: isNotFoundMeta } = useIsNotFound();
@@ -51,7 +66,10 @@ export const ShareLinkPageView = (props: Props): JSX.Element => {
 
   const markdown = page?.revision?.body;
 
-  const isSlide = useSlidesByFrontmatter(markdown, rendererConfig.isEnabledMarp);
+  const isSlide = useSlidesByFrontmatter(
+    markdown,
+    rendererConfig.isEnabledMarp,
+  );
 
   const isNotFound = isNotFoundMeta || page == null || shareLink == null;
 
@@ -61,20 +79,17 @@ export const ShareLinkPageView = (props: Props): JSX.Element => {
     }
   }, [disableLinkSharing, props.disableLinkSharing]);
 
-  const headerContents = <PagePathNavTitle pageId={page?._id} pagePath={pagePath} isWipPage={page?.wip} />;
-
-  const sideContents = !isNotFound
-    ? (
-      <PageSideContents page={page} />
-    )
-    : null;
+  const headerContents = (
+    <PagePathNavTitle
+      pageId={page?._id}
+      pagePath={pagePath}
+      isWipPage={page?.wip}
+    />
+  );
 
+  const sideContents = !isNotFound ? <PageSideContents page={page} /> : null;
 
-  const footerContents = !isNotFound
-    ? (
-      <PageContentFooter page={page} />
-    )
-    : null;
+  const footerContents = !isNotFound ? <PageContentFooter page={page} /> : null;
 
   const Contents = () => {
     if (isNotFound || page.revision == null) {
@@ -85,19 +100,24 @@ export const ShareLinkPageView = (props: Props): JSX.Element => {
       return (
         <>
           <h2 className="text-muted mt-4">
-            <span className="material-symbols-outlined" aria-hidden="true">block</span>
+            <span className="material-symbols-outlined" aria-hidden="true">
+              block
+            </span>
             <span> Page is expired</span>
           </h2>
         </>
       );
     }
 
-    const rendererOptions = viewOptions ?? generateSSRViewOptions(rendererConfig, pagePath);
+    const rendererOptions =
+      viewOptions ?? generateSSRViewOptions(rendererConfig, pagePath);
     const markdown = page.revision.body;
 
-    return isSlide != null
-      ? <SlideRenderer marp={isSlide.marp} markdown={markdown} />
-      : <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />;
+    return isSlide != null ? (
+      <SlideRenderer marp={isSlide.marp} markdown={markdown} />
+    ) : (
+      <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
+    );
   };
 
   return (
@@ -107,25 +127,30 @@ export const ShareLinkPageView = (props: Props): JSX.Element => {
       expandContentWidth={shouldExpandContent}
       footerContents={footerContents}
     >
-      { specialContents }
-      { specialContents == null && (
+      {specialContents}
+      {specialContents == null && (
         <>
-          { isNotFound && (
+          {isNotFound && (
             <h2 className="text-muted mt-4">
-              <span className="material-symbols-outlined" aria-hidden="true">block</span>
+              <span className="material-symbols-outlined" aria-hidden="true">
+                block
+              </span>
               <span> Page is not found</span>
             </h2>
-          ) }
-          { !isNotFound && (
+          )}
+          {!isNotFound && (
             <>
-              <ShareLinkAlert expiredAt={shareLink.expiredAt} createdAt={shareLink.createdAt} />
+              <ShareLinkAlert
+                expiredAt={shareLink.expiredAt}
+                createdAt={shareLink.createdAt}
+              />
               <div className="mb-5">
                 <Contents />
               </div>
             </>
-          ) }
+          )}
         </>
-      ) }
+      )}
     </PageViewLayout>
   );
 };

+ 1 - 5
apps/app/src/components/User/UserDate.jsx

@@ -1,8 +1,6 @@
-import React from 'react';
-
 import { format } from 'date-fns/format';
 import PropTypes from 'prop-types';
-
+import React from 'react';
 
 /**
  * UserDate
@@ -10,7 +8,6 @@ import PropTypes from 'prop-types';
  * display date depends on user timezone of user settings
  */
 export default class UserDate extends React.Component {
-
   render() {
     const date = new Date(this.props.dateTime);
     const dt = format(date, this.props.format);
@@ -21,7 +18,6 @@ export default class UserDate extends React.Component {
       </span>
     );
   }
-
 }
 
 UserDate.propTypes = {

+ 18 - 19
apps/app/src/components/User/UserInfo.tsx

@@ -1,17 +1,14 @@
-import React, { type JSX } from 'react';
-
 import type { IUserHasId } from '@growi/core';
 import { UserPicture } from '@growi/ui/dist/components';
+import React, { type JSX } from 'react';
 
 import styles from './UserInfo.module.scss';
 
-
 export type UserInfoProps = {
-  author?: IUserHasId,
-}
+  author?: IUserHasId;
+};
 
 export const UserInfo = (props: UserInfoProps): JSX.Element => {
-
   const { author } = props;
 
   if (author == null || author.status === 4) {
@@ -19,29 +16,31 @@ export const UserInfo = (props: UserInfoProps): JSX.Element => {
   }
 
   return (
-    <div className={`${styles['grw-users-info']} grw-users-info d-flex align-items-center d-edit-none mb-5 pb-3 border-bottom`} data-testid="grw-users-info">
+    <div
+      className={`${styles['grw-users-info']} grw-users-info d-flex align-items-center d-edit-none mb-5 pb-3 border-bottom`}
+      data-testid="grw-users-info"
+    >
       <UserPicture user={author} noTooltip noLink />
       <div className="users-meta">
-        <h1 className="user-page-name">
-          {author.name}
-        </h1>
+        <h1 className="user-page-name">{author.name}</h1>
         <div className="user-page-meta mt-3 mb-0">
           <span className="user-page-username me-4">
-            <span className="user-page-username me-4"><span className="material-symbols-outlined">person</span>{author.username}</span>
+            <span className="user-page-username me-4">
+              <span className="material-symbols-outlined">person</span>
+              {author.username}
+            </span>
           </span>
           <span className="user-page-email me-2">
             <span className="material-symbols-outlined me-1">mail</span>
-            { author.isEmailPublished
-              ? author.email
-              : '*****'
-            }
+            {author.isEmailPublished ? author.email : '*****'}
           </span>
-          { author.introduction && (
-            <span className="user-page-introduction">{author.introduction}</span>
-          ) }
+          {author.introduction && (
+            <span className="user-page-introduction">
+              {author.introduction}
+            </span>
+          )}
         </div>
       </div>
     </div>
   );
-
 };

+ 6 - 5
apps/app/src/components/User/Username.tsx

@@ -1,12 +1,13 @@
-import React, { type JSX } from 'react';
-
 import type { IUserHasId } from '@growi/core';
-import { isPopulated, type IUser, type Ref } from '@growi/core';
+import { type IUser, isPopulated, type Ref } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import Link from 'next/link';
+import type React from 'react';
+import type { JSX } from 'react';
 
-export const Username: React.FC<{ user?: IUserHasId | Ref<IUser> }> = ({ user }): JSX.Element => {
-
+export const Username: React.FC<{ user?: IUserHasId | Ref<IUser> }> = ({
+  user,
+}): JSX.Element => {
   if (user == null || !isPopulated(user)) {
     return <i>(anyone)</i>;
   }

+ 0 - 1
biome.json

@@ -29,7 +29,6 @@
       "!apps/app/playwright/**",
       "!apps/app/public/**",
       "!apps/app/src/client/**",
-      "!apps/app/src/components/**",
       "!apps/app/src/features/openai/**",
       "!apps/app/src/models/**",
       "!apps/app/src/pages/**",