2
0
Эх сурвалжийг харах

Merge remote-tracking branch 'origin/master' into support/use-jotai

Yuki Takei 5 сар өмнө
parent
commit
45dc371910
44 өөрчлөгдсөн 1469 нэмэгдсэн , 849 устгасан
  1. 1 0
      apps/app/.eslintrc.js
  2. 1 1
      apps/app/src/client/components/Admin/App/FileUploadSetting.tsx
  3. 9 4
      apps/app/src/client/components/Admin/App/MailSetting.tsx
  4. 1 1
      apps/app/src/client/components/Admin/App/SesSetting.tsx
  5. 1 1
      apps/app/src/client/components/Admin/App/SmtpSetting.tsx
  6. 2 1
      apps/app/src/client/components/Admin/Common/AdminUpdateButtonRow.tsx
  7. 270 94
      apps/app/src/components/Admin/Common/AdminNavigation.tsx
  8. 1 0
      apps/app/src/components/Common/GrowiLogo.jsx
  9. 97 73
      apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.tsx
  10. 23 12
      apps/app/src/components/Common/PagePathNav/PagePathNav.tsx
  11. 30 26
      apps/app/src/components/Common/PagePathNav/PagePathNavLayout.tsx
  12. 8 3
      apps/app/src/components/Common/PagePathNav/Separator.tsx
  13. 36 22
      apps/app/src/components/Common/PagePathNavTitle/PagePathNavTitle.tsx
  14. 1 1
      apps/app/src/components/FontFamily/GlobalFonts.tsx
  15. 27 26
      apps/app/src/components/Layout/AdminLayout.tsx
  16. 110 37
      apps/app/src/components/Layout/BasicLayout.tsx
  17. 15 20
      apps/app/src/components/Layout/NoLoginLayout.tsx
  18. 16 11
      apps/app/src/components/Layout/RawLayout.tsx
  19. 4 5
      apps/app/src/components/Layout/SearchResultLayout.tsx
  20. 24 14
      apps/app/src/components/Layout/ShareLinkLayout.tsx
  21. 4 1
      apps/app/src/components/Navbar/GroundGlassBar.tsx
  22. 204 130
      apps/app/src/components/PageView/PageAlerts/FixPageGrantAlert.tsx
  23. 15 7
      apps/app/src/components/PageView/PageAlerts/FullTextSearchNotCoverAlert.tsx
  24. 17 6
      apps/app/src/components/PageView/PageAlerts/OldRevisionAlert.tsx
  25. 21 9
      apps/app/src/components/PageView/PageAlerts/PageAlerts.tsx
  26. 10 10
      apps/app/src/components/PageView/PageAlerts/PageGrantAlert.tsx
  27. 15 9
      apps/app/src/components/PageView/PageAlerts/PageRedirectedAlert.tsx
  28. 15 10
      apps/app/src/components/PageView/PageAlerts/PageStaleAlert.tsx
  29. 63 27
      apps/app/src/components/PageView/PageAlerts/TrashPageAlert.tsx
  30. 10 11
      apps/app/src/components/PageView/PageAlerts/WipPageAlert.tsx
  31. 26 14
      apps/app/src/components/PageView/PageContentFooter.tsx
  32. 112 54
      apps/app/src/components/PageView/PageView.tsx
  33. 36 31
      apps/app/src/components/PageView/PageViewLayout.tsx
  34. 23 21
      apps/app/src/components/PageView/RevisionRenderer.tsx
  35. 45 26
      apps/app/src/components/ReactMarkdownComponents/CodeBlock.tsx
  36. 37 18
      apps/app/src/components/ReactMarkdownComponents/NextLink.tsx
  37. 2 3
      apps/app/src/components/Script/DrawioViewerScript/DrawioViewerScript.tsx
  38. 14 11
      apps/app/src/components/Script/DrawioViewerScript/use-viewer-min-js-url.spec.ts
  39. 18 15
      apps/app/src/components/ShareLinkPageView/ShareLinkAlert.tsx
  40. 80 54
      apps/app/src/components/ShareLinkPageView/ShareLinkPageView.tsx
  41. 1 5
      apps/app/src/components/User/UserDate.jsx
  42. 18 19
      apps/app/src/components/User/UserInfo.tsx
  43. 6 5
      apps/app/src/components/User/Username.tsx
  44. 0 1
      biome.json

+ 1 - 0
apps/app/.eslintrc.js

@@ -50,6 +50,7 @@ module.exports = {
     'src/stores-universal/**',
     'src/stores-universal/**',
     'src/interfaces/**',
     'src/interfaces/**',
     'src/utils/**',
     'src/utils/**',
+    'src/components/**',
     'src/services/**',
     'src/services/**',
     'src/states/**',
     'src/states/**',
   ],
   ],

+ 1 - 1
apps/app/src/client/components/Admin/App/FileUploadSetting.tsx

@@ -337,7 +337,7 @@ const FileUploadSetting = (props: FileUploadSettingProps): JSX.Element => {
         onChangeAzureStorageAccountName={onChangeAzureStorageAccountNameHandler}
         onChangeAzureStorageAccountName={onChangeAzureStorageAccountNameHandler}
         onChangeAzureStorageContainerName={onChangeAzureStorageContainerNameHandler}
         onChangeAzureStorageContainerName={onChangeAzureStorageContainerNameHandler}
       />
       />
-      <AdminUpdateButtonRow disabled={retrieveError != null} />
+      <AdminUpdateButtonRow type="submit" disabled={retrieveError != null} />
     </form>
     </form>
   );
   );
 };
 };

+ 9 - 4
apps/app/src/client/components/Admin/App/MailSetting.tsx

@@ -27,8 +27,12 @@ const MailSetting = (props: Props) => {
     register,
     register,
     handleSubmit,
     handleSubmit,
     reset,
     reset,
+    watch,
   } = useForm();
   } = useForm();
 
 
+  // Watch the transmission method to dynamically switch between SMTP and SES settings
+  const currentTransmissionMethod = watch('transmissionMethod', adminAppContainer.state.transmissionMethod || 'smtp');
+
   // Reset form when adminAppContainer state changes
   // Reset form when adminAppContainer state changes
   useEffect(() => {
   useEffect(() => {
     reset({
     reset({
@@ -92,7 +96,7 @@ const MailSetting = (props: Props) => {
       {!adminAppContainer.state.isMailerSetup && (
       {!adminAppContainer.state.isMailerSetup && (
         <div className="alert alert-danger"><span className="material-symbols-outlined">error</span> {t('admin:app_setting.mailer_is_not_set_up')}</div>
         <div className="alert alert-danger"><span className="material-symbols-outlined">error</span> {t('admin:app_setting.mailer_is_not_set_up')}</div>
       )}
       )}
-      <div className="row mb-5">
+      <div className="row mb-4">
         <label className="col-md-3 col-form-label text-end">{t('admin:app_setting.from_e-mail_address')}</label>
         <label className="col-md-3 col-form-label text-end">{t('admin:app_setting.from_e-mail_address')}</label>
         <div className="col-md-6">
         <div className="col-md-6">
           <input
           <input
@@ -126,11 +130,12 @@ const MailSetting = (props: Props) => {
         </div>
         </div>
       </div>
       </div>
 
 
-      {adminAppContainer.state.transmissionMethod === 'smtp' && <SmtpSetting register={register} />}
-      {adminAppContainer.state.transmissionMethod === 'ses' && <SesSetting register={register} />}
+      {currentTransmissionMethod === 'smtp' && <SmtpSetting register={register} />}
+      {currentTransmissionMethod === 'ses' && <SesSetting register={register} />}
 
 
       <div className="row my-3">
       <div className="row my-3">
-        <div className="mx-auto">
+        <div className="col-md-3"></div>
+        <div className="col-md-9">
           <button type="submit" className="btn btn-primary" disabled={adminAppContainer.state.retrieveError != null}>
           <button type="submit" className="btn btn-primary" disabled={adminAppContainer.state.retrieveError != null}>
             { t('Update') }
             { t('Update') }
           </button>
           </button>

+ 1 - 1
apps/app/src/client/components/Admin/App/SesSetting.tsx

@@ -18,7 +18,7 @@ const SesSetting = (props: Props): JSX.Element => {
 
 
   return (
   return (
     <React.Fragment>
     <React.Fragment>
-      <div id="mail-ses" className="tab-pane active mt-5">
+      <div id="mail-ses" className="tab-pane active">
 
 
         <div className="row">
         <div className="row">
           <label className="text-start text-md-end col-md-3 col-form-label">
           <label className="text-start text-md-end col-md-3 col-form-label">

+ 1 - 1
apps/app/src/client/components/Admin/App/SmtpSetting.tsx

@@ -21,7 +21,7 @@ const SmtpSetting = (props: Props): JSX.Element => {
 
 
   return (
   return (
     <React.Fragment>
     <React.Fragment>
-      <div id="mail-smtp" className="tab-pane active mt-5">
+      <div id="mail-smtp" className="tab-pane active">
         <div className="row">
         <div className="row">
           <label className="text-start text-md-end col-md-3 col-form-label">
           <label className="text-start text-md-end col-md-3 col-form-label">
             {t('admin:app_setting.host')}
             {t('admin:app_setting.host')}

+ 2 - 1
apps/app/src/client/components/Admin/Common/AdminUpdateButtonRow.tsx

@@ -13,7 +13,8 @@ const AdminUpdateButtonRow = (props: Props): JSX.Element => {
 
 
   return (
   return (
     <div className="row my-3">
     <div className="row my-3">
-      <div className="mx-auto">
+      <div className="col-md-3"></div>
+      <div className="col-md-9">
         <button
         <button
           // eslint-disable-next-line react/button-has-type
           // eslint-disable-next-line react/button-has-type
           type={props.type ?? 'button'}
           type={props.type ?? 'button'}

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

@@ -1,37 +1,119 @@
-import React, { useCallback, type JSX } from 'react';
-
 import { pathUtils } from '@growi/core/dist/utils';
 import { pathUtils } from '@growi/core/dist/utils';
-import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
 import Link from 'next/link';
+import { useTranslation } from 'next-i18next';
+import React, { type JSX, useCallback } from 'react';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
-import { useGrowiCloudUri, useGrowiAppIdForGrowiCloud } from '~/states/global';
+import { useGrowiAppIdForGrowiCloud, useGrowiCloudUri } from '~/states/global';
 
 
 import styles from './AdminNavigation.module.scss';
 import styles from './AdminNavigation.module.scss';
 
 
 const moduleClass = styles['admin-navigation'];
 const moduleClass = styles['admin-navigation'];
 
 
-
 // eslint-disable-next-line react/prop-types
 // eslint-disable-next-line react/prop-types
 const MenuLabel = ({ menu }: { menu: string }) => {
 const MenuLabel = ({ menu }: { menu: string }) => {
   const { t } = useTranslation(['admin', 'commons']);
   const { t } = useTranslation(['admin', 'commons']);
 
 
   switch (menu) {
   switch (menu) {
     /* eslint-disable no-multi-spaces, max-len */
     /* 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
     // Temporarily hiding
     // case 'ai-integration':           return (
     // case 'ai-integration':           return (
     //   <>{/* TODO: unify sizing of growi-custom-icons so that simplify code -- 2024.10.09 Yuki Takei */}
     //   <>{/* TODO: unify sizing of growi-custom-icons so that simplify code -- 2024.10.09 Yuki Takei */}
@@ -46,24 +128,44 @@ const MenuLabel = ({ menu }: { menu: string }) => {
     //     {t('ai_integration.ai_integration')}
     //     {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 = {
 type MenuLinkProps = {
-  menu: string,
-  isListGroupItems: boolean,
-  isRoot?: boolean,
-  isActive?: boolean,
-}
+  menu: string;
+  isListGroupItems: boolean;
+  isRoot?: boolean;
+  isActive?: boolean;
+};
 
 
 const MenuLink = ({
 const MenuLink = ({
-  menu, isRoot, isListGroupItems, isActive,
+  menu,
+  isRoot,
+  isListGroupItems,
+  isActive,
 }: MenuLinkProps) => {
 }: MenuLinkProps) => {
-
   const pageTransitionClassName = isListGroupItems
   const pageTransitionClassName = isListGroupItems
     ? 'list-group-item list-group-item-action rounded border-0'
     ? 'list-group-item list-group-item-action rounded border-0'
     : 'dropdown-item px-3 py-2';
     : 'dropdown-item px-3 py-2';
@@ -86,57 +188,122 @@ export const AdminNavigation = (): JSX.Element => {
   const growiCloudUri = useGrowiCloudUri();
   const growiCloudUri = useGrowiCloudUri();
   const growiAppIdForGrowiCloud = useGrowiAppIdForGrowiCloud();
   const 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
             <a
               href={`${growiCloudUri}/my/apps/${growiAppIdForGrowiCloud}`}
               href={`${growiCloudUri}/my/apps/${growiAppIdForGrowiCloud}`}
               className="list-group-item list-group-item-action border-0 round-corner"
               className="list-group-item list-group-item-action border-0 round-corner"
             >
             >
               <MenuLabel menu="cloud" />
               <MenuLabel menu="cloud" />
             </a>
             </a>
-          )
-        }
-        {/* eslint-enable no-multi-spaces */}
-      </>
-    );
-  }, [growiAppIdForGrowiCloud, growiCloudUri, isActiveMenu, pathname]);
+          )}
+          {/* eslint-enable no-multi-spaces */}
+        </>
+      );
+    },
+    [growiAppIdForGrowiCloud, growiCloudUri, isActiveMenu, pathname],
+  );
 
 
   return (
   return (
     <React.Fragment>
     <React.Fragment>
@@ -158,33 +325,42 @@ export const AdminNavigation = (): JSX.Element => {
         >
         >
           <span className="float-start">
           <span className="float-start">
             {/* eslint-disable no-multi-spaces */}
             {/* 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 */}
             {/* Temporarily hiding */}
             {/* {isActiveMenu('/ai-integration')                && <MenuLabel menu="ai-integration" />} */}
             {/* {isActiveMenu('/ai-integration')                && <MenuLabel menu="ai-integration" />} */}
             {/* eslint-enable no-multi-spaces */}
             {/* eslint-enable no-multi-spaces */}
           </span>
           </span>
         </button>
         </button>
-        <div className="dropdown-menu" aria-labelledby="dropdown-admin-navigation">
+        <div
+          className="dropdown-menu"
+          role="menu"
+          aria-labelledby="dropdown-admin-navigation"
+        >
           {getListGroupItemOrDropdownItemList(false)}
           {getListGroupItemOrDropdownItemList(false)}
         </div>
         </div>
       </div>
       </div>
-
     </React.Fragment>
     </React.Fragment>
   );
   );
 };
 };

+ 1 - 0
apps/app/src/components/Common/GrowiLogo.jsx

@@ -12,6 +12,7 @@ const GrowiLogo = memo(() => (
       height="32"
       height="32"
       viewBox="0 0 64 56"
       viewBox="0 0 64 56"
     >
     >
+      <title>GROWI</title>
       <path
       <path
         // eslint-disable-next-line max-len
         // eslint-disable-next-line max-len
         d="M17.123 33.8015L10.4717 45.3855C10.2686 45.7427 10.2686 46.1829 10.4717 46.5337L15.5934 55.4514C15.7838 55.7767 16.171 55.9999 16.5645 55.9999H17.123L23.5014 44.9007L17.123 33.8015Z"
         d="M17.123 33.8015L10.4717 45.3855C10.2686 45.7427 10.2686 46.1829 10.4717 46.5337L15.5934 55.4514C15.7838 55.7767 16.171 55.9999 16.5645 55.9999H17.123L23.5014 44.9007L17.123 33.8015Z"

+ 97 - 73
apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.tsx

@@ -1,100 +1,124 @@
-import React, { memo, useCallback, type JSX } from 'react';
-
 import Link from 'next/link';
 import Link from 'next/link';
+import { type JSX, memo, useCallback } from 'react';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
 import type LinkedPagePath from '~/models/linked-page-path';
 import type LinkedPagePath from '~/models/linked-page-path';
 
 
 import styles from './PagePathHierarchicalLink.module.scss';
 import styles from './PagePathHierarchicalLink.module.scss';
 
 
-
 type PagePathHierarchicalLinkProps = {
 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!!
   // !!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>
         <RootElm>
           <span className="path-segment">
           <span className="path-segment">
             <Link href="/trash" prefetch={false}>
             <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>
             </Link>
           </span>
           </span>
-          <span className={`separator ${styles.separator}`}><a href="/">/</a></span>
+          <span className={`separator ${styles.separator}`}>
+            <a href="/">/</a>
+          </span>
         </RootElm>
         </RootElm>
-      )
-      : (
+      ) : (
         <RootElm>
         <RootElm>
           <span className="path-segment">
           <span className="path-segment">
             <Link href="/" prefetch={false}>
             <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>
               <span className={`separator ${styles.separator}`}>/</span>
             </Link>
             </Link>
           </span>
           </span>
         </RootElm>
         </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
-            // 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>
-  );
-});
+    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 ? (
+            // biome-ignore-start lint/a11y/useValidAnchor: ignore
+            <a
+              className="page-segment"
+              // biome-ignore lint/security/noDangerouslySetInnerHtml: ignore
+              dangerouslySetInnerHTML={{
+                __html: linkedPagePathByHtml.pathName,
+              }}
+            ></a>
+          ) : (
+            <a className="page-segment">{linkedPagePath.pathName}</a>
+            // biome-ignore-end lint/a11y/useValidAnchor: ignore
+          )}
+        </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 { DevidedPagePath } from '@growi/core/dist/models';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { pagePathUtils } from '@growi/core/dist/utils';
+import { type JSX, useMemo } from 'react';
 
 
 import LinkedPagePath from '~/models/linked-page-path';
 import LinkedPagePath from '~/models/linked-page-path';
 
 
 import { PagePathHierarchicalLink } from '../PagePathHierarchicalLink';
 import { PagePathHierarchicalLink } from '../PagePathHierarchicalLink';
-
+import styles from './PagePathNav.module.scss';
 import type { PagePathNavLayoutProps } from './PagePathNavLayout';
 import type { PagePathNavLayoutProps } from './PagePathNavLayout';
 import { PagePathNavLayout } from './PagePathNavLayout';
 import { PagePathNavLayout } from './PagePathNavLayout';
 
 
-import styles from './PagePathNav.module.scss';
-
-
 const { isTrashPage } = pagePathUtils;
 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 => {
 export const PagePathNav = (props: PagePathNavLayoutProps): JSX.Element => {
@@ -37,7 +36,10 @@ export const PagePathNav = (props: PagePathNavLayoutProps): JSX.Element => {
     const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
     const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
     return (
     return (
       <>
       <>
-        <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} isInTrash={isInTrash} />
+        <PagePathHierarchicalLink
+          linkedPagePath={linkedPagePathFormer}
+          isInTrash={isInTrash}
+        />
         <Separator />
         <Separator />
       </>
       </>
     );
     );
@@ -49,13 +51,22 @@ export const PagePathNav = (props: PagePathNavLayoutProps): JSX.Element => {
     // one line
     // one line
     if (dPagePath.isRoot || dPagePath.isFormerRoot) {
     if (dPagePath.isRoot || dPagePath.isFormerRoot) {
       const linkedPagePath = new LinkedPagePath(pagePath);
       const linkedPagePath = new LinkedPagePath(pagePath);
-      return <PagePathHierarchicalLink linkedPagePath={linkedPagePath} isInTrash={isInTrash} />;
+      return (
+        <PagePathHierarchicalLink
+          linkedPagePath={linkedPagePath}
+          isInTrash={isInTrash}
+        />
+      );
     }
     }
 
 
     // two line
     // two line
     const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
     const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
     return (
     return (
-      <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.former} isInTrash={isInTrash} />
+      <PagePathHierarchicalLink
+        linkedPagePath={linkedPagePathLatter}
+        basePath={dPagePath.former}
+        isInTrash={isInTrash}
+      />
     );
     );
   }, [isInTrash, pagePath]);
   }, [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 dynamic from 'next/dynamic';
+import type { JSX, ReactNode } from 'react';
 
 
 import { usePageNotFound } from '~/states/page';
 import { usePageNotFound } from '~/states/page';
 
 
@@ -9,28 +8,36 @@ import styles from './PagePathNav.module.scss';
 const moduleClass = styles['grw-page-path-nav-layout'] ?? '';
 const moduleClass = styles['grw-page-path-nav-layout'] ?? '';
 
 
 export type PagePathNavLayoutProps = {
 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 & {
 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 => {
 export const PagePathNavLayout = (props: Props): JSX.Element => {
   const {
   const {
     className = '',
     className = '',
     inline = false,
     inline = false,
-    pageId, pagePath, isWipPage,
+    pageId,
+    pagePath,
+    isWipPage,
     formerLink,
     formerLink,
     formerLinkClassName = '',
     formerLinkClassName = '',
     latterLink,
     latterLink,
@@ -45,12 +52,11 @@ export const PagePathNavLayout = (props: Props): JSX.Element => {
   const containerLayoutClass = inline ? '' : 'd-flex align-items-center';
   const containerLayoutClass = inline ? '' : 'd-flex align-items-center';
 
 
   return (
   return (
-    <div
-      className={`${className} ${moduleClass}`}
-      style={{ maxWidth }}
-    >
+    <div className={`${className} ${moduleClass}`} style={{ maxWidth }}>
       {formerLink && (
       {formerLink && (
-        <span className={`${formerLinkClassName ?? ''} ${styles['grw-former-link']} mb-2 d-block`}>
+        <span
+          className={`${formerLinkClassName ?? ''} ${styles['grw-former-link']} mb-2 d-block`}
+        >
           {formerLink}
           {formerLink}
         </span>
         </span>
       )}
       )}
@@ -58,11 +64,9 @@ export const PagePathNavLayout = (props: Props): JSX.Element => {
         <h1 className={`m-0 d-inline align-bottom ${latterLinkClassName}`}>
         <h1 className={`m-0 d-inline align-bottom ${latterLinkClassName}`}>
           {latterLink}
           {latterLink}
         </h1>
         </h1>
-        { pageId != null && !isNotFound && (
+        {pageId != null && !isNotFound && (
           <span className="d-inline-flex align-items-center align-bottom ms-2 gap-2">
           <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">
             <span className="grw-page-path-nav-copydropdown">
               <CopyDropdown
               <CopyDropdown
                 pageId={pageId}
                 pageId={pageId}
@@ -75,7 +79,7 @@ export const PagePathNavLayout = (props: Props): JSX.Element => {
               </CopyDropdown>
               </CopyDropdown>
             </span>
             </span>
           </span>
           </span>
-        ) }
+        )}
       </div>
       </div>
     </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';
 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 withLoadingProps from 'next-dynamic-loading-props';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import withLoadingProps from 'next-dynamic-loading-props';
+import { type JSX, useState } from 'react';
 import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 
 
-import { PagePathNav } from '../PagePathNav';
 import type { PagePathNavLayoutProps } from '../PagePathNav';
 import type { PagePathNavLayoutProps } from '../PagePathNav';
+import { PagePathNav } from '../PagePathNav';
 
 
 import styles from './PagePathNavTitle.module.scss';
 import styles from './PagePathNavTitle.module.scss';
 
 
 const moduleClass = styles['grw-page-path-nav-title'] ?? '';
 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
  * Switch PagePathNav and PagePathNavSticky
  * @returns
  * @returns
  */
  */
-export const PagePathNavTitle = (props: PagePathNavLayoutProps): JSX.Element => {
-
+export const PagePathNavTitle = (
+  props: PagePathNavLayoutProps,
+): JSX.Element => {
   const [isClient, setClient] = useState(false);
   const [isClient, setClient] = useState(false);
 
 
   useIsomorphicLayoutEffect(() => {
   useIsomorphicLayoutEffect(() => {
     setClient(true);
     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 { useGrowiCustomIcon } from './use-growi-custom-icons';
 import { useLatoFontFamily } from './use-lato';
 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 dynamic from 'next/dynamic';
 import Link from 'next/link';
 import Link from 'next/link';
+import type { JSX, ReactNode } from 'react';
+import React from 'react';
 
 
 import GrowiLogo from '~/components/Common/GrowiLogo';
 import GrowiLogo from '~/components/Common/GrowiLogo';
-
-import { RawLayout } from './RawLayout';
-
-
 import styles from './Admin.module.scss';
 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 = {
 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 (
   return (
     <RawLayout>
     <RawLayout>
       <div className={`admin-page ${styles['admin-page']}`}>
       <div className={`admin-page ${styles['admin-page']}`}>
-
         <header className="py-0 container">
         <header className="py-0 container">
           <h1 className="p-3 fs-2 d-flex align-items-center">
           <h1 className="p-3 fs-2 d-flex align-items-center">
             <Link href="/" className="d-block mb-1 me-2">
             <Link href="/" className="d-block mb-1 me-2">
@@ -46,9 +50,7 @@ const AdminLayout = ({
               <div className="col-lg-3">
               <div className="col-lg-3">
                 <AdminNavigation />
                 <AdminNavigation />
               </div>
               </div>
-              <div className="col-lg-9 mb-5">
-                {children}
-              </div>
+              <div className="col-lg-9 mb-5">{children}</div>
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
@@ -58,7 +60,6 @@ const AdminLayout = ({
       </div>
       </div>
 
 
       <HotkeysManager />
       <HotkeysManager />
-
     </RawLayout>
     </RawLayout>
   );
   );
 };
 };

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

@@ -1,58 +1,130 @@
-import type { ReactNode, JSX } from 'react';
-import React from 'react';
-
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
-
-import { RawLayout } from './RawLayout';
-
-
+import type { JSX, ReactNode } from 'react';
 import styles from './BasicLayout.module.scss';
 import styles from './BasicLayout.module.scss';
+import { RawLayout } from './RawLayout';
 
 
 const AiAssistantSidebar = dynamic(
 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 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(
 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
 // 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(
 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(
 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 = {
 type Props = {
-  children?: ReactNode
-  className?: string
-}
-
+  children?: ReactNode;
+  className?: string;
+};
 
 
 export const BasicLayout = ({ children, className }: Props): JSX.Element => {
 export const BasicLayout = ({ children, className }: Props): JSX.Element => {
   return (
   return (
@@ -62,7 +134,8 @@ export const BasicLayout = ({ children, className }: Props): JSX.Element => {
           <Sidebar />
           <Sidebar />
         </div>
         </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 />
           <AlertSiteUrlUndefined />
           {children}
           {children}
         </div>
         </div>

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

@@ -1,27 +1,19 @@
-import type { ReactNode, JSX } from 'react';
-import React from 'react';
-
 import Image from 'next/image';
 import Image from 'next/image';
+import type { JSX, ReactNode } from 'react';
 
 
 import { useAppTitle } from '~/states/global';
 import { useAppTitle } from '~/states/global';
 
 
 import GrowiLogo from '../Common/GrowiLogo';
 import GrowiLogo from '../Common/GrowiLogo';
-
-import { RawLayout } from './RawLayout';
-
-
 import commonStyles from './NoLoginLayout.module.scss';
 import commonStyles from './NoLoginLayout.module.scss';
+import { RawLayout } from './RawLayout';
 
 
 type Props = {
 type Props = {
-  className?: string,
-  children?: ReactNode,
-}
-
-export const NoLoginLayout = ({
-  children, className,
-}: Props): JSX.Element => {
+  className?: string;
+  children?: ReactNode;
+};
 
 
-  const appTitle = useAppTitle();
+export const NoLoginLayout = ({ children, className }: Props): JSX.Element => {
+  const { data: appTitle } = useAppTitle();
 
 
   const classNames: string[] = [''];
   const classNames: string[] = [''];
   if (className != null) {
   if (className != null) {
@@ -32,23 +24,26 @@ export const NoLoginLayout = ({
     <RawLayout className={`nologin ${commonStyles.nologin} ${classNames}`}>
     <RawLayout className={`nologin ${commonStyles.nologin} ${classNames}`}>
       <div className="d-flex align-items-center vh-100 mt-0 flex-row">
       <div className="d-flex align-items-center vh-100 mt-0 flex-row">
         <div className="main container-fluid">
         <div className="main container-fluid">
-
           <div className="row">
           <div className="row">
-
             <div className="col-md-12 position-relative">
             <div className="col-md-12 position-relative">
               <div className="nologin-header mx-auto rounded-4 rounded-bottom-0">
               <div className="nologin-header mx-auto rounded-4 rounded-bottom-0">
                 <div className="d-flex justify-content-center align-items-center">
                 <div className="d-flex justify-content-center align-items-center">
                   <GrowiLogo />
                   <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>
                 </div>
                 {appTitle !== 'GROWI' ? (
                 {appTitle !== 'GROWI' ? (
-                  <h2 className="fs-4 text-center text-white">{ appTitle }</h2>
+                  <h2 className="fs-4 text-center text-white">{appTitle}</h2>
                 ) : null}
                 ) : null}
                 <div className="noLogin-form-errors px-3"></div>
                 <div className="noLogin-form-errors px-3"></div>
               </div>
               </div>
               {children}
               {children}
             </div>
             </div>
-
           </div>
           </div>
         </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 type { ColorScheme } from '@growi/core';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import Head from 'next/head';
+import type { JSX, ReactNode } from 'react';
+import React, { useState } from 'react';
 import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 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 loggerFactory from '~/utils/logger';
 
 
 import styles from './RawLayout.module.scss';
 import styles from './RawLayout.module.scss';
@@ -15,14 +17,15 @@ const toastContainerClass = styles['grw-toast-container'] ?? '';
 
 
 const logger = loggerFactory('growi:cli:RawLayout');
 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 = {
 type Props = {
-  className?: string,
-  children?: ReactNode,
-}
+  className?: string;
+  children?: ReactNode;
+};
 
 
 export const RawLayout = ({ children, className }: Props): JSX.Element => {
 export const RawLayout = ({ children, className }: Props): JSX.Element => {
   const classNames: string[] = ['layout-root', 'growi'];
   const classNames: string[] = ['layout-root', 'growi'];
@@ -32,7 +35,9 @@ export const RawLayout = ({ children, className }: Props): JSX.Element => {
   // get color scheme from next-themes
   // get color scheme from next-themes
   const { resolvedTheme, resolvedThemeByAttributes } = useNextThemes();
   const { resolvedTheme, resolvedThemeByAttributes } = useNextThemes();
 
 
-  const [colorScheme, setColorScheme] = useState<ColorScheme|undefined>(undefined);
+  const [colorScheme, setColorScheme] = useState<ColorScheme | undefined>(
+    undefined,
+  );
 
 
   // set colorScheme in CSR
   // set colorScheme in CSR
   useIsomorphicLayoutEffect(() => {
   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 { BasicLayout } from './BasicLayout';
 
 
 import commonStyles from './SearchResultLayout.module.scss';
 import commonStyles from './SearchResultLayout.module.scss';
 
 
 type Props = {
 type Props = {
-  children?: ReactNode,
-}
+  children?: ReactNode;
+};
 
 
 const SearchResultLayout = ({ children }: Props): JSX.Element => {
 const SearchResultLayout = ({ children }: Props): JSX.Element => {
-
   return (
   return (
     <BasicLayout className={`on-search ${commonStyles['on-search']}`}>
     <BasicLayout className={`on-search ${commonStyles['on-search']}`}>
-      { children }
+      {children}
     </BasicLayout>
     </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 dynamic from 'next/dynamic';
+import type { JSX, ReactNode } from 'react';
+import React from 'react';
 
 
 import { RawLayout } from './RawLayout';
 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 = {
 type Props = {
-  children?: ReactNode
-}
+  children?: ReactNode;
+};
 
 
 export const ShareLinkLayout = ({ children }: Props): JSX.Element => {
 export const ShareLinkLayout = ({ children }: Props): JSX.Element => {
   return (
   return (
     <RawLayout>
     <RawLayout>
-
-      <div className="page-wrapper">
-        {children}
-      </div>
+      <div className="page-wrapper">{children}</div>
 
 
       <GrowiNavbarBottom />
       <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'];
 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 => {
 export const GroundGlassBar = (props: Props): JSX.Element => {
   const { className, children, ...rest } = props;
   const { className, children, ...rest } = props;

+ 204 - 130
apps/app/src/components/PageView/PageAlerts/FixPageGrantAlert.tsx

@@ -1,43 +1,53 @@
-import React, {
-  useEffect, useState, useCallback, type JSX,
-} from 'react';
-
-import { PageGrant, GroupType } from '@growi/core';
+import { GroupType, PageGrant } from '@growi/core';
+import { type JSX, useCallback, useEffect, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
+import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
 import {
 import {
-  Modal, ModalHeader, ModalBody, ModalFooter,
-} from 'reactstrap';
-
-import { UserGroupPageGrantStatus, type IPageGrantData } from '~/interfaces/page';
-import type { PopulatedGrantedGroup, IRecordApplicableGrant, IResGrantData } from '~/interfaces/page-grant';
+  type IPageGrantData,
+  UserGroupPageGrantStatus,
+} from '~/interfaces/page';
+import type {
+  IRecordApplicableGrant,
+  IResGrantData,
+  PopulatedGrantedGroup,
+} from '~/interfaces/page-grant';
 import { useCurrentUser } from '~/states/global';
 import { useCurrentUser } from '~/states/global';
 import { useCurrentPageData } from '~/states/page';
 import { useCurrentPageData } from '~/states/page';
 import { useSWRxApplicableGrant, useSWRxCurrentGrantData } from '~/stores/page';
 import { useSWRxApplicableGrant, useSWRxCurrentGrantData } from '~/stores/page';
 
 
 type ModalProps = {
 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 FixPageGrantModal = (props: ModalProps): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   const {
   const {
-    isOpen, pageId, dataApplicableGrant, currentAndParentPageGrantData, close,
+    isOpen,
+    pageId,
+    dataApplicableGrant,
+    currentAndParentPageGrantData,
+    close,
   } = props;
   } = props;
 
 
-  const [selectedGrant, setSelectedGrant] = useState<PageGrant>(PageGrant.GRANT_RESTRICTED);
+  const [selectedGrant, setSelectedGrant] = useState<PageGrant>(
+    PageGrant.GRANT_RESTRICTED,
+  );
 
 
   const [isGroupSelectModalShown, setIsGroupSelectModalShown] = useState(false);
   const [isGroupSelectModalShown, setIsGroupSelectModalShown] = useState(false);
-  const [selectedGroups, setSelectedGroups] = useState<PopulatedGrantedGroup[]>([]);
+  const [selectedGroups, setSelectedGroups] = useState<PopulatedGrantedGroup[]>(
+    [],
+  );
 
 
   // Alert message state
   // Alert message state
   const [shouldShowModalAlert, setShowModalAlert] = useState<boolean>(false);
   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
   // Reset state when opened
   useEffect(() => {
   useEffect(() => {
@@ -49,17 +59,21 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
   }, [isOpen]);
   }, [isOpen]);
 
 
   const groupListItemClickHandler = (group: PopulatedGrantedGroup) => {
   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]);
       setSelectedGroups([...selectedGroups, group]);
     }
     }
   };
   };
 
 
-  const submit = async() => {
+  const submit = async () => {
     // Validate input values
     // Validate input values
-    if (selectedGrant === PageGrant.GRANT_USER_GROUP && selectedGroups.length === 0) {
+    if (
+      selectedGrant === PageGrant.GRANT_USER_GROUP &&
+      selectedGroups.length === 0
+    ) {
       setShowModalAlert(true);
       setShowModalAlert(true);
       return;
       return;
     }
     }
@@ -70,69 +84,89 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
       const apiv3Put = (await import('~/client/util/apiv3-client')).apiv3Put;
       const apiv3Put = (await import('~/client/util/apiv3-client')).apiv3Put;
       await apiv3Put(`/page/${pageId}/grant`, {
       await apiv3Put(`/page/${pageId}/grant`, {
         grant: selectedGrant,
         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;
       const toastSuccess = (await import('~/client/util/toastr')).toastSuccess;
       toastSuccess(t('Successfully updated'));
       toastSuccess(t('Successfully updated'));
-    }
-    catch (err) {
+    } catch (err) {
       const toastError = (await import('~/client/util/toastr')).toastError;
       const toastError = (await import('~/client/util/toastr')).toastError;
       toastError(t('Failed to update'));
       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 renderGrantDataLabel = useCallback(() => {
-    const { isForbidden, currentPageGrant, parentPageGrant } = currentAndParentPageGrantData;
+    const { isForbidden, currentPageGrant, parentPageGrant } =
+      currentAndParentPageGrantData;
 
 
     const currentGrantLabel = getGrantLabel(false, currentPageGrant);
     const currentGrantLabel = getGrantLabel(false, currentPageGrant);
     const parentGrantLabel = getGrantLabel(isForbidden, parentPageGrant);
     const parentGrantLabel = getGrantLabel(isForbidden, parentPageGrant);
 
 
     return (
     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>
-        {/* eslint-disable-next-line react/no-danger */}
-        <p dangerouslySetInnerHTML={{ __html: t('fix_page_grant.modal.grant_label.docLink') }} />
+        <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
+          // biome-ignore lint/security/noDangerouslySetInnerHtml: ignore
+          dangerouslySetInnerHTML={{
+            __html: t('fix_page_grant.modal.grant_label.docLink'),
+          }}
+        />
       </>
       </>
     );
     );
   }, [t, currentAndParentPageGrantData, getGrantLabel]);
   }, [t, currentAndParentPageGrantData, getGrantLabel]);
@@ -142,9 +176,7 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
 
 
     if (!isGrantAvailable) {
     if (!isGrantAvailable) {
       return (
       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,8 +184,13 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
       <>
       <>
         <ModalBody>
         <ModalBody>
           <div>
           <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"
+              // biome-ignore lint/security/noDangerouslySetInnerHtml: ignore
+              dangerouslySetInnerHTML={{
+                __html: t('fix_page_grant.modal.need_to_fix_grant'),
+              }}
+            />
 
 
             {/* grant data label */}
             {/* grant data label */}
             {renderGrantDataLabel()}
             {renderGrantDataLabel()}
@@ -165,12 +202,17 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
                   name="grantRestricted"
                   name="grantRestricted"
                   id="grantRestricted"
                   id="grantRestricted"
                   type="radio"
                   type="radio"
-                  disabled={!(PageGrant.GRANT_RESTRICTED in dataApplicableGrant)}
+                  disabled={
+                    !(PageGrant.GRANT_RESTRICTED in dataApplicableGrant)
+                  }
                   checked={selectedGrant === PageGrant.GRANT_RESTRICTED}
                   checked={selectedGrant === PageGrant.GRANT_RESTRICTED}
                   onChange={() => setSelectedGrant(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>
                 </label>
               </div>
               </div>
               <div className="form-check mb-3">
               <div className="form-check mb-3">
@@ -183,8 +225,11 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
                   checked={selectedGrant === PageGrant.GRANT_OWNER}
                   checked={selectedGrant === PageGrant.GRANT_OWNER}
                   onChange={() => setSelectedGrant(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>
                 </label>
               </div>
               </div>
               <div className="form-check d-flex mb-3">
               <div className="form-check d-flex mb-3">
@@ -193,12 +238,17 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
                   name="grantUserGroup"
                   name="grantUserGroup"
                   id="grantUserGroup"
                   id="grantUserGroup"
                   type="radio"
                   type="radio"
-                  disabled={!(PageGrant.GRANT_USER_GROUP in dataApplicableGrant)}
+                  disabled={
+                    !(PageGrant.GRANT_USER_GROUP in dataApplicableGrant)
+                  }
                   checked={selectedGrant === PageGrant.GRANT_USER_GROUP}
                   checked={selectedGrant === PageGrant.GRANT_USER_GROUP}
                   onChange={() => setSelectedGrant(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>
                 </label>
                 <div className="dropdown ms-2">
                 <div className="dropdown ms-2">
                   <button
                   <button
@@ -208,28 +258,24 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
                     onClick={() => setIsGroupSelectModalShown(true)}
                     onClick={() => setIsGroupSelectModalShown(true)}
                   >
                   >
                     <span className="float-start ms-2">
                     <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>
                     </span>
                   </button>
                   </button>
                 </div>
                 </div>
               </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>
           </div>
           </div>
         </ModalBody>
         </ModalBody>
         <ModalFooter>
         <ModalFooter>
           <button type="button" className="btn btn-primary" onClick={submit}>
           <button type="button" className="btn btn-primary" onClick={submit}>
-            { t('fix_page_grant.modal.btn_label') }
+            {t('fix_page_grant.modal.btn_label')}
           </button>
           </button>
         </ModalFooter>
         </ModalFooter>
       </>
       </>
@@ -240,7 +286,7 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
     <>
     <>
       <Modal size="lg" isOpen={isOpen} toggle={close}>
       <Modal size="lg" isOpen={isOpen} toggle={close}>
         <ModalHeader tag="h4" toggle={close}>
         <ModalHeader tag="h4" toggle={close}>
-          { t('fix_page_grant.modal.title') }
+          {t('fix_page_grant.modal.title')}
         </ModalHeader>
         </ModalHeader>
         {renderModalBodyAndFooter()}
         {renderModalBodyAndFooter()}
       </Modal>
       </Modal>
@@ -249,31 +295,46 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
           isOpen={isGroupSelectModalShown}
           isOpen={isGroupSelectModalShown}
           toggle={() => setIsGroupSelectModalShown(false)}
           toggle={() => setIsGroupSelectModalShown(false)}
         >
         >
-          <ModalHeader tag="h4" toggle={() => setIsGroupSelectModalShown(false)}>
+          <ModalHeader
+            tag="h4"
+            toggle={() => setIsGroupSelectModalShown(false)}
+          >
             {t('user_group.select_group')}
             {t('user_group.select_group')}
           </ModalHeader>
           </ModalHeader>
           <ModalBody>
           <ModalBody>
-            <>
-              { applicableGroups.map((group) => {
-                const groupIsGranted = selectedGroups?.find(g => g.item._id === group.item._id) != null;
-                const activeClass = groupIsGranted ? 'active' : '';
-
-                return (
-                  <button
-                    className={`btn btn-outline-primary w-100 d-flex justify-content-start mb-3 align-items-center p-3 ${activeClass}`}
-                    type="button"
-                    key={group.item._id}
-                    onClick={() => groupListItemClickHandler(group)}
-                  >
-                    <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>}
-                    {/* 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>
-            </>
+            {applicableGroups.map((group) => {
+              const groupIsGranted =
+                selectedGroups?.find((g) => g.item._id === group.item._id) !=
+                null;
+              const activeClass = groupIsGranted ? 'active' : '';
+
+              return (
+                <button
+                  className={`btn btn-outline-primary w-100 d-flex justify-content-start mb-3 align-items-center p-3 ${activeClass}`}
+                  type="button"
+                  key={group.item._id}
+                  onClick={() => groupListItemClickHandler(group)}
+                >
+                  <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>
+                  )}
+                  {/* 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>
           </ModalBody>
           </ModalBody>
         </Modal>
         </Modal>
       )}
       )}
@@ -291,18 +352,27 @@ export const FixPageGrantAlert = (): JSX.Element => {
 
 
   const [isOpen, setOpen] = useState<boolean>(false);
   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
   // Dependencies
   if (pageData == null) {
   if (pageData == null) {
+    // biome-ignore lint/complexity/noUselessFragments: ignore
     return <></>;
     return <></>;
   }
   }
 
 
   if (!hasParent) {
   if (!hasParent) {
+    // biome-ignore lint/complexity/noUselessFragments: ignore
     return <></>;
     return <></>;
   }
   }
-  if (dataIsGrantNormalized?.isGrantNormalized == null || dataIsGrantNormalized.isGrantNormalized) {
+  if (
+    dataIsGrantNormalized?.isGrantNormalized == null ||
+    dataIsGrantNormalized.isGrantNormalized
+  ) {
     return <></>;
     return <></>;
   }
   }
 
 
@@ -310,27 +380,31 @@ export const FixPageGrantAlert = (): JSX.Element => {
     <>
     <>
       <div className="alert alert-warning py-3 ps-4 d-flex flex-column flex-lg-row">
       <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">
         <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')}
           {t('fix_page_grant.alert.description')}
         </div>
         </div>
         <div className="d-flex align-items-end align-items-lg-center">
         <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')}
             {t('fix_page_grant.alert.btn_label')}
           </button>
           </button>
         </div>
         </div>
       </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)}
+        />
+      )}
     </>
     </>
   );
   );
 };
 };

+ 15 - 7
apps/app/src/components/PageView/PageAlerts/FullTextSearchNotCoverAlert.tsx

@@ -1,29 +1,37 @@
-import type { JSX } from 'react';
-
 import { useAtomValue } from 'jotai';
 import { useAtomValue } from 'jotai';
+import type { JSX } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
 import { useCurrentPageData } from '~/states/page';
 import { useCurrentPageData } from '~/states/page';
 import { elasticsearchMaxBodyLengthToIndexAtom } from '~/states/server-configurations';
 import { elasticsearchMaxBodyLengthToIndexAtom } from '~/states/server-configurations';
 
 
-
 export const FullTextSearchNotCoverAlert = (): JSX.Element => {
 export const FullTextSearchNotCoverAlert = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
-  const elasticsearchMaxBodyLengthToIndex = useAtomValue(elasticsearchMaxBodyLengthToIndexAtom);
+  const elasticsearchMaxBodyLengthToIndex = useAtomValue(
+    elasticsearchMaxBodyLengthToIndexAtom,
+  );
   const data = useCurrentPageData();
   const data = useCurrentPageData();
 
 
   const markdownLength = data?.revision?.body?.length;
   const markdownLength = data?.revision?.body?.length;
 
 
-  if (markdownLength == null || elasticsearchMaxBodyLengthToIndex == null || markdownLength <= elasticsearchMaxBodyLengthToIndex) {
+  if (
+    markdownLength == null ||
+    elasticsearchMaxBodyLengthToIndex == null ||
+    markdownLength <= elasticsearchMaxBodyLengthToIndex
+  ) {
+    // biome-ignore lint/complexity/noUselessFragments: ignore
     return <></>;
     return <></>;
   }
   }
 
 
   return (
   return (
     <div className="alert alert-warning">
     <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
       <small
-        // eslint-disable-next-line react/no-danger
+        // biome-ignore lint/security/noDangerouslySetInnerHtml: ignore
         dangerouslySetInnerHTML={{
         dangerouslySetInnerHTML={{
           __html: t('page_page.notice.not_indexed2', {
           __html: t('page_page.notice.not_indexed2', {
             threshold: `<code>ELASTICSEARCH_MAX_BODY_LENGTH_TO_INDEX=${elasticsearchMaxBodyLengthToIndex}</code>`,
             threshold: `<code>ELASTICSEARCH_MAX_BODY_LENGTH_TO_INDEX=${elasticsearchMaxBodyLengthToIndex}</code>`,

+ 17 - 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 { returnPathForURL } from '@growi/core/dist/utils/path-utils';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
+import { type JSX, useCallback } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
-import { useCurrentPageData, useLatestRevision, useFetchCurrentPage } from '~/states/page';
+import {
+  useCurrentPageData,
+  useFetchCurrentPage,
+  useLatestRevision,
+} from '~/states/page';
 
 
 export const OldRevisionAlert = (): JSX.Element => {
 export const OldRevisionAlert = (): JSX.Element => {
   const router = useRouter();
   const router = useRouter();
@@ -14,7 +17,7 @@ export const OldRevisionAlert = (): JSX.Element => {
   const page = useCurrentPageData();
   const page = useCurrentPageData();
   const { fetchCurrentPage } = useFetchCurrentPage();
   const { fetchCurrentPage } = useFetchCurrentPage();
 
 
-  const onClickShowLatestButton = useCallback(async() => {
+  const onClickShowLatestButton = useCallback(async () => {
     if (page == null) {
     if (page == null) {
       return;
       return;
     }
     }
@@ -25,14 +28,22 @@ export const OldRevisionAlert = (): JSX.Element => {
   }, [fetchCurrentPage, page, router]);
   }, [fetchCurrentPage, page, router]);
 
 
   if (page == null || isOldRevisionPage) {
   if (page == null || isOldRevisionPage) {
+    // biome-ignore lint/complexity/noUselessFragments: ignore
     return <></>;
     return <></>;
   }
   }
 
 
   return (
   return (
     <div className="alert alert-warning">
     <div className="alert alert-warning">
       <strong>{t('Warning')}: </strong> {t('page_page.notice.version')}
       <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>
       </button>
     </div>
     </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 dynamic from 'next/dynamic';
+import type { JSX } from 'react';
 
 
 import { usePageNotFound } from '~/states/page';
 import { usePageNotFound } from '~/states/page';
 
 
@@ -9,21 +8,34 @@ import { PageGrantAlert } from './PageGrantAlert';
 import { PageStaleAlert } from './PageStaleAlert';
 import { PageStaleAlert } from './PageStaleAlert';
 import { WipPageAlert } from './WipPageAlert';
 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 => {
 export const PageAlerts = (): JSX.Element => {
-
   const isNotFound = usePageNotFound();
   const isNotFound = usePageNotFound();
 
 
   return (
   return (
     <div className="row d-edit-none">
     <div className="row d-edit-none">
       <div className="col-sm-12">
       <div className="col-sm-12">
         {/* alerts */}
         {/* alerts */}
-        { !isNotFound && <FixPageGrantAlert /> }
+        {!isNotFound && <FixPageGrantAlert />}
         <FullTextSearchNotCoverAlert />
         <FullTextSearchNotCoverAlert />
         <WipPageAlert />
         <WipPageAlert />
         <PageGrantAlert />
         <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 { isPopulated } from '@growi/core';
+import React, { type JSX } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
 import { useCurrentPageData } from '~/states/page';
 import { useCurrentPageData } from '~/states/page';
 
 
-
 export const PageGrantAlert = (): JSX.Element => {
 export const PageGrantAlert = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const pageData = useCurrentPageData();
   const pageData = useCurrentPageData();
@@ -15,7 +13,7 @@ export const PageGrantAlert = (): JSX.Element => {
   }
   }
 
 
   const populatedGrantedGroups = () => {
   const populatedGrantedGroups = () => {
-    return pageData.grantedGroups.filter(group => isPopulated(group.item));
+    return pageData.grantedGroups.filter((group) => isPopulated(group.item));
   };
   };
 
 
   const renderAlertContent = () => {
   const renderAlertContent = () => {
@@ -23,14 +21,16 @@ export const PageGrantAlert = (): JSX.Element => {
       if (pageData.grant === 2) {
       if (pageData.grant === 2) {
         return (
         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) {
       if (pageData.grant === 4) {
         return (
         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 (
         return (
           <>
           <>
             <span className="material-symbols-outlined me-1">account_tree</span>
             <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>
             </strong>
           </>
           </>
         );
         );
@@ -53,7 +54,6 @@ export const PageGrantAlert = (): JSX.Element => {
     );
     );
   };
   };
 
 
-
   return (
   return (
     <p data-testid="page-grant-alert" className="alert alert-primary py-3 px-4">
     <p data-testid="page-grant-alert" className="alert alert-primary py-3 px-4">
       {renderAlertContent()}
       {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 { useTranslation } from 'next-i18next';
+import React, { type JSX, useCallback, useState } from 'react';
 
 
 import { useCurrentPagePath, useRedirectFrom } from '~/states/page';
 import { useCurrentPagePath, useRedirectFrom } from '~/states/page';
 
 
@@ -11,7 +10,7 @@ export const PageRedirectedAlert = React.memo((): JSX.Element => {
 
 
   const [isUnlinked, setIsUnlinked] = useState(false);
   const [isUnlinked, setIsUnlinked] = useState(false);
 
 
-  const unlinkButtonClickHandler = useCallback(async() => {
+  const unlinkButtonClickHandler = useCallback(async () => {
     if (currentPagePath == null) {
     if (currentPagePath == null) {
       return;
       return;
     }
     }
@@ -19,8 +18,7 @@ export const PageRedirectedAlert = React.memo((): JSX.Element => {
       const unlink = (await import('~/client/services/page-operation')).unlink;
       const unlink = (await import('~/client/services/page-operation')).unlink;
       await unlink(currentPagePath);
       await unlink(currentPagePath);
       setIsUnlinked(true);
       setIsUnlinked(true);
-    }
-    catch (err) {
+    } catch (err) {
       const toastError = (await import('~/client/util/toastr')).toastError;
       const toastError = (await import('~/client/util/toastr')).toastError;
       toastError(err);
       toastError(err);
     }
     }
@@ -33,7 +31,7 @@ export const PageRedirectedAlert = React.memo((): JSX.Element => {
   if (isUnlinked) {
   if (isUnlinked) {
     return (
     return (
       <div className="alert alert-info d-edit-none py-3 px-4">
       <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>
       </div>
     );
     );
   }
   }
@@ -41,10 +39,18 @@ export const PageRedirectedAlert = React.memo((): JSX.Element => {
   return (
   return (
     <div className="alert alert-pink d-edit-none py-3 px-4 d-flex align-items-center justify-content-between">
     <div className="alert alert-pink d-edit-none py-3 px-4 d-flex align-items-center justify-content-between">
       <span>
       <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>
       </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')}
         {t('unlink_redirection')}
       </button>
       </button>
     </div>
     </div>

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

@@ -1,34 +1,39 @@
-import type { JSX } from 'react';
-
 import { isIPageInfoForEntity } from '@growi/core';
 import { isIPageInfoForEntity } from '@growi/core';
 import { useAtomValue } from 'jotai';
 import { useAtomValue } from 'jotai';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-
+import type { JSX } from 'react';
 
 
 import { useCurrentPageData } from '~/states/page';
 import { useCurrentPageData } from '~/states/page';
 import { isEnabledStaleNotificationAtom } from '~/states/server-configurations';
 import { isEnabledStaleNotificationAtom } from '~/states/server-configurations';
 import { useSWRxPageInfo } from '~/stores/page';
 import { useSWRxPageInfo } from '~/stores/page';
 
 
-
-export const PageStaleAlert = ():JSX.Element => {
+export const PageStaleAlert = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const isEnabledStaleNotification = useAtomValue(isEnabledStaleNotificationAtom);
+  const isEnabledStaleNotification = useAtomValue(
+    isEnabledStaleNotificationAtom,
+  );
 
 
   // Todo: determine if it should fetch or not like useSWRxPageInfo below after https://redmine.weseek.co.jp/issues/96788
   // Todo: determine if it should fetch or not like useSWRxPageInfo below after https://redmine.weseek.co.jp/issues/96788
   const pageData = useCurrentPageData();
   const pageData = useCurrentPageData();
-  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) {
   if (!isEnabledStaleNotification) {
+    // biome-ignore lint/complexity/noUselessFragments: ignore
     return <></>;
     return <></>;
   }
   }
 
 
   if (pageInfo == null || contentAge == null || contentAge === 0) {
   if (pageInfo == null || contentAge == null || contentAge === 0) {
+    // biome-ignore lint/complexity/noUselessFragments: ignore
     return <></>;
     return <></>;
   }
   }
 
 
-  let alertClass;
+  let alertClass: string;
   switch (contentAge) {
   switch (contentAge) {
     case 1:
     case 1:
       alertClass = 'alert-info';
       alertClass = 'alert-info';
@@ -43,7 +48,7 @@ export const PageStaleAlert = ():JSX.Element => {
   return (
   return (
     <div className={`alert ${alertClass}`}>
     <div className={`alert ${alertClass}`}>
       <span className="material-symbols-outlined me-1">hourglass</span>
       <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>
     </div>
   );
   );
 };
 };

+ 63 - 27
apps/app/src/components/PageView/PageAlerts/TrashPageAlert.tsx

@@ -1,12 +1,14 @@
-import React, { useCallback, type JSX } from 'react';
-
 import { UserPicture } from '@growi/ui/dist/components';
 import { UserPicture } from '@growi/ui/dist/components';
 import { format } from 'date-fns/format';
 import { format } from 'date-fns/format';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
+import { type JSX, useCallback } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
 import {
 import {
-  useCurrentPageData, useCurrentPagePath, useIsTrashPage, useFetchCurrentPage,
+  useCurrentPageData,
+  useCurrentPagePath,
+  useFetchCurrentPage,
+  useIsTrashPage,
 } from '~/states/page';
 } from '~/states/page';
 import { usePageDeleteModalActions } from '~/states/ui/modal/page-delete';
 import { usePageDeleteModalActions } from '~/states/ui/modal/page-delete';
 import { usePutBackPageModalActions } from '~/states/ui/modal/put-back-page';
 import { usePutBackPageModalActions } from '~/states/ui/modal/put-back-page';
@@ -14,7 +16,6 @@ import { useIsAbleToShowTrashPageManagementButtons } from '~/states/ui/page-abil
 import { useSWRxPageInfo } from '~/stores/page';
 import { useSWRxPageInfo } from '~/stores/page';
 import { mutateRecentlyUpdated } from '~/stores/page-listing';
 import { mutateRecentlyUpdated } from '~/stores/page-listing';
 
 
-
 const onDeletedHandler = (pathOrPathsToDelete) => {
 const onDeletedHandler = (pathOrPathsToDelete) => {
   if (typeof pathOrPathsToDelete !== 'string') {
   if (typeof pathOrPathsToDelete !== 'string') {
     return;
     return;
@@ -27,7 +28,8 @@ export const TrashPageAlert = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const router = useRouter();
   const router = useRouter();
 
 
-  const isAbleToShowTrashPageManagementButtons = useIsAbleToShowTrashPageManagementButtons();
+  const isAbleToShowTrashPageManagementButtons =
+    useIsAbleToShowTrashPageManagementButtons();
   const pageData = useCurrentPageData();
   const pageData = useCurrentPageData();
   const isTrashPage = useIsTrashPage();
   const isTrashPage = useIsTrashPage();
   const pageId = pageData?._id;
   const pageId = pageData?._id;
@@ -41,7 +43,9 @@ export const TrashPageAlert = (): JSX.Element => {
   const { fetchCurrentPage } = useFetchCurrentPage();
   const { fetchCurrentPage } = useFetchCurrentPage();
 
 
   const deleteUser = pageData?.deleteUser;
   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 revisionId = pageData?.revision?._id;
   const isEmptyPage = pageId == null || revisionId == null || pagePath == null;
   const isEmptyPage = pageId == null || revisionId == null || pagePath == null;
 
 
@@ -55,20 +59,31 @@ export const TrashPageAlert = (): JSX.Element => {
         return;
         return;
       }
       }
       try {
       try {
-        const unlink = (await import('~/client/services/page-operation')).unlink;
+        const unlink = (await import('~/client/services/page-operation'))
+          .unlink;
         unlink(currentPagePath);
         unlink(currentPagePath);
 
 
         router.push(`/${pageId}`);
         router.push(`/${pageId}`);
         fetchCurrentPage();
         fetchCurrentPage();
         mutateRecentlyUpdated();
         mutateRecentlyUpdated();
-      }
-      catch (err) {
+      } catch (err) {
         const toastError = (await import('~/client/util/toastr')).toastError;
         const toastError = (await import('~/client/util/toastr')).toastError;
         toastError(err);
         toastError(err);
       }
       }
     };
     };
-    openPutBackPageModal({ pageId, path: pagePath }, { onPutBacked: putBackedHandler });
-  }, [isEmptyPage, openPutBackPageModal, pageId, pagePath, currentPagePath, router, fetchCurrentPage]);
+    openPutBackPageModal(
+      { pageId, path: pagePath },
+      { onPutBacked: putBackedHandler },
+    );
+  }, [
+    isEmptyPage,
+    openPutBackPageModal,
+    pageId,
+    pagePath,
+    currentPagePath,
+    router,
+    fetchCurrentPage,
+  ]);
 
 
   const openPageDeleteModalHandler = useCallback(() => {
   const openPageDeleteModalHandler = useCallback(() => {
     // User cannot operate empty page.
     // User cannot operate empty page.
@@ -95,7 +110,10 @@ export const TrashPageAlert = (): JSX.Element => {
           onClick={openPutbackPageModalHandler}
           onClick={openPutbackPageModalHandler}
           data-testid="put-back-button"
           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>
         <button
         <button
           type="button"
           type="button"
@@ -103,32 +121,50 @@ export const TrashPageAlert = (): JSX.Element => {
           disabled={!(pageInfo?.isAbleToDeleteCompletely ?? false)}
           disabled={!(pageInfo?.isAbleToDeleteCompletely ?? false)}
           onClick={openPageDeleteModalHandler}
           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>
         </button>
       </>
       </>
     );
     );
-  }, [openPageDeleteModalHandler, openPutbackPageModalHandler, pageInfo?.isAbleToDeleteCompletely, t]);
+  }, [
+    openPageDeleteModalHandler,
+    openPutbackPageModalHandler,
+    pageInfo?.isAbleToDeleteCompletely,
+    t,
+  ]);
 
 
   // Show this alert only for non-empty pages in trash.
   // Show this alert only for non-empty pages in trash.
   if (!isTrashPage || isEmptyPage) {
   if (!isTrashPage || isEmptyPage) {
+    // biome-ignore lint/complexity/noUselessFragments: ignore
     return <></>;
     return <></>;
   }
   }
 
 
   return (
   return (
-    <>
-      <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>.
-          <br />
-          <UserPicture user={deleteUser} />
-          <span className="ms-2">
-            Deleted by {deleteUser?.name} at <span data-vrt-blackout-datetime>{deletedAt ?? pageData?.updatedAt}</span>
+    <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>
+        .
+        <br />
+        <UserPicture user={deleteUser} />
+        <span className="ms-2">
+          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()}
-        </div>
+        </span>
+      </div>
+      <div className="pt-1 d-flex align-items-end align-items-lg-center">
+        {isAbleToShowTrashPageManagementButtons &&
+          renderTrashPageManagementButtons()}
       </div>
       </div>
-    </>
+    </div>
   );
   );
 };
 };

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

@@ -1,16 +1,14 @@
-import React, { useCallback, type JSX } from 'react';
-
+import React, { type JSX, useCallback } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
 import { useCurrentPageData, useFetchCurrentPage } from '~/states/page';
 import { useCurrentPageData, useFetchCurrentPage } from '~/states/page';
 
 
-
 export const WipPageAlert = (): JSX.Element => {
 export const WipPageAlert = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const currentPage = useCurrentPageData();
   const currentPage = useCurrentPageData();
   const { fetchCurrentPage } = useFetchCurrentPage();
   const { fetchCurrentPage } = useFetchCurrentPage();
 
 
-  const clickPagePublishButton = useCallback(async() => {
+  const clickPagePublishButton = useCallback(async () => {
     const pageId = currentPage?._id;
     const pageId = currentPage?._id;
 
 
     if (pageId == null) {
     if (pageId == null) {
@@ -18,27 +16,28 @@ export const WipPageAlert = (): JSX.Element => {
     }
     }
 
 
     try {
     try {
-      const publish = (await import('~/client/services/page-operation')).publish;
+      const publish = (await import('~/client/services/page-operation'))
+        .publish;
       await publish(pageId);
       await publish(pageId);
 
 
       await fetchCurrentPage();
       await fetchCurrentPage();
 
 
-      const mutatePageTree = (await import('~/stores/page-listing')).mutatePageTree;
+      const mutatePageTree = (await import('~/stores/page-listing'))
+        .mutatePageTree;
       await mutatePageTree();
       await mutatePageTree();
 
 
-      const mutateRecentlyUpdated = (await import('~/stores/page-listing')).mutateRecentlyUpdated;
+      const mutateRecentlyUpdated = (await import('~/stores/page-listing'))
+        .mutateRecentlyUpdated;
       await mutateRecentlyUpdated();
       await mutateRecentlyUpdated();
 
 
       const toastSuccess = (await import('~/client/util/toastr')).toastSuccess;
       const toastSuccess = (await import('~/client/util/toastr')).toastSuccess;
       toastSuccess(t('wip_page.success_publish_page'));
       toastSuccess(t('wip_page.success_publish_page'));
-    }
-    catch {
+    } catch {
       const toastError = (await import('~/client/util/toastr')).toastError;
       const toastError = (await import('~/client/util/toastr')).toastError;
       toastError(t('wip_page.fail_publish_page'));
       toastError(t('wip_page.fail_publish_page'));
     }
     }
   }, [currentPage?._id, fetchCurrentPage, t]);
   }, [currentPage?._id, fetchCurrentPage, t]);
 
 
-
   if (!currentPage?.wip) {
   if (!currentPage?.wip) {
     return <></>;
     return <></>;
   }
   }
@@ -52,7 +51,7 @@ export const WipPageAlert = (): JSX.Element => {
         className="btn btn-outline-secondary ms-auto"
         className="btn btn-outline-secondary ms-auto"
         onClick={clickPagePublishButton}
         onClick={clickPagePublishButton}
       >
       >
-        {t('wip_page.publish_page') }
+        {t('wip_page.publish_page')}
       </button>
       </button>
     </p>
     </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 type { IPage, IPagePopulatedToShowRevision } from '@growi/core';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import type { JSX } from 'react';
 
 
 import styles from './PageContentFooter.module.scss';
 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 = {
 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 { page } = props;
 
 
-  const {
-    creator, lastUpdateUser, createdAt, updatedAt,
-  } = page;
+  const { creator, lastUpdateUser, createdAt, updatedAt } = page;
 
 
   if (page.isEmpty) {
   if (page.isEmpty) {
     return <></>;
     return <></>;
   }
   }
 
 
   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">
       <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>
     </div>
     </div>
   );
   );

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

@@ -1,54 +1,93 @@
-import {
-  useEffect, useMemo, useRef, memo, type JSX,
-} from 'react';
-
 import { isUsersHomepage } from '@growi/core/dist/utils/page-path-utils';
 import { isUsersHomepage } from '@growi/core/dist/utils/page-path-utils';
 import { useSlidesByFrontmatter } from '@growi/presentation/dist/services';
 import { useSlidesByFrontmatter } from '@growi/presentation/dist/services';
-import dynamic from 'next/dynamic';
+import { type JSX, useEffect, useMemo, useRef } from 'react';
 
 
 import { PagePathNavTitle } from '~/components/Common/PagePathNavTitle';
 import { PagePathNavTitle } from '~/components/Common/PagePathNavTitle';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import { useShouldExpandContent } from '~/services/layout/use-should-expand-content';
 import { useShouldExpandContent } from '~/services/layout/use-should-expand-content';
 import { generateSSRViewOptions } from '~/services/renderer/renderer';
 import { generateSSRViewOptions } from '~/services/renderer/renderer';
 import {
 import {
-  useIsForbidden, useIsIdenticalPath, useIsNotCreatable, useCurrentPageData, useCurrentPageId, usePageNotFound,
+  useCurrentPageData,
+  useCurrentPageId,
+  useIsForbidden,
+  useIsIdenticalPath,
+  useIsNotCreatable,
+  usePageNotFound,
 } from '~/states/page';
 } from '~/states/page';
 import { useViewOptions } from '~/stores/renderer';
 import { useViewOptions } from '~/stores/renderer';
 
 
 import { UserInfo } from '../User/UserInfo';
 import { UserInfo } from '../User/UserInfo';
-
 import { PageAlerts } from './PageAlerts/PageAlerts';
 import { PageAlerts } from './PageAlerts/PageAlerts';
 import { PageContentFooter } from './PageContentFooter';
 import { PageContentFooter } from './PageContentFooter';
 import { PageViewLayout } from './PageViewLayout';
 import { PageViewLayout } from './PageViewLayout';
 import RevisionRenderer from './RevisionRenderer';
 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 = {
 type Props = {
-  pagePath: string,
-  rendererConfig: RendererConfig,
-  className?: string,
-}
+  pagePath: string;
+  rendererConfig: RendererConfig;
+  className?: string;
+};
 
 
 export const PageView = memo((props: Props): JSX.Element => {
 export const PageView = memo((props: Props): JSX.Element => {
   const renderStartTime = performance.now();
   const renderStartTime = performance.now();
 
 
   const commentsContainerRef = useRef<HTMLDivElement>(null);
   const commentsContainerRef = useRef<HTMLDivElement>(null);
 
 
-  const {
-    pagePath, rendererConfig, className,
-  } = props;
+  const { pagePath, rendererConfig, className } = props;
 
 
   const currentPageId = useCurrentPageId();
   const currentPageId = useCurrentPageId();
   const isIdenticalPathPage = useIsIdenticalPath();
   const isIdenticalPathPage = useIsIdenticalPath();
@@ -73,10 +112,11 @@ export const PageView = memo((props: Props): JSX.Element => {
 
 
   const shouldExpandContent = useShouldExpandContent(page);
   const shouldExpandContent = useShouldExpandContent(page);
 
 
-
   const markdown = page?.revision?.body;
   const markdown = page?.revision?.body;
-  const isSlide = useSlidesByFrontmatter(markdown, rendererConfig.isEnabledMarp);
-
+  const isSlide = useSlidesByFrontmatter(
+    markdown,
+    rendererConfig.isEnabledMarp,
+  );
 
 
   // ***************************  Auto Scroll  ***************************
   // ***************************  Auto Scroll  ***************************
   useEffect(() => {
   useEffect(() => {
@@ -100,7 +140,9 @@ export const PageView = memo((props: Props): JSX.Element => {
       return;
       return;
     }
     }
 
 
-    const contentContainer = document.getElementById('page-view-content-container');
+    const contentContainer = document.getElementById(
+      'page-view-content-container',
+    );
     if (contentContainer == null) return;
     if (contentContainer == null) return;
 
 
     const targetId = decodeURIComponent(hash.slice(1));
     const targetId = decodeURIComponent(hash.slice(1));
@@ -137,26 +179,28 @@ export const PageView = memo((props: Props): JSX.Element => {
     }
     }
   }, [isForbidden, isIdenticalPathPage, isNotCreatable]);
   }, [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} />
           <UsersHomepageFooter creatorId={page.creator._id} />
         )}
         )}
         <PageContentFooter page={page} />
         <PageContentFooter page={page} />
       </>
       </>
-    )
-    : null;
+    ) : null;
 
 
-  const Contents = () => {
+  const Contents = useCallback(() => {
     const contentsRenderStartTime = performance.now();
     const contentsRenderStartTime = performance.now();
     console.log('[PAGEVIEW-DEBUG] Contents component render started:', {
     console.log('[PAGEVIEW-DEBUG] Contents component render started:', {
       isNotFound,
       isNotFound,
@@ -173,7 +217,8 @@ export const PageView = memo((props: Props): JSX.Element => {
     }
     }
 
 
     const markdown = page.revision.body;
     const markdown = page.revision.body;
-    const rendererOptions = viewOptions ?? generateSSRViewOptions(rendererConfig, pagePath);
+    const rendererOptions =
+      viewOptions ?? generateSSRViewOptions(rendererConfig, pagePath);
 
 
     console.log('[PAGEVIEW-DEBUG] Rendering page content:', {
     console.log('[PAGEVIEW-DEBUG] Rendering page content:', {
       markdownLength: markdown?.length,
       markdownLength: markdown?.length,
@@ -187,13 +232,16 @@ export const PageView = memo((props: Props): JSX.Element => {
         <PageContentsUtilities />
         <PageContentsUtilities />
 
 
         <div className="flex-expand-vert justify-content-between">
         <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}>
             <div id="comments-container" ref={commentsContainerRef}>
               <Comments
               <Comments
                 pageId={page._id}
                 pageId={page._id}
@@ -201,11 +249,20 @@ export const PageView = memo((props: Props): JSX.Element => {
                 revision={page.revision}
                 revision={page.revision}
               />
               />
             </div>
             </div>
-          ) }
+          )}
         </div>
         </div>
       </>
       </>
     );
     );
-  };
+  }, [
+    isNotFound,
+    page?.revision,
+    page?._id,
+    rendererConfig,
+    pagePath,
+    viewOptions,
+    isSlide,
+    isIdenticalPathPage,
+  ]);
 
 
   // DEBUG: Log final render completion time
   // DEBUG: Log final render completion time
   const renderEndTime = performance.now();
   const renderEndTime = performance.now();
@@ -230,13 +287,14 @@ export const PageView = memo((props: Props): JSX.Element => {
       {specialContents}
       {specialContents}
       {specialContents == null && (
       {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">
           <div id="page-view-content-container" className="flex-expand-vert">
             <Contents />
             <Contents />
           </div>
           </div>
         </>
         </>
       )}
       )}
-
     </PageViewLayout>
     </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';
 import styles from './PageViewLayout.module.scss';
 
 
@@ -6,18 +6,21 @@ const pageViewLayoutClass = styles['page-view-layout'] ?? '';
 const _fluidLayoutClass = styles['fluid-layout'] ?? '';
 const _fluidLayoutClass = styles['fluid-layout'] ?? '';
 
 
 type Props = {
 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 => {
 export const PageViewLayout = (props: Props): JSX.Element => {
   const {
   const {
     className,
     className,
-    children, headerContents, sideContents, footerContents,
+    children,
+    headerContents,
+    sideContents,
+    footerContents,
     expandContentWidth,
     expandContentWidth,
   } = props;
   } = props;
 
 
@@ -25,36 +28,38 @@ export const PageViewLayout = (props: Props): JSX.Element => {
 
 
   return (
   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">
         <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}
                 {children}
               </div>
               </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>
       </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}
           {footerContents}
         </footer>
         </footer>
-      ) }
+      )}
     </>
     </>
   );
   );
 };
 };

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

@@ -1,5 +1,4 @@
 import React, { type JSX } from 'react';
 import React, { type JSX } from 'react';
-
 import type { FallbackProps } from 'react-error-boundary';
 import type { FallbackProps } from 'react-error-boundary';
 import { ErrorBoundary } from 'react-error-boundary';
 import { ErrorBoundary } from 'react-error-boundary';
 import ReactMarkdown from 'react-markdown';
 import ReactMarkdown from 'react-markdown';
@@ -9,31 +8,35 @@ import loggerFactory from '~/utils/logger';
 
 
 import 'katex/dist/katex.min.css';
 import 'katex/dist/katex.min.css';
 
 
-
 const logger = loggerFactory('components:Page:RevisionRenderer');
 const logger = loggerFactory('components:Page:RevisionRenderer');
 
 
 type Props = {
 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';
 ErrorFallback.displayName = 'ErrorFallback';
 
 
 const RevisionRenderer = React.memo((props: Props): JSX.Element => {
 const RevisionRenderer = React.memo((props: Props): JSX.Element => {
-
-  const {
-    rendererOptions, markdown, additionalClassName,
-  } = props;
+  const { rendererOptions, markdown, additionalClassName } = props;
 
 
   return (
   return (
     <ErrorBoundary FallbackComponent={ErrorFallback}>
     <ErrorBoundary FallbackComponent={ErrorFallback}>
@@ -45,7 +48,6 @@ const RevisionRenderer = React.memo((props: Props): JSX.Element => {
       </ReactMarkdown>
       </ReactMarkdown>
     </ErrorBoundary>
     </ErrorBoundary>
   );
   );
-
 });
 });
 RevisionRenderer.displayName = 'RevisionRenderer';
 RevisionRenderer.displayName = 'RevisionRenderer';
 
 

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

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

+ 37 - 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 { pagePathUtils } from '@growi/core/dist/utils';
 import type { LinkProps } from 'next/link';
 import type { LinkProps } from 'next/link';
 import Link from 'next/link';
 import Link from 'next/link';
+import type { JSX } from 'react';
 
 
 import { useSiteUrl } from '~/states/global';
 import { useSiteUrl } from '~/states/global';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-
 const logger = loggerFactory('growi:components:NextLink');
 const logger = loggerFactory('growi:components:NextLink');
 
 
 const isAnchorLink = (href: string): boolean => {
 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 baseUrl = new URL(siteUrl ?? 'https://example.com');
     const hrefUrl = new URL(href, baseUrl);
     const hrefUrl = new URL(href, baseUrl);
     return baseUrl.host !== hrefUrl.host;
     return baseUrl.host !== hrefUrl.host;
-  }
-  catch (err) {
+  } catch (err) {
     logger.debug(err);
     logger.debug(err);
     return false;
     return false;
   }
   }
@@ -31,28 +28,26 @@ const isCreatablePage = (href: string) => {
     const url = new URL(href, 'http://example.com');
     const url = new URL(href, 'http://example.com');
     const pathName = url.pathname;
     const pathName = url.pathname;
     return pagePathUtils.isCreatablePage(pathName);
     return pagePathUtils.isCreatablePage(pathName);
-  }
-  catch (err) {
+  } catch (err) {
     logger.debug(err);
     logger.debug(err);
     return false;
     return false;
   }
   }
 };
 };
 
 
 type Props = Omit<LinkProps, 'href'> & {
 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 => {
 export const NextLink = (props: Props): JSX.Element => {
-  const {
-    id, href, children, className, onClick, ...rest
-  } = props;
+  const { id, href, children, className, onClick, ...rest } = props;
 
 
   const siteUrl = useSiteUrl();
   const siteUrl = useSiteUrl();
 
 
   if (href == null) {
   if (href == null) {
+    // biome-ignore lint/a11y/useValidAnchor: ignore
     return <a className={className}>{children}</a>;
     return <a className={className}>{children}</a>;
   }
   }
 
 
@@ -63,8 +58,17 @@ export const NextLink = (props: Props): JSX.Element => {
 
 
   if (isExternalLink(href, siteUrl)) {
   if (isExternalLink(href, siteUrl)) {
     return (
     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>
       </a>
     );
     );
   }
   }
@@ -72,13 +76,28 @@ export const NextLink = (props: Props): JSX.Element => {
   // when href is an anchor link or not-creatable path
   // when href is an anchor link or not-creatable path
   if (isAnchorLink(href) || !isCreatablePage(href)) {
   if (isAnchorLink(href) || !isCreatablePage(href)) {
     return (
     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 (
   return (
     <Link {...rest} href={href} prefetch={false} legacyBehavior>
     <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>
     </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 type { IGraphViewerGlobal } from '@growi/remark-drawio';
 import Head from 'next/head';
 import Head from 'next/head';
+import { type JSX, useCallback } from 'react';
 
 
 import { generateViewerMinJsUrl } from './use-viewer-min-js-url';
 import { generateViewerMinJsUrl } from './use-viewer-min-js-url';
 
 
@@ -12,7 +11,7 @@ declare global {
 
 
 type Props = {
 type Props = {
   drawioUri: string;
   drawioUri: string;
-}
+};
 
 
 export const DrawioViewerScript = ({ drawioUri }: Props): JSX.Element => {
 export const DrawioViewerScript = ({ drawioUri }: Props): JSX.Element => {
   const loadedHandler = useCallback(() => {
   const loadedHandler = useCallback(() => {

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

@@ -2,16 +2,19 @@ import { generateViewerMinJsUrl } from './use-viewer-min-js-url';
 
 
 describe('generateViewerMinJsUrl', () => {
 describe('generateViewerMinJsUrl', () => {
   it.each`
   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 = generateViewerMinJsUrl(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 = generateViewerMinJsUrl(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 type { FC } from 'react';
 import React from 'react';
 import React from 'react';
 
 
-import { useTranslation } from 'next-i18next';
-
 const generateRatio = (expiredAt: Date, createdAt: Date): number => {
 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();
   const remainingTime = new Date(expiredAt).getTime() - new Date().getTime();
   return remainingTime / wholeTime;
   return remainingTime / wholeTime;
 };
 };
@@ -14,23 +14,20 @@ const getAlertColor = (ratio: number): string => {
 
 
   if (ratio >= 0.75) {
   if (ratio >= 0.75) {
     color = 'success';
     color = 'success';
-  }
-  else if (ratio < 0.75 && ratio >= 0.5) {
+  } else if (ratio < 0.75 && ratio >= 0.5) {
     color = 'info';
     color = 'info';
-  }
-  else if (ratio < 0.5 && ratio >= 0.25) {
+  } else if (ratio < 0.5 && ratio >= 0.25) {
     color = 'warning';
     color = 'warning';
-  }
-  else {
+  } else {
     color = 'danger';
     color = 'danger';
   }
   }
   return color;
   return color;
 };
 };
 
 
 type Props = {
 type Props = {
-  createdAt: Date,
-  expiredAt?: Date,
-}
+  createdAt: Date;
+  expiredAt?: Date;
+};
 
 
 const ShareLinkAlert: FC<Props> = (props: Props) => {
 const ShareLinkAlert: FC<Props> = (props: Props) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
@@ -42,9 +39,15 @@ const ShareLinkAlert: FC<Props> = (props: Props) => {
   return (
   return (
     <p className={`alert alert-${alertColor} px-4 d-edit-none`}>
     <p className={`alert alert-${alertColor} px-4 d-edit-none`}>
       <span className="material-symbols-outlined me-1">link</span>
       <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>
+      ) : (
+        <span
+          // biome-ignore lint/security/noDangerouslySetInnerHtml: ignore
+          dangerouslySetInnerHTML={{
+            __html: t('page_page.notice.expiration', { expiredAt }),
+          }}
+        />
       )}
       )}
     </p>
     </p>
   );
   );

+ 80 - 54
apps/app/src/components/ShareLinkPageView/ShareLinkPageView.tsx

@@ -1,7 +1,5 @@
-import { useMemo, memo, type JSX } from 'react';
-
 import { useSlidesByFrontmatter } from '@growi/presentation/dist/services';
 import { useSlidesByFrontmatter } from '@growi/presentation/dist/services';
-import dynamic from 'next/dynamic';
+import { type JSX, memo, useMemo } from 'react';
 
 
 import { PagePathNavTitle } from '~/components/Common/PagePathNavTitle';
 import { PagePathNavTitle } from '~/components/Common/PagePathNavTitle';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
@@ -15,31 +13,40 @@ import loggerFactory from '~/utils/logger';
 import { PageContentFooter } from '../PageView/PageContentFooter';
 import { PageContentFooter } from '../PageView/PageContentFooter';
 import { PageViewLayout } from '../PageView/PageViewLayout';
 import { PageViewLayout } from '../PageView/PageViewLayout';
 import RevisionRenderer from '../PageView/RevisionRenderer';
 import RevisionRenderer from '../PageView/RevisionRenderer';
-
 import ShareLinkAlert from './ShareLinkAlert';
 import ShareLinkAlert from './ShareLinkAlert';
 
 
-
 const logger = loggerFactory('growi:components:ShareLinkPageView');
 const logger = loggerFactory('growi:components:ShareLinkPageView');
 
 
-
-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 = {
 type Props = {
-  pagePath: string,
-  rendererConfig: RendererConfig,
-  shareLink?: IShareLinkHasId,
-  isExpired?: boolean,
-  disableLinkSharing: boolean,
-}
+  pagePath: string;
+  rendererConfig: RendererConfig;
+  shareLink?: IShareLinkHasId;
+  isExpired?: boolean;
+  disableLinkSharing: boolean;
+};
 
 
 export const ShareLinkPageView = memo((props: Props): JSX.Element => {
 export const ShareLinkPageView = memo((props: Props): JSX.Element => {
-  const {
-    pagePath, rendererConfig,
-    shareLink,
-    isExpired, disableLinkSharing,
-  } = props;
+  const { pagePath, rendererConfig, shareLink, isExpired, disableLinkSharing } =
+    props;
 
 
   const isNotFoundMeta = usePageNotFound();
   const isNotFoundMeta = usePageNotFound();
 
 
@@ -51,7 +58,10 @@ export const ShareLinkPageView = memo((props: Props): JSX.Element => {
 
 
   const markdown = page?.revision?.body;
   const markdown = page?.revision?.body;
 
 
-  const isSlide = useSlidesByFrontmatter(markdown, rendererConfig.isEnabledMarp);
+  const isSlide = useSlidesByFrontmatter(
+    markdown,
+    rendererConfig.isEnabledMarp,
+  );
 
 
   const isNotFound = isNotFoundMeta || page == null || shareLink == null;
   const isNotFound = isNotFoundMeta || page == null || shareLink == null;
 
 
@@ -61,44 +71,55 @@ export const ShareLinkPageView = memo((props: Props): JSX.Element => {
     }
     }
   }, [disableLinkSharing, props.disableLinkSharing]);
   }, [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 = () => {
+  const Contents = useCallback(() => {
     if (isNotFound || page.revision == null) {
     if (isNotFound || page.revision == null) {
+      // biome-ignore lint/complexity/noUselessFragments: ignore
       return <></>;
       return <></>;
     }
     }
 
 
     if (isExpired) {
     if (isExpired) {
       return (
       return (
-        <>
-          <h2 className="text-muted mt-4">
-            <span className="material-symbols-outlined" aria-hidden="true">block</span>
-            <span> Page is expired</span>
-          </h2>
-        </>
+        <h2 className="text-muted mt-4">
+          <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;
     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} />
+    );
+  }, [
+    isExpired,
+    isSlide,
+    pagePath,
+    viewOptions,
+    page?.revision?.body,
+    rendererConfig,
+    page?.revision,
+    isNotFound,
+    isSlide?.marp,
+  ]);
 
 
   return (
   return (
     <PageViewLayout
     <PageViewLayout
@@ -107,25 +128,30 @@ export const ShareLinkPageView = memo((props: Props): JSX.Element => {
       expandContentWidth={shouldExpandContent}
       expandContentWidth={shouldExpandContent}
       footerContents={footerContents}
       footerContents={footerContents}
     >
     >
-      { specialContents }
-      { specialContents == null && (
+      {specialContents}
+      {specialContents == null && (
         <>
         <>
-          { isNotFound && (
+          {isNotFound && (
             <h2 className="text-muted mt-4">
             <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>
               <span> Page is not found</span>
             </h2>
             </h2>
-          ) }
-          { !isNotFound && (
+          )}
+          {!isNotFound && (
             <>
             <>
-              <ShareLinkAlert expiredAt={shareLink.expiredAt} createdAt={shareLink.createdAt} />
+              <ShareLinkAlert
+                expiredAt={shareLink.expiredAt}
+                createdAt={shareLink.createdAt}
+              />
               <div className="mb-5">
               <div className="mb-5">
                 <Contents />
                 <Contents />
               </div>
               </div>
             </>
             </>
-          ) }
+          )}
         </>
         </>
-      ) }
+      )}
     </PageViewLayout>
     </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 { format } from 'date-fns/format';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
-
+import React from 'react';
 
 
 /**
 /**
  * UserDate
  * UserDate
@@ -10,7 +8,6 @@ import PropTypes from 'prop-types';
  * display date depends on user timezone of user settings
  * display date depends on user timezone of user settings
  */
  */
 export default class UserDate extends React.Component {
 export default class UserDate extends React.Component {
-
   render() {
   render() {
     const date = new Date(this.props.dateTime);
     const date = new Date(this.props.dateTime);
     const dt = format(date, this.props.format);
     const dt = format(date, this.props.format);
@@ -21,7 +18,6 @@ export default class UserDate extends React.Component {
       </span>
       </span>
     );
     );
   }
   }
-
 }
 }
 
 
 UserDate.propTypes = {
 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 type { IUserHasId } from '@growi/core';
 import { UserPicture } from '@growi/ui/dist/components';
 import { UserPicture } from '@growi/ui/dist/components';
+import React, { type JSX } from 'react';
 
 
 import styles from './UserInfo.module.scss';
 import styles from './UserInfo.module.scss';
 
 
-
 export type UserInfoProps = {
 export type UserInfoProps = {
-  author?: IUserHasId,
-}
+  author?: IUserHasId;
+};
 
 
 export const UserInfo = (props: UserInfoProps): JSX.Element => {
 export const UserInfo = (props: UserInfoProps): JSX.Element => {
-
   const { author } = props;
   const { author } = props;
 
 
   if (author == null || author.status === 4) {
   if (author == null || author.status === 4) {
@@ -19,29 +16,31 @@ export const UserInfo = (props: UserInfoProps): JSX.Element => {
   }
   }
 
 
   return (
   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 />
       <UserPicture user={author} noTooltip noLink />
       <div className="users-meta">
       <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">
         <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="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>
           <span className="user-page-email me-2">
           <span className="user-page-email me-2">
             <span className="material-symbols-outlined me-1">mail</span>
             <span className="material-symbols-outlined me-1">mail</span>
-            { author.isEmailPublished
-              ? author.email
-              : '*****'
-            }
+            {author.isEmailPublished ? author.email : '*****'}
           </span>
           </span>
-          { author.introduction && (
-            <span className="user-page-introduction">{author.introduction}</span>
-          ) }
+          {author.introduction && (
+            <span className="user-page-introduction">
+              {author.introduction}
+            </span>
+          )}
         </div>
         </div>
       </div>
       </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 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 { pagePathUtils } from '@growi/core/dist/utils';
 import Link from 'next/link';
 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)) {
   if (user == null || !isPopulated(user)) {
     return <i>(anyone)</i>;
     return <i>(anyone)</i>;
   }
   }

+ 0 - 1
biome.json

@@ -30,7 +30,6 @@
       "!packages/pdf-converter-client/specs",
       "!packages/pdf-converter-client/specs",
       "!apps/app/playwright",
       "!apps/app/playwright",
       "!apps/app/src/client",
       "!apps/app/src/client",
-      "!apps/app/src/components",
       "!apps/app/src/features/openai",
       "!apps/app/src/features/openai",
       "!apps/app/src/pages",
       "!apps/app/src/pages",
       "!apps/app/src/server",
       "!apps/app/src/server",