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

Merge branch 'master' into support/156162-172751-app-services-stores-dir-biome

Futa Arai 5 месяцев назад
Родитель
Сommit
dae2dc5edb
87 измененных файлов с 3833 добавлено и 1987 удалено
  1. 4 6
      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. 273 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. 22 10
      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. 25 22
      apps/app/src/components/Layout/AdminLayout.tsx
  16. 109 33
      apps/app/src/components/Layout/BasicLayout.tsx
  17. 13 16
      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. 23 13
      apps/app/src/components/Layout/ShareLinkLayout.tsx
  21. 4 1
      apps/app/src/components/Navbar/GroundGlassBar.tsx
  22. 195 116
      apps/app/src/components/PageView/PageAlerts/FixPageGrantAlert.tsx
  23. 13 7
      apps/app/src/components/PageView/PageAlerts/FullTextSearchNotCoverAlert.tsx
  24. 17 7
      apps/app/src/components/PageView/PageAlerts/OldRevisionAlert.tsx
  25. 20 8
      apps/app/src/components/PageView/PageAlerts/PageAlerts.tsx
  26. 9 9
      apps/app/src/components/PageView/PageAlerts/PageGrantAlert.tsx
  27. 15 9
      apps/app/src/components/PageView/PageAlerts/PageRedirectedAlert.tsx
  28. 10 9
      apps/app/src/components/PageView/PageAlerts/PageStaleAlert.tsx
  29. 58 20
      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. 121 56
      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. 3 4
      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. 17 14
      apps/app/src/components/ShareLinkPageView/ShareLinkAlert.tsx
  40. 82 48
      apps/app/src/components/ShareLinkPageView/ShareLinkPageView.tsx
  41. 0 4
      apps/app/src/components/User/UserDate.jsx
  42. 17 18
      apps/app/src/components/User/UserInfo.tsx
  43. 7 6
      apps/app/src/components/User/Username.tsx
  44. 396 181
      apps/app/src/pages/[[...path]].page.tsx
  45. 25 14
      apps/app/src/pages/_app.page.tsx
  46. 47 27
      apps/app/src/pages/_document.page.tsx
  47. 2 4
      apps/app/src/pages/_error.page.tsx
  48. 86 33
      apps/app/src/pages/_private-legacy-pages.page.tsx
  49. 92 46
      apps/app/src/pages/_search.page.tsx
  50. 21 7
      apps/app/src/pages/admin/[...path].page.tsx
  51. 39 14
      apps/app/src/pages/admin/ai-integration.page.tsx
  52. 26 13
      apps/app/src/pages/admin/app.page.tsx
  53. 42 18
      apps/app/src/pages/admin/audit-log.page.tsx
  54. 42 19
      apps/app/src/pages/admin/customize.page.tsx
  55. 35 15
      apps/app/src/pages/admin/data-transfer.page.tsx
  56. 25 12
      apps/app/src/pages/admin/export.page.tsx
  57. 46 26
      apps/app/src/pages/admin/global-notification/[globalNotificationId].page.tsx
  58. 26 13
      apps/app/src/pages/admin/global-notification/new.page.tsx
  59. 25 14
      apps/app/src/pages/admin/importer.page.tsx
  60. 41 22
      apps/app/src/pages/admin/index.page.tsx
  61. 26 13
      apps/app/src/pages/admin/markdown.page.tsx
  62. 25 14
      apps/app/src/pages/admin/notification.page.tsx
  63. 26 14
      apps/app/src/pages/admin/plugins.page.tsx
  64. 35 14
      apps/app/src/pages/admin/search.page.tsx
  65. 58 24
      apps/app/src/pages/admin/security.page.tsx
  66. 30 14
      apps/app/src/pages/admin/slack-integration-legacy.page.tsx
  67. 33 15
      apps/app/src/pages/admin/slack-integration.page.tsx
  68. 41 16
      apps/app/src/pages/admin/user-group-detail/[userGroupId].page.tsx
  69. 34 14
      apps/app/src/pages/admin/user-groups.page.tsx
  70. 27 14
      apps/app/src/pages/admin/users/external-accounts.page.tsx
  71. 34 16
      apps/app/src/pages/admin/users/index.page.tsx
  72. 53 20
      apps/app/src/pages/forgot-password-errors.page.tsx
  73. 36 11
      apps/app/src/pages/forgot-password.page.tsx
  74. 74 24
      apps/app/src/pages/installer.page.tsx
  75. 48 19
      apps/app/src/pages/invited.page.tsx
  76. 72 59
      apps/app/src/pages/login/error/[message].page.tsx
  77. 88 52
      apps/app/src/pages/login/index.page.tsx
  78. 29 10
      apps/app/src/pages/maintenance.page.tsx
  79. 116 61
      apps/app/src/pages/me/[[...path]].page.tsx
  80. 44 15
      apps/app/src/pages/reset-password.page.tsx
  81. 146 76
      apps/app/src/pages/share/[[...path]].page.tsx
  82. 76 61
      apps/app/src/pages/tags.page.tsx
  83. 67 38
      apps/app/src/pages/trash.page.tsx
  84. 42 20
      apps/app/src/pages/user-activation.page.tsx
  85. 98 55
      apps/app/src/pages/utils/commons.ts
  86. 3 3
      apps/app/src/pages/utils/objectid-transformer.ts
  87. 1 6
      biome.json

+ 4 - 6
apps/app/.eslintrc.js

@@ -2,12 +2,8 @@
  * @type {import('eslint').Linter.Config}
  */
 module.exports = {
-  extends: [
-    'next/core-web-vitals',
-    'weseek/react',
-  ],
-  plugins: [
-  ],
+  extends: ['next/core-web-vitals', 'weseek/react'],
+  plugins: [],
   ignorePatterns: [
     'dist/**',
     '**/dist/**',
@@ -49,8 +45,10 @@ module.exports = {
     'src/stores-universal/**',
     'src/interfaces/**',
     'src/utils/**',
+    'src/components/**',
     'src/services/**',
     'src/stores/**',
+    'src/pages/**',
   ],
   settings: {
     // resolve path aliases by eslint-import-resolver-typescript

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

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

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

@@ -27,8 +27,12 @@ const MailSetting = (props: Props) => {
     register,
     handleSubmit,
     reset,
+    watch,
   } = 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
   useEffect(() => {
     reset({
@@ -92,7 +96,7 @@ const MailSetting = (props: Props) => {
       {!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="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>
         <div className="col-md-6">
           <input
@@ -126,11 +130,12 @@ const MailSetting = (props: Props) => {
         </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="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}>
             { t('Update') }
           </button>

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

@@ -18,7 +18,7 @@ const SesSetting = (props: Props): JSX.Element => {
 
   return (
     <React.Fragment>
-      <div id="mail-ses" className="tab-pane active mt-5">
+      <div id="mail-ses" className="tab-pane active">
 
         <div className="row">
           <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 (
     <React.Fragment>
-      <div id="mail-smtp" className="tab-pane active mt-5">
+      <div id="mail-smtp" className="tab-pane active">
         <div className="row">
           <label className="text-start text-md-end col-md-3 col-form-label">
             {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 (
     <div className="row my-3">
-      <div className="mx-auto">
+      <div className="col-md-3"></div>
+      <div className="col-md-9">
         <button
           // eslint-disable-next-line react/button-has-type
           type={props.type ?? 'button'}

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

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

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

@@ -12,6 +12,7 @@ const GrowiLogo = memo(() => (
       height="32"
       viewBox="0 0 64 56"
     >
+      <title>GROWI</title>
       <path
         // 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"

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

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

+ 22 - 10
apps/app/src/components/Common/PagePathNav/PagePathNav.tsx

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

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

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

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

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

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

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

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

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

+ 25 - 22
apps/app/src/components/Layout/AdminLayout.tsx

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

+ 109 - 33
apps/app/src/components/Layout/BasicLayout.tsx

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

+ 13 - 16
apps/app/src/components/Layout/NoLoginLayout.tsx

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

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

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

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

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

+ 23 - 13
apps/app/src/components/Layout/ShareLinkLayout.tsx

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

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

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

+ 195 - 116
apps/app/src/components/PageView/PageAlerts/FixPageGrantAlert.tsx

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

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

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

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

+ 20 - 8
apps/app/src/components/PageView/PageAlerts/PageAlerts.tsx

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

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

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

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

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

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

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

+ 58 - 20
apps/app/src/components/PageView/PageAlerts/TrashPageAlert.tsx

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

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

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

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

+ 121 - 56
apps/app/src/components/PageView/PageView.tsx

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

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

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

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

@@ -1,5 +1,4 @@
 import React, { type JSX } from 'react';
-
 import type { FallbackProps } from 'react-error-boundary';
 import { ErrorBoundary } from 'react-error-boundary';
 import ReactMarkdown from 'react-markdown';
@@ -9,31 +8,35 @@ import loggerFactory from '~/utils/logger';
 
 import 'katex/dist/katex.min.css';
 
-
 const logger = loggerFactory('components:Page:RevisionRenderer');
 
 type Props = {
-  rendererOptions: RendererOptions,
-  markdown: string,
-  additionalClassName?: string,
-}
-
-const ErrorFallback: React.FC<FallbackProps> = React.memo(({ error, resetErrorBoundary }) => {
-  return (
-    <div role="alert">
-      <p>Something went wrong:</p>
-      <pre>{error.message}</pre>
-      <button type="button" className="btn btn-primary" onClick={resetErrorBoundary}>Reload</button>
-    </div>
-  );
-});
+  rendererOptions: RendererOptions;
+  markdown: string;
+  additionalClassName?: string;
+};
+
+const ErrorFallback: React.FC<FallbackProps> = React.memo(
+  ({ error, resetErrorBoundary }) => {
+    return (
+      <div role="alert">
+        <p>Something went wrong:</p>
+        <pre>{error.message}</pre>
+        <button
+          type="button"
+          className="btn btn-primary"
+          onClick={resetErrorBoundary}
+        >
+          Reload
+        </button>
+      </div>
+    );
+  },
+);
 ErrorFallback.displayName = 'ErrorFallback';
 
 const RevisionRenderer = React.memo((props: Props): JSX.Element => {
-
-  const {
-    rendererOptions, markdown, additionalClassName,
-  } = props;
+  const { rendererOptions, markdown, additionalClassName } = props;
 
   return (
     <ErrorBoundary FallbackComponent={ErrorFallback}>
@@ -45,7 +48,6 @@ const RevisionRenderer = React.memo((props: Props): JSX.Element => {
       </ReactMarkdown>
     </ErrorBoundary>
   );
-
 });
 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 { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
 
@@ -12,20 +11,21 @@ Object.entries<object>(oneDark).forEach(([key, value]) => {
   }
 });
 
-
 type InlineCodeBlockProps = {
-  children: ReactNode,
-  className?: string,
-}
+  children: ReactNode;
+  className?: string;
+};
 
 const InlineCodeBlockSubstance = (props: InlineCodeBlockProps): JSX.Element => {
   const { children, className, ...rest } = props;
-  return <code className={`code-inline ${className ?? ''}`} {...rest}>{children}</code>;
+  return (
+    <code className={`code-inline ${className ?? ''}`} {...rest}>
+      {children}
+    </code>
+  );
 };
 
-
 function extractChildrenToIgnoreReactNode(children: ReactNode): ReactNode {
-
   if (children == null) {
     return children;
   }
@@ -37,30 +37,46 @@ function extractChildrenToIgnoreReactNode(children: ReactNode): ReactNode {
 
   // Multiple element array
   if (Array.isArray(children) && children.length > 1) {
-    return children.map(node => extractChildrenToIgnoreReactNode(node)).join('');
+    return children
+      .map((node) => extractChildrenToIgnoreReactNode(node))
+      .join('');
   }
 
   // object
   if (typeof children === 'object') {
-    const grandChildren = (children as any).children ?? (children as any).props.children;
+    const grandChildren =
+      (children as any).children ?? (children as any).props.children;
     return extractChildrenToIgnoreReactNode(grandChildren);
   }
 
   return String(children).replace(/\n$/, '');
 }
 
-function CodeBlockSubstance({ lang, children }: { lang: string, children: ReactNode }): JSX.Element {
+function CodeBlockSubstance({
+  lang,
+  children,
+}: {
+  lang: string;
+  children: ReactNode;
+}): JSX.Element {
   // return alternative element
   //   in order to fix "CodeBlock string is be [object Object] if searched"
   // see: https://github.com/growilabs/growi/pull/7484
   //
   // Note: You can also remove this code if the user requests to see the code highlighted in Prism as-is.
 
-  const isSimpleString = typeof children === 'string' || (Array.isArray(children) && children.length === 1 && typeof children[0] === 'string');
+  const isSimpleString =
+    typeof children === 'string' ||
+    (Array.isArray(children) &&
+      children.length === 1 &&
+      typeof children[0] === 'string');
   if (!isSimpleString) {
     return (
       <div style={oneDark['pre[class*="language-"]']}>
-        <code className={`language-${lang}`} style={oneDark['code[class*="language-"]']}>
+        <code
+          className={`language-${lang}`}
+          style={oneDark['code[class*="language-"]']}
+        >
           {children}
         </code>
       </div>
@@ -68,28 +84,27 @@ function CodeBlockSubstance({ lang, children }: { lang: string, children: ReactN
   }
 
   return (
-    <PrismAsyncLight
-      PreTag="div"
-      style={oneDark}
-      language={lang}
-    >
+    <PrismAsyncLight PreTag="div" style={oneDark} language={lang}>
       {extractChildrenToIgnoreReactNode(children)}
     </PrismAsyncLight>
   );
 }
 
 type CodeBlockProps = {
-  children: ReactNode,
-  className?: string,
-  inline?: true,
-}
+  children: ReactNode;
+  className?: string;
+  inline?: true;
+};
 
 export const CodeBlock = (props: CodeBlockProps): JSX.Element => {
-
   // TODO: set border according to the value of 'customize:highlightJsStyleBorder'
   const { className, children, inline } = props;
   if (inline) {
-    return <InlineCodeBlockSubstance className={`code-inline ${className ?? ''}`}>{children}</InlineCodeBlockSubstance>;
+    return (
+      <InlineCodeBlockSubstance className={`code-inline ${className ?? ''}`}>
+        {children}
+      </InlineCodeBlockSubstance>
+    );
   }
 
   const match = /language-(\w+)(:?.+)?/.exec(className || '');
@@ -99,7 +114,11 @@ export const CodeBlock = (props: CodeBlockProps): JSX.Element => {
   return (
     <>
       {name != null && (
-        <cite className={`code-highlighted-title ${styles['code-highlighted-title']}`}>{name}</cite>
+        <cite
+          className={`code-highlighted-title ${styles['code-highlighted-title']}`}
+        >
+          {name}
+        </cite>
       )}
       <CodeBlockSubstance lang={lang}>{children}</CodeBlockSubstance>
     </>

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

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

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

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

+ 17 - 14
apps/app/src/components/ShareLinkPageView/ShareLinkAlert.tsx

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

+ 82 - 48
apps/app/src/components/ShareLinkPageView/ShareLinkPageView.tsx

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

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

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

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

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

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

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

+ 396 - 181
apps/app/src/pages/[[...path]].page.tsx

@@ -1,116 +1,220 @@
-import type { ReactNode, JSX } from 'react';
-import React, { useEffect } from 'react';
-
-import EventEmitter from 'events';
-
-import { isIPageInfo } from '@growi/core';
+import type React from 'react';
+import type { JSX, ReactNode } from 'react';
+import { useEffect } from 'react';
+import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
+import dynamic from 'next/dynamic';
+import Head from 'next/head';
+import { useRouter } from 'next/router';
 import type {
-  IDataWithMeta, IPageInfo, IPagePopulatedToShowRevision,
+  IDataWithMeta,
+  IPageInfo,
+  IPagePopulatedToShowRevision,
 } from '@growi/core';
-import {
-  isClient, pagePathUtils, pathUtils,
-} from '@growi/core/dist/utils';
+import { isIPageInfo } from '@growi/core';
+import { isClient, pagePathUtils, pathUtils } from '@growi/core/dist/utils';
+import EventEmitter from 'events';
 import ExtensibleCustomError from 'extensible-custom-error';
-import type {
-  GetServerSideProps, GetServerSidePropsContext,
-} from 'next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
-import dynamic from 'next/dynamic';
-import Head from 'next/head';
-import { useRouter } from 'next/router';
 import superjson from 'superjson';
 
 import { BasicLayout } from '~/components/Layout/BasicLayout';
 import { PageView } from '~/components/PageView/PageView';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
-import { SupportedAction, type SupportedActionType } from '~/interfaces/activity';
+import {
+  SupportedAction,
+  type SupportedActionType,
+} from '~/interfaces/activity';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import { RegistrationMode } from '~/interfaces/registration-mode';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { CurrentPageYjsData } from '~/interfaces/yjs';
-import type { PageModel, PageDocument } from '~/server/models/page';
+import type { PageDocument, PageModel } from '~/server/models/page';
 import type { PageRedirectModel } from '~/server/models/page-redirect';
 import { useEditorModeClassName } from '~/services/layout/use-editor-mode-class-name';
+import { useEditingMarkdown } from '~/stores/editor';
 import {
+  useCurrentPageId,
+  useIsLatestRevision,
+  useIsNotFound,
+  useSWRMUTxCurrentPage,
+  useSWRxCurrentPage,
+  useTemplateBodyData,
+  useTemplateTagData,
+} from '~/stores/page';
+import { useRedirectFrom } from '~/stores/page-redirect';
+import { useRemoteRevisionId } from '~/stores/remote-latest-page';
+import {
+  useSetupGlobalSocket,
+  useSetupGlobalSocketForPage,
+} from '~/stores/websocket';
+import {
+  useCurrentPageYjsData,
+  useSWRMUTxCurrentPageYjsData,
+} from '~/stores/yjs';
+import {
+  useCsrfToken,
+  useCurrentPathname,
   useCurrentUser,
-  useIsForbidden, useIsSharedUser,
-  useIsEnabledStaleNotification, useIsIdenticalPath,
-  useIsSearchServiceConfigured, useIsSearchServiceReachable, useDisableLinkSharing,
-  useDefaultIndentSize, useIsIndentSizeForced,
-  useIsAclEnabled, useIsSearchPage, useIsEnabledAttachTitleHeader,
-  useCsrfToken, useIsSearchScopeChildrenAsDefault, useIsEnabledMarp, useCurrentPathname,
-  useIsSlackConfigured, useRendererConfig, useGrowiCloudUri,
-  useIsAllReplyShown, useShowPageSideAuthors, useIsContainerFluid, useIsNotCreatable,
-  useIsUploadAllFileAllowed, useIsUploadEnabled, useIsBulkExportPagesEnabled,
+  useDefaultIndentSize,
+  useDisableLinkSharing,
   useElasticsearchMaxBodyLengthToIndex,
+  useGrowiCloudUri,
+  useIsAclEnabled,
+  useIsAiEnabled,
+  useIsAllReplyShown,
+  useIsBulkExportPagesEnabled,
+  useIsContainerFluid,
+  useIsEnabledAttachTitleHeader,
+  useIsEnabledMarp,
+  useIsEnabledStaleNotification,
+  useIsForbidden,
+  useIsGuestUser,
+  useIsIdenticalPath,
+  useIsIndentSizeForced,
   useIsLocalAccountRegistrationEnabled,
-  useIsRomUserAllowedToComment,
+  useIsNotCreatable,
   useIsPdfBulkExportEnabled,
-  useIsAiEnabled, useLimitLearnablePageCountPerAssistant, useIsUsersHomepageDeletionEnabled, useIsGuestUser,
+  useIsRomUserAllowedToComment,
+  useIsSearchPage,
+  useIsSearchScopeChildrenAsDefault,
+  useIsSearchServiceConfigured,
+  useIsSearchServiceReachable,
+  useIsSharedUser,
+  useIsSlackConfigured,
+  useIsUploadAllFileAllowed,
+  useIsUploadEnabled,
+  useIsUsersHomepageDeletionEnabled,
+  useLimitLearnablePageCountPerAssistant,
+  useRendererConfig,
+  useShowPageSideAuthors,
 } from '~/stores-universal/context';
-import { useEditingMarkdown } from '~/stores/editor';
-import {
-  useSWRxCurrentPage, useSWRMUTxCurrentPage, useCurrentPageId,
-  useIsNotFound, useIsLatestRevision, useTemplateTagData, useTemplateBodyData,
-} from '~/stores/page';
-import { useRedirectFrom } from '~/stores/page-redirect';
-import { useRemoteRevisionId } from '~/stores/remote-latest-page';
-import { useSetupGlobalSocket, useSetupGlobalSocketForPage } from '~/stores/websocket';
-import { useCurrentPageYjsData, useSWRMUTxCurrentPageYjsData } from '~/stores/yjs';
 import loggerFactory from '~/utils/logger';
 
 import type { NextPageWithLayout } from './_app.page';
 import type { CommonProps } from './utils/commons';
 import {
-  getNextI18NextConfig, getServerSideCommonProps, generateCustomTitleForPage, useInitSidebarConfig, skipSSR, addActivity,
+  addActivity,
+  generateCustomTitleForPage,
+  getNextI18NextConfig,
+  getServerSideCommonProps,
+  skipSSR,
+  useInitSidebarConfig,
 } from './utils/commons';
 
-
 declare global {
   // eslint-disable-next-line vars-on-top, no-var
   var globalEmitter: EventEmitter;
 }
 
+const GrowiContextualSubNavigationSubstance = dynamic(
+  () => import('~/client/components/Navbar/GrowiContextualSubNavigation'),
+  { ssr: false },
+);
 
-const GrowiContextualSubNavigationSubstance = dynamic(() => import('~/client/components/Navbar/GrowiContextualSubNavigation'), { ssr: false });
-
-const GrowiPluginsActivator = dynamic(() => import('~/features/growi-plugin/client/components').then(mod => mod.GrowiPluginsActivator), { ssr: false });
+const GrowiPluginsActivator = dynamic(
+  () =>
+    import('~/features/growi-plugin/client/components').then(
+      (mod) => mod.GrowiPluginsActivator,
+    ),
+  { ssr: false },
+);
 
-const DisplaySwitcher = dynamic(() => import('~/client/components/Page/DisplaySwitcher').then(mod => mod.DisplaySwitcher), { ssr: false });
-const PageStatusAlert = dynamic(() => import('~/client/components/PageStatusAlert').then(mod => mod.PageStatusAlert), { ssr: false });
+const DisplaySwitcher = dynamic(
+  () =>
+    import('~/client/components/Page/DisplaySwitcher').then(
+      (mod) => mod.DisplaySwitcher,
+    ),
+  { ssr: false },
+);
+const PageStatusAlert = dynamic(
+  () =>
+    import('~/client/components/PageStatusAlert').then(
+      (mod) => mod.PageStatusAlert,
+    ),
+  { ssr: false },
+);
 
-const UnsavedAlertDialog = dynamic(() => import('~/client/components/UnsavedAlertDialog'), { ssr: false });
+const UnsavedAlertDialog = dynamic(
+  () => import('~/client/components/UnsavedAlertDialog'),
+  { ssr: false },
+);
 const DescendantsPageListModal = dynamic(
-  () => import('~/client/components/DescendantsPageListModal').then(mod => mod.DescendantsPageListModal),
+  () =>
+    import('~/client/components/DescendantsPageListModal').then(
+      (mod) => mod.DescendantsPageListModal,
+    ),
+  { ssr: false },
+);
+const DrawioModal = dynamic(
+  () =>
+    import('~/client/components/PageEditor/DrawioModal').then(
+      (mod) => mod.DrawioModal,
+    ),
+  { ssr: false },
+);
+const HandsontableModal = dynamic(
+  () =>
+    import('~/client/components/PageEditor/HandsontableModal').then(
+      (mod) => mod.HandsontableModal,
+    ),
+  { ssr: false },
+);
+const TemplateModal = dynamic(
+  () =>
+    import('~/client/components/TemplateModal').then(
+      (mod) => mod.TemplateModal,
+    ),
+  { ssr: false },
+);
+const LinkEditModal = dynamic(
+  () =>
+    import('~/client/components/PageEditor/LinkEditModal').then(
+      (mod) => mod.LinkEditModal,
+    ),
+  { ssr: false },
+);
+const TagEditModal = dynamic(
+  () =>
+    import('~/client/components/PageTags/TagEditModal').then(
+      (mod) => mod.TagEditModal,
+    ),
+  { ssr: false },
+);
+const ConflictDiffModal = dynamic(
+  () =>
+    import('~/client/components/PageEditor/ConflictDiffModal').then(
+      (mod) => mod.ConflictDiffModal,
+    ),
   { ssr: false },
 );
-const DrawioModal = dynamic(() => import('~/client/components/PageEditor/DrawioModal').then(mod => mod.DrawioModal), { ssr: false });
-const HandsontableModal = dynamic(() => import('~/client/components/PageEditor/HandsontableModal').then(mod => mod.HandsontableModal), { ssr: false });
-const TemplateModal = dynamic(() => import('~/client/components/TemplateModal').then(mod => mod.TemplateModal), { ssr: false });
-const LinkEditModal = dynamic(() => import('~/client/components/PageEditor/LinkEditModal').then(mod => mod.LinkEditModal), { ssr: false });
-const TagEditModal = dynamic(() => import('~/client/components/PageTags/TagEditModal').then(mod => mod.TagEditModal), { ssr: false });
-const ConflictDiffModal = dynamic(() => import('~/client/components/PageEditor/ConflictDiffModal').then(mod => mod.ConflictDiffModal), { ssr: false });
-
-const EditablePageEffects = dynamic(() => import('~/client/components/Page/EditablePageEffects').then(mod => mod.EditablePageEffects), { ssr: false });
 
+const EditablePageEffects = dynamic(
+  () =>
+    import('~/client/components/Page/EditablePageEffects').then(
+      (mod) => mod.EditablePageEffects,
+    ),
+  { ssr: false },
+);
 
 const logger = loggerFactory('growi:pages:all');
 
-const {
-  isPermalink: _isPermalink, isCreatablePage,
-} = pagePathUtils;
+const { isPermalink: _isPermalink, isCreatablePage } = pagePathUtils;
 const { removeHeadingSlash } = pathUtils;
 
-type IPageToShowRevisionWithMeta = IDataWithMeta<IPagePopulatedToShowRevision & PageDocument, IPageInfo>;
+type IPageToShowRevisionWithMeta = IDataWithMeta<
+  IPagePopulatedToShowRevision & PageDocument,
+  IPageInfo
+>;
 type IPageToShowRevisionWithMetaSerialized = IDataWithMeta<string, string>;
 
-superjson.registerCustom<IPageToShowRevisionWithMeta, IPageToShowRevisionWithMetaSerialized>(
+superjson.registerCustom<
+  IPageToShowRevisionWithMeta,
+  IPageToShowRevisionWithMetaSerialized
+>(
   {
     isApplicable: (v): v is IPageToShowRevisionWithMeta => {
-      return v?.data != null
-        && v?.data.toObject != null
-        && isIPageInfo(v.meta);
+      return v?.data != null && v?.data.toObject != null && isIPageInfo(v.meta);
     },
     serialize: (v) => {
       return {
@@ -130,76 +234,81 @@ superjson.registerCustom<IPageToShowRevisionWithMeta, IPageToShowRevisionWithMet
 
 // GrowiContextualSubNavigation for NOT shared page
 type GrowiContextualSubNavigationProps = {
-  isLinkSharingDisabled: boolean,
-}
+  isLinkSharingDisabled: boolean;
+};
 
-const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps): JSX.Element => {
+const GrowiContextualSubNavigation = (
+  props: GrowiContextualSubNavigationProps,
+): JSX.Element => {
   const { isLinkSharingDisabled } = props;
   const { data: currentPage } = useSWRxCurrentPage();
   return (
-    <GrowiContextualSubNavigationSubstance currentPage={currentPage} isLinkSharingDisabled={isLinkSharingDisabled} />
+    <GrowiContextualSubNavigationSubstance
+      currentPage={currentPage}
+      isLinkSharingDisabled={isLinkSharingDisabled}
+    />
   );
 };
 
 type Props = CommonProps & {
-  pageWithMeta: IPageToShowRevisionWithMeta | null,
+  pageWithMeta: IPageToShowRevisionWithMeta | null;
   // pageUser?: any,
   redirectFrom?: string;
 
   // shareLinkId?: string;
-  isLatestRevision?: boolean,
+  isLatestRevision?: boolean;
 
-  isIdenticalPathPage?: boolean,
-  isForbidden: boolean,
-  isNotFound: boolean,
-  isNotCreatable: boolean,
+  isIdenticalPathPage?: boolean;
+  isForbidden: boolean;
+  isNotFound: boolean;
+  isNotCreatable: boolean;
   // isAbleToDeleteCompletely: boolean,
 
-  templateTagData?: string[],
-  templateBodyData?: string,
+  templateTagData?: string[];
+  templateBodyData?: string;
 
-  isLocalAccountRegistrationEnabled: boolean,
+  isLocalAccountRegistrationEnabled: boolean;
 
-  isSearchServiceConfigured: boolean,
-  isSearchServiceReachable: boolean,
-  isSearchScopeChildrenAsDefault: boolean,
-  elasticsearchMaxBodyLengthToIndex: number,
-  isEnabledMarp: boolean,
+  isSearchServiceConfigured: boolean;
+  isSearchServiceReachable: boolean;
+  isSearchScopeChildrenAsDefault: boolean;
+  elasticsearchMaxBodyLengthToIndex: number;
+  isEnabledMarp: boolean;
 
-  isRomUserAllowedToComment: boolean,
+  isRomUserAllowedToComment: boolean;
 
-  sidebarConfig: ISidebarConfig,
+  sidebarConfig: ISidebarConfig;
 
-  isSlackConfigured: boolean,
+  isSlackConfigured: boolean;
   // isMailerSetup: boolean,
-  isAclEnabled: boolean,
+  isAclEnabled: boolean;
   // hasSlackConfig: boolean,
-  drawioUri: string | null,
+  drawioUri: string | null;
   // highlightJsStyle: string,
-  isAllReplyShown: boolean,
-  showPageSideAuthors: boolean,
-  isContainerFluid: boolean,
-  isUploadEnabled: boolean,
-  isUploadAllFileAllowed: boolean,
-  isBulkExportPagesEnabled: boolean,
-  isPdfBulkExportEnabled: boolean,
-  isEnabledStaleNotification: boolean,
-  isEnabledAttachTitleHeader: boolean,
+  isAllReplyShown: boolean;
+  showPageSideAuthors: boolean;
+  isContainerFluid: boolean;
+  isUploadEnabled: boolean;
+  isUploadAllFileAllowed: boolean;
+  isBulkExportPagesEnabled: boolean;
+  isPdfBulkExportEnabled: boolean;
+  isEnabledStaleNotification: boolean;
+  isEnabledAttachTitleHeader: boolean;
   // isEnabledLinebreaks: boolean,
   // isEnabledLinebreaksInComments: boolean,
-  adminPreferredIndentSize: number,
-  isIndentSizeForced: boolean,
-  disableLinkSharing: boolean,
-  skipSSR: boolean,
-  ssrMaxRevisionBodyLength: number,
+  adminPreferredIndentSize: number;
+  isIndentSizeForced: boolean;
+  disableLinkSharing: boolean;
+  skipSSR: boolean;
+  ssrMaxRevisionBodyLength: number;
 
-  yjsData: CurrentPageYjsData,
+  yjsData: CurrentPageYjsData;
 
-  rendererConfig: RendererConfig,
+  rendererConfig: RendererConfig;
 
-  aiEnabled: boolean,
-  limitLearnablePageCountPerAssistant: number,
-  isUsersHomepageDeletionEnabled: boolean,
+  aiEnabled: boolean;
+  limitLearnablePageCountPerAssistant: number;
+  isUsersHomepageDeletionEnabled: boolean;
 };
 
 const Page: NextPageWithLayout<Props> = (props: Props) => {
@@ -256,11 +365,12 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   useIsRomUserAllowedToComment(props.isRomUserAllowedToComment);
 
   useIsAiEnabled(props.aiEnabled);
-  useLimitLearnablePageCountPerAssistant(props.limitLearnablePageCountPerAssistant);
+  useLimitLearnablePageCountPerAssistant(
+    props.limitLearnablePageCountPerAssistant,
+  );
 
   useIsUsersHomepageDeletionEnabled(props.isUsersHomepageDeletionEnabled);
 
-
   const { pageWithMeta } = props;
 
   const pageId = pageWithMeta?.data._id;
@@ -272,10 +382,12 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   const { data: currentPage } = useSWRxCurrentPage(pageWithMeta?.data ?? null); // store initial data
 
   const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
-  const { trigger: mutateCurrentPageYjsDataFromApi } = useSWRMUTxCurrentPageYjsData();
+  const { trigger: mutateCurrentPageYjsDataFromApi } =
+    useSWRMUTxCurrentPageYjsData();
 
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
-  const { data: currentPageId, mutate: mutateCurrentPageId } = useCurrentPageId();
+  const { data: currentPageId, mutate: mutateCurrentPageId } =
+    useCurrentPageId();
   const { data: isGuestUser } = useIsGuestUser();
 
   const { mutate: mutateIsNotFound } = useIsNotFound();
@@ -299,7 +411,7 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
     }
 
     if (currentPageId != null && revisionId != null && !props.isNotFound) {
-      const mutatePageData = async() => {
+      const mutatePageData = async () => {
         const pageData = await mutateCurrentPage();
         mutateEditingMarkdown(pageData?.revision?.body);
       };
@@ -309,24 +421,41 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
       mutatePageData();
     }
   }, [
-    revisionId, currentPageId,
-    mutateCurrentPage, mutateEditingMarkdown,
-    props.isNotFound, props.skipSSR,
+    revisionId,
+    currentPageId,
+    mutateCurrentPage,
+    mutateEditingMarkdown,
+    props.isNotFound,
+    props.skipSSR,
   ]);
 
   // Load current yjs data
   useEffect(() => {
-    if (!isGuestUser && currentPageId != null && revisionId != null && mutateCurrentPageYjsDataFromApi != null && !props.isNotFound) {
+    if (
+      !isGuestUser &&
+      currentPageId != null &&
+      revisionId != null &&
+      mutateCurrentPageYjsDataFromApi != null &&
+      !props.isNotFound
+    ) {
       mutateCurrentPageYjsDataFromApi();
     }
-  }, [isGuestUser, currentPageId, mutateCurrentPageYjsDataFromApi, props.isNotFound, revisionId]);
+  }, [
+    isGuestUser,
+    currentPageId,
+    mutateCurrentPageYjsDataFromApi,
+    props.isNotFound,
+    revisionId,
+  ]);
 
   // sync pathname by Shallow Routing https://nextjs.org/docs/routing/shallow-routing
   useEffect(() => {
     const decodedURI = decodeURI(window.location.pathname);
     if (isClient() && decodedURI !== props.currentPathname) {
       const { search, hash } = window.location;
-      router.replace(`${props.currentPathname}${search}${hash}`, undefined, { shallow: true });
+      router.replace(`${props.currentPathname}${search}${hash}`, undefined, {
+        shallow: true,
+      });
     }
   }, [props.currentPathname, router]);
 
@@ -368,7 +497,8 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
 
   // If the data on the page changes without router.push, pageWithMeta remains old because getServerSideProps() is not executed
   // So preferentially take page data from useSWRxCurrentPage
-  const pagePath = currentPage?.path ?? pageWithMeta?.data.path ?? props.currentPathname;
+  const pagePath =
+    currentPage?.path ?? pageWithMeta?.data.path ?? props.currentPathname;
 
   const title = generateCustomTitleForPage(props, pagePath);
 
@@ -378,8 +508,9 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
         <title>{title}</title>
       </Head>
       <div className="dynamic-layout-root justify-content-between">
-
-        <GrowiContextualSubNavigation isLinkSharingDisabled={props.disableLinkSharing} />
+        <GrowiContextualSubNavigation
+          isLinkSharingDisabled={props.disableLinkSharing}
+        />
 
         <PageView
           className="d-edit-none"
@@ -397,15 +528,18 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   );
 };
 
-
-const BasicLayoutWithEditor = ({ children }: { children?: ReactNode }): JSX.Element => {
+const BasicLayoutWithEditor = ({
+  children,
+}: {
+  children?: ReactNode;
+}): JSX.Element => {
   const editorModeClassName = useEditorModeClassName();
   return <BasicLayout className={editorModeClassName}>{children}</BasicLayout>;
 };
 
 type LayoutProps = Props & {
-  children?: ReactNode
-}
+  children?: ReactNode;
+};
 
 const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
   // init sidebar config with UserUISettings and sidebarConfig
@@ -420,9 +554,7 @@ Page.getLayout = function getLayout(page: React.ReactElement<Props>) {
       <GrowiPluginsActivator />
       <DrawioViewerScript drawioUri={page.props.rendererConfig.drawioUri} />
 
-      <Layout {...page.props}>
-        {page}
-      </Layout>
+      <Layout {...page.props}>{page}</Layout>
       <UnsavedAlertDialog />
       <DescendantsPageListModal />
       <DrawioModal />
@@ -435,23 +567,25 @@ Page.getLayout = function getLayout(page: React.ReactElement<Props>) {
   );
 };
 
-
 function getPageIdFromPathname(currentPathname: string): string | null {
-  return _isPermalink(currentPathname) ? removeHeadingSlash(currentPathname) : null;
+  return _isPermalink(currentPathname)
+    ? removeHeadingSlash(currentPathname)
+    : null;
 }
 
 class MultiplePagesHitsError extends ExtensibleCustomError {
-
   pagePath: string;
 
   constructor(pagePath: string) {
     super(`MultiplePagesHitsError occured by '${pagePath}'`);
     this.pagePath = pagePath;
   }
-
 }
 
-async function injectPageData(context: GetServerSidePropsContext, props: Props): Promise<void> {
+async function injectPageData(
+  context: GetServerSidePropsContext,
+  props: Props,
+): Promise<void> {
   const { model: mongooseModel } = await import('mongoose');
 
   const req: CrowiRequest = context.req as CrowiRequest;
@@ -471,7 +605,8 @@ async function injectPageData(context: GetServerSidePropsContext, props: Props):
 
   if (!isPermalink) {
     // check redirects
-    const chains = await PageRedirect.retrievePageRedirectEndpoints(currentPathname);
+    const chains =
+      await PageRedirect.retrievePageRedirectEndpoints(currentPathname);
     if (chains != null) {
       // overwrite currentPathname
       currentPathname = chains.end.toPath;
@@ -481,13 +616,23 @@ async function injectPageData(context: GetServerSidePropsContext, props: Props):
     }
 
     // check whether the specified page path hits to multiple pages
-    const count = await Page.countByPathAndViewer(currentPathname, user, null, true);
+    const count = await Page.countByPathAndViewer(
+      currentPathname,
+      user,
+      null,
+      true,
+    );
     if (count > 1) {
       throw new MultiplePagesHitsError(currentPathname);
     }
   }
 
-  const pageWithMeta = await pageService.findPageAndMetaDataByViewer(pageId, currentPathname, user, true); // includeEmpty = true, isSharedPage = false
+  const pageWithMeta = await pageService.findPageAndMetaDataByViewer(
+    pageId,
+    currentPathname,
+    user,
+    true,
+  ); // includeEmpty = true, isSharedPage = false
   const { data: page, meta } = pageWithMeta ?? {};
 
   // add user to seen users
@@ -501,7 +646,9 @@ async function injectPageData(context: GetServerSidePropsContext, props: Props):
   if (page != null) {
     page.initLatestRevisionField(revisionId);
     props.isLatestRevision = page.isLatestRevision();
-    const ssrMaxRevisionBodyLength = configManager.getConfig('app:ssrMaxRevisionBodyLength');
+    const ssrMaxRevisionBodyLength = configManager.getConfig(
+      'app:ssrMaxRevisionBodyLength',
+    );
     props.skipSSR = await skipSSR(page, ssrMaxRevisionBodyLength);
     const populatedPage = await page.populateDataToShowRevision(props.skipSSR); // shouldExcludeBody = skipSSR
 
@@ -512,7 +659,10 @@ async function injectPageData(context: GetServerSidePropsContext, props: Props):
   }
 }
 
-async function injectRoutingInformation(context: GetServerSidePropsContext, props: Props): Promise<void> {
+async function injectRoutingInformation(
+  context: GetServerSidePropsContext,
+  props: Props,
+): Promise<void> {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const Page = crowi.model('Page') as PageModel;
@@ -525,15 +675,15 @@ async function injectRoutingInformation(context: GetServerSidePropsContext, prop
 
   if (props.isIdenticalPathPage) {
     props.isNotCreatable = true;
-  }
-  else if (page == null) {
+  } else if (page == null) {
     props.isNotFound = true;
     props.isNotCreatable = !isCreatablePage(currentPathname);
     // check the page is forbidden or just does not exist.
-    const count = isPermalink ? await Page.count({ _id: pageId }) : await Page.count({ path: currentPathname });
+    const count = isPermalink
+      ? await Page.count({ _id: pageId })
+      : await Page.count({ path: currentPathname });
     props.isForbidden = count > 0;
-  }
-  else {
+  } else {
     props.isNotFound = page.isEmpty;
     props.isNotCreatable = false;
     props.isForbidden = false;
@@ -570,23 +720,40 @@ async function injectRoutingInformation(context: GetServerSidePropsContext, prop
 //   }
 // }
 
-function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): void {
+function injectServerConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+): void {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const {
-    configManager, searchService, aclService, fileUploadService,
-    slackIntegrationService, passportService,
+    configManager,
+    searchService,
+    aclService,
+    fileUploadService,
+    slackIntegrationService,
+    passportService,
   } = crowi;
 
   props.aiEnabled = configManager.getConfig('app:aiEnabled');
-  props.limitLearnablePageCountPerAssistant = configManager.getConfig('openai:limitLearnablePageCountPerAssistant');
-  props.isUsersHomepageDeletionEnabled = configManager.getConfig('security:user-homepage-deletion:isEnabled');
+  props.limitLearnablePageCountPerAssistant = configManager.getConfig(
+    'openai:limitLearnablePageCountPerAssistant',
+  );
+  props.isUsersHomepageDeletionEnabled = configManager.getConfig(
+    'security:user-homepage-deletion:isEnabled',
+  );
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceReachable = searchService.isReachable;
-  props.isSearchScopeChildrenAsDefault = configManager.getConfig('customize:isSearchScopeChildrenAsDefault');
-  props.elasticsearchMaxBodyLengthToIndex = configManager.getConfig('app:elasticsearchMaxBodyLengthToIndex');
+  props.isSearchScopeChildrenAsDefault = configManager.getConfig(
+    'customize:isSearchScopeChildrenAsDefault',
+  );
+  props.elasticsearchMaxBodyLengthToIndex = configManager.getConfig(
+    'app:elasticsearchMaxBodyLengthToIndex',
+  );
 
-  props.isRomUserAllowedToComment = configManager.getConfig('security:isRomUserAllowedToComment');
+  props.isRomUserAllowedToComment = configManager.getConfig(
+    'security:isRomUserAllowedToComment',
+  );
 
   props.isSlackConfigured = slackIntegrationService.isSlackConfigured;
   // props.isMailerSetup = mailService.isMailerSetup;
@@ -595,50 +762,90 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
   props.drawioUri = configManager.getConfig('app:drawioUri');
   // props.highlightJsStyle = configManager.getConfig('customize:highlightJsStyle');
   props.isAllReplyShown = configManager.getConfig('customize:isAllReplyShown');
-  props.showPageSideAuthors = configManager.getConfig('customize:showPageSideAuthors');
-  props.isContainerFluid = configManager.getConfig('customize:isContainerFluid');
-  props.isEnabledStaleNotification = configManager.getConfig('customize:isEnabledStaleNotification');
-  props.disableLinkSharing = configManager.getConfig('security:disableLinkSharing');
+  props.showPageSideAuthors = configManager.getConfig(
+    'customize:showPageSideAuthors',
+  );
+  props.isContainerFluid = configManager.getConfig(
+    'customize:isContainerFluid',
+  );
+  props.isEnabledStaleNotification = configManager.getConfig(
+    'customize:isEnabledStaleNotification',
+  );
+  props.disableLinkSharing = configManager.getConfig(
+    'security:disableLinkSharing',
+  );
   props.isUploadAllFileAllowed = fileUploadService.getFileUploadEnabled();
   props.isUploadEnabled = fileUploadService.getIsUploadable();
   // TODO: remove growiCloudUri condition when bulk export can be relased for GROWI.cloud (https://redmine.weseek.co.jp/issues/163220)
-  props.isBulkExportPagesEnabled = configManager.getConfig('app:isBulkExportPagesEnabled') && configManager.getConfig('app:growiCloudUri') == null;
-  props.isPdfBulkExportEnabled = configManager.getConfig('app:pageBulkExportPdfConverterUri') != null;
-
-  props.isLocalAccountRegistrationEnabled = passportService.isLocalStrategySetup
-  && configManager.getConfig('security:registrationMode') !== RegistrationMode.CLOSED;
-
-  props.adminPreferredIndentSize = configManager.getConfig('markdown:adminPreferredIndentSize');
-  props.isIndentSizeForced = configManager.getConfig('markdown:isIndentSizeForced');
+  props.isBulkExportPagesEnabled =
+    configManager.getConfig('app:isBulkExportPagesEnabled') &&
+    configManager.getConfig('app:growiCloudUri') == null;
+  props.isPdfBulkExportEnabled =
+    configManager.getConfig('app:pageBulkExportPdfConverterUri') != null;
+
+  props.isLocalAccountRegistrationEnabled =
+    passportService.isLocalStrategySetup &&
+    configManager.getConfig('security:registrationMode') !==
+      RegistrationMode.CLOSED;
+
+  props.adminPreferredIndentSize = configManager.getConfig(
+    'markdown:adminPreferredIndentSize',
+  );
+  props.isIndentSizeForced = configManager.getConfig(
+    'markdown:isIndentSizeForced',
+  );
 
-  props.isEnabledAttachTitleHeader = configManager.getConfig('customize:isEnabledAttachTitleHeader');
+  props.isEnabledAttachTitleHeader = configManager.getConfig(
+    'customize:isEnabledAttachTitleHeader',
+  );
 
   props.sidebarConfig = {
-    isSidebarCollapsedMode: configManager.getConfig('customize:isSidebarCollapsedMode'),
-    isSidebarClosedAtDockMode: configManager.getConfig('customize:isSidebarClosedAtDockMode'),
+    isSidebarCollapsedMode: configManager.getConfig(
+      'customize:isSidebarCollapsedMode',
+    ),
+    isSidebarClosedAtDockMode: configManager.getConfig(
+      'customize:isSidebarClosedAtDockMode',
+    ),
   };
 
   props.rendererConfig = {
-    isEnabledLinebreaks: configManager.getConfig('markdown:isEnabledLinebreaks'),
-    isEnabledLinebreaksInComments: configManager.getConfig('markdown:isEnabledLinebreaksInComments'),
+    isEnabledLinebreaks: configManager.getConfig(
+      'markdown:isEnabledLinebreaks',
+    ),
+    isEnabledLinebreaksInComments: configManager.getConfig(
+      'markdown:isEnabledLinebreaksInComments',
+    ),
     isEnabledMarp: configManager.getConfig('customize:isEnabledMarp'),
-    adminPreferredIndentSize: configManager.getConfig('markdown:adminPreferredIndentSize'),
+    adminPreferredIndentSize: configManager.getConfig(
+      'markdown:adminPreferredIndentSize',
+    ),
     isIndentSizeForced: configManager.getConfig('markdown:isIndentSizeForced'),
 
     drawioUri: configManager.getConfig('app:drawioUri'),
     plantumlUri: configManager.getConfig('app:plantumlUri'),
 
     // XSS Options
-    isEnabledXssPrevention: configManager.getConfig('markdown:rehypeSanitize:isEnabledPrevention'),
+    isEnabledXssPrevention: configManager.getConfig(
+      'markdown:rehypeSanitize:isEnabledPrevention',
+    ),
     sanitizeType: configManager.getConfig('markdown:rehypeSanitize:option'),
-    customTagWhitelist: configManager.getConfig('markdown:rehypeSanitize:tagNames'),
-    customAttrWhitelist: configManager.getConfig('markdown:rehypeSanitize:attributes') != null
-      ? JSON.parse(configManager.getConfig('markdown:rehypeSanitize:attributes'))
-      : undefined,
-    highlightJsStyleBorder: configManager.getConfig('customize:highlightJsStyleBorder'),
+    customTagWhitelist: configManager.getConfig(
+      'markdown:rehypeSanitize:tagNames',
+    ),
+    customAttrWhitelist:
+      configManager.getConfig('markdown:rehypeSanitize:attributes') != null
+        ? JSON.parse(
+            configManager.getConfig('markdown:rehypeSanitize:attributes'),
+          )
+        : undefined,
+    highlightJsStyleBorder: configManager.getConfig(
+      'customize:highlightJsStyleBorder',
+    ),
   };
 
-  props.ssrMaxRevisionBodyLength = configManager.getConfig('app:ssrMaxRevisionBodyLength');
+  props.ssrMaxRevisionBodyLength = configManager.getConfig(
+    'app:ssrMaxRevisionBodyLength',
+  );
 }
 
 /**
@@ -647,8 +854,16 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
  * @param props
  * @param namespacesRequired
  */
-async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
-  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+async function injectNextI18NextConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+  namespacesRequired?: string[] | undefined,
+): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(
+    serverSideTranslations,
+    context,
+    namespacesRequired,
+  );
   props._nextI18Next = nextI18NextConfig._nextI18Next;
 }
 
@@ -668,7 +883,9 @@ const getAction = (props: Props): SupportedActionType => {
   return SupportedAction.ACTION_PAGE_VIEW;
 };
 
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const req = context.req as CrowiRequest;
   const { user } = req;
 
@@ -697,12 +914,10 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
 
   try {
     await injectPageData(context, props);
-  }
-  catch (err) {
+  } catch (err) {
     if (err instanceof MultiplePagesHitsError) {
       props.isIdenticalPathPage = true;
-    }
-    else {
+    } else {
       throw err;
     }
   }

+ 25 - 14
apps/app/src/pages/_app.page.tsx

@@ -1,12 +1,11 @@
-import type { ReactNode, JSX } from 'react';
+import type { JSX, ReactNode } from 'react';
 import React, { useEffect } from 'react';
-
-import type { Locale } from '@growi/core/dist/interfaces';
 import type { NextPage } from 'next';
-import { appWithTranslation } from 'next-i18next';
 import type { AppContext, AppProps } from 'next/app';
 import App from 'next/app';
 import { useRouter } from 'next/router';
+import type { Locale } from '@growi/core/dist/interfaces';
+import { appWithTranslation } from 'next-i18next';
 import { SWRConfig } from 'swr';
 
 import * as nextI18nConfig from '^/config/next-i18next.config';
@@ -14,29 +13,39 @@ import * as nextI18nConfig from '^/config/next-i18next.config';
 import { GlobalFonts } from '~/components/FontFamily/GlobalFonts';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import {
-  useAppTitle, useConfidential, useGrowiVersion, useSiteUrl, useIsDefaultLogo, useForcedColorScheme,
+  useAppTitle,
+  useConfidential,
+  useForcedColorScheme,
+  useGrowiVersion,
+  useIsDefaultLogo,
+  useSiteUrl,
 } from '~/stores-universal/context';
 import { swrGlobalConfiguration } from '~/utils/swr-utils';
 
-import { getLocaleAtServerSide, type CommonProps } from './utils/commons';
+import { type CommonProps, getLocaleAtServerSide } from './utils/commons';
 import '~/styles/prebuilt/vendor.css';
 import '~/styles/style-app.scss';
+
 import { registerTransformerForObjectId } from './utils/objectid-transformer';
 
 // eslint-disable-next-line @typescript-eslint/ban-types
 export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
-  getLayout?: (page: JSX.Element) => ReactNode,
-}
+  getLayout?: (page: JSX.Element) => ReactNode;
+};
 
 type GrowiAppProps = AppProps & {
-  Component: NextPageWithLayout,
-  userLocale: Locale,
+  Component: NextPageWithLayout;
+  userLocale: Locale;
 };
 
 // register custom serializer
 registerTransformerForObjectId();
 
-function GrowiApp({ Component, pageProps, userLocale }: GrowiAppProps): JSX.Element {
+function GrowiApp({
+  Component,
+  pageProps,
+  userLocale,
+}: GrowiAppProps): JSX.Element {
   const router = useRouter();
 
   useEffect(() => {
@@ -64,7 +73,7 @@ function GrowiApp({ Component, pageProps, userLocale }: GrowiAppProps): JSX.Elem
   useForcedColorScheme(commonPageProps.forcedColorScheme);
 
   // Use the layout defined at the page level, if available
-  const getLayout = Component.getLayout ?? (page => page);
+  const getLayout = Component.getLayout ?? ((page) => page);
 
   return (
     <>
@@ -76,9 +85,11 @@ function GrowiApp({ Component, pageProps, userLocale }: GrowiAppProps): JSX.Elem
   );
 }
 
-GrowiApp.getInitialProps = async(appContext: AppContext) => {
+GrowiApp.getInitialProps = async (appContext: AppContext) => {
   const appProps = App.getInitialProps(appContext);
-  const userLocale = getLocaleAtServerSide(appContext.ctx.req as unknown as CrowiRequest);
+  const userLocale = getLocaleAtServerSide(
+    appContext.ctx.req as unknown as CrowiRequest,
+  );
 
   return { ...appProps, userLocale };
 };

+ 47 - 27
apps/app/src/pages/_document.page.tsx

@@ -1,11 +1,8 @@
 /* eslint-disable @next/next/google-font-display */
 import React, { type JSX } from 'react';
-
-import type { Locale } from '@growi/core/dist/interfaces';
 import type { DocumentContext, DocumentInitialProps } from 'next/document';
-import Document, {
-  Html, Head, Main, NextScript,
-} from 'next/document';
+import Document, { Head, Html, Main, NextScript } from 'next/document';
+import type { Locale } from '@growi/core/dist/interfaces';
 
 import type { GrowiPluginResourceEntries } from '~/features/growi-plugin/server/services';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
@@ -17,42 +14,50 @@ const logger = loggerFactory('growi:page:_document');
 
 type HeadersForGrowiPluginProps = {
   pluginResourceEntries: GrowiPluginResourceEntries;
-}
-const HeadersForGrowiPlugin = (props: HeadersForGrowiPluginProps): JSX.Element => {
+};
+const HeadersForGrowiPlugin = (
+  props: HeadersForGrowiPluginProps,
+): JSX.Element => {
   const { pluginResourceEntries } = props;
 
   return (
     <>
-      { pluginResourceEntries.map(([installedPath, href]) => {
+      {pluginResourceEntries.map(([installedPath, href]) => {
         if (href.endsWith('.js')) {
           // eslint-disable-next-line @next/next/no-sync-scripts
-          return <script type="module" key={`script_${installedPath}`} src={href} />;
+          return (
+            <script type="module" key={`script_${installedPath}`} src={href} />
+          );
         }
         if (href.endsWith('.css')) {
           // eslint-disable-next-line @next/next/no-sync-scripts
-          return <link rel="stylesheet" key={`link_${installedPath}`} href={href} />;
+          return (
+            <link rel="stylesheet" key={`link_${installedPath}`} href={href} />
+          );
         }
         return <></>;
-      }) }
+      })}
     </>
   );
 };
 
 interface GrowiDocumentProps {
-  themeHref: string,
-  customScript: string | null,
-  customCss: string | null,
-  customNoscript: string | null,
+  themeHref: string;
+  customScript: string | null;
+  customCss: string | null;
+  customNoscript: string | null;
   pluginResourceEntries: GrowiPluginResourceEntries;
   locale: Locale;
 }
-declare type GrowiDocumentInitialProps = DocumentInitialProps & GrowiDocumentProps;
+declare type GrowiDocumentInitialProps = DocumentInitialProps &
+  GrowiDocumentProps;
 
 class GrowiDocument extends Document<GrowiDocumentInitialProps> {
-
-  static override async getInitialProps(ctx: DocumentContext): Promise<GrowiDocumentInitialProps> {
-
-    const initialProps: DocumentInitialProps = await Document.getInitialProps(ctx);
+  static override async getInitialProps(
+    ctx: DocumentContext,
+  ): Promise<GrowiDocumentInitialProps> {
+    const initialProps: DocumentInitialProps =
+      await Document.getInitialProps(ctx);
     const req = ctx.req as CrowiRequest;
     const { crowi } = req;
     const { customizeService } = crowi;
@@ -63,8 +68,11 @@ class GrowiDocument extends Document<GrowiDocumentInitialProps> {
     const customNoscript: string | null = customizeService.getCustomNoscript();
 
     // retrieve plugin manifests
-    const growiPluginService = await import('~/features/growi-plugin/server/services').then(mod => mod.growiPluginService);
-    const pluginResourceEntries = await growiPluginService.retrieveAllPluginResourceEntries();
+    const growiPluginService = await import(
+      '~/features/growi-plugin/server/services'
+    ).then((mod) => mod.growiPluginService);
+    const pluginResourceEntries =
+      await growiPluginService.retrieveAllPluginResourceEntries();
 
     const locale = getLocaleAtServerSide(req);
 
@@ -83,13 +91,20 @@ class GrowiDocument extends Document<GrowiDocumentInitialProps> {
     if (customScript == null || customScript.length === 0) {
       return <></>;
     }
-    return <script id="customScript" dangerouslySetInnerHTML={{ __html: customScript }} />;
+    return (
+      <script
+        id="customScript"
+        // biome-ignore lint/security/noDangerouslySetInnerHtml: ignore
+        dangerouslySetInnerHTML={{ __html: customScript }}
+      />
+    );
   }
 
   renderCustomCss(customCss: string | null): JSX.Element {
     if (customCss == null || customCss.length === 0) {
       return <></>;
     }
+    // biome-ignore lint/security/noDangerouslySetInnerHtml: ignore
     return <style dangerouslySetInnerHTML={{ __html: customCss }} />;
   }
 
@@ -97,13 +112,17 @@ class GrowiDocument extends Document<GrowiDocumentInitialProps> {
     if (customNoscript == null || customNoscript.length === 0) {
       return <></>;
     }
+    // biome-ignore lint/security/noDangerouslySetInnerHtml: ignore
     return <noscript dangerouslySetInnerHTML={{ __html: customNoscript }} />;
   }
 
   override render(): JSX.Element {
     const {
-      customCss, customScript, customNoscript,
-      themeHref, pluginResourceEntries,
+      customCss,
+      customScript,
+      customNoscript,
+      themeHref,
+      pluginResourceEntries,
       locale,
     } = this.props;
 
@@ -114,7 +133,9 @@ class GrowiDocument extends Document<GrowiDocumentInitialProps> {
           <link rel="stylesheet" key="link-theme" href={themeHref} />
           <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
           <link rel="alternate icon" href="/favicon.ico" />
-          <HeadersForGrowiPlugin pluginResourceEntries={pluginResourceEntries} />
+          <HeadersForGrowiPlugin
+            pluginResourceEntries={pluginResourceEntries}
+          />
           {this.renderCustomCss(customCss)}
         </Head>
         <body>
@@ -125,7 +146,6 @@ class GrowiDocument extends Document<GrowiDocumentInitialProps> {
       </Html>
     );
   }
-
 }
 
 export default GrowiDocument;

+ 2 - 4
apps/app/src/pages/_error.page.tsx

@@ -1,12 +1,10 @@
 import type { JSX } from 'react';
-
 import type { NextPageContext } from 'next';
 import type { ErrorProps } from 'next/error';
-import Error from 'next/error';
-
+import NextError from 'next/error';
 
 export default function ErrorPage(props: ErrorProps): JSX.Element {
-  return <Error {...props} />;
+  return <NextError {...props} />;
 }
 
 // add getInitialProps to disable "https://nextjs.org/docs/messages/prerender-error"

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

@@ -1,47 +1,66 @@
-import type { IUser } from '@growi/core';
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
-import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import type { IUser } from '@growi/core';
+import { useTranslation } from 'next-i18next';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { ISidebarConfig } from '~/interfaces/sidebar-config';
+import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 import {
-  useCsrfToken, useCurrentUser, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
-  useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig, useGrowiCloudUri, useIsEnabledMarp, useCurrentPathname,
+  useCsrfToken,
+  useCurrentPathname,
+  useCurrentUser,
+  useGrowiCloudUri,
+  useIsEnabledMarp,
+  useIsSearchPage,
+  useIsSearchScopeChildrenAsDefault,
+  useIsSearchServiceConfigured,
+  useIsSearchServiceReachable,
+  useRendererConfig,
 } from '~/stores-universal/context';
-import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 
 import type { CommonProps } from './utils/commons';
 import {
-  getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle, useInitSidebarConfig,
+  generateCustomTitle,
+  getNextI18NextConfig,
+  getServerSideCommonProps,
+  useInitSidebarConfig,
 } from './utils/commons';
 
-const SearchResultLayout = dynamic(() => import('~/components/Layout/SearchResultLayout'), { ssr: false });
+const SearchResultLayout = dynamic(
+  () => import('~/components/Layout/SearchResultLayout'),
+  { ssr: false },
+);
 
 type Props = CommonProps & {
-  currentUser: IUser,
+  currentUser: IUser;
 
-  isSearchServiceConfigured: boolean,
-  isSearchServiceReachable: boolean,
-  isSearchScopeChildrenAsDefault: boolean,
-  isEnabledMarp: boolean,
+  isSearchServiceConfigured: boolean;
+  isSearchServiceReachable: boolean;
+  isSearchScopeChildrenAsDefault: boolean;
+  isEnabledMarp: boolean;
 
   // Render config
-  rendererConfig: RendererConfig,
+  rendererConfig: RendererConfig;
 
-  sidebarConfig: ISidebarConfig,
+  sidebarConfig: ISidebarConfig;
 };
 
 const PrivateLegacyPage: NextPage<Props> = (props: Props) => {
   const { t } = useTranslation();
 
-  const PrivateLegacyPages = dynamic(() => import('~/client/components/PrivateLegacyPages'), { ssr: false });
+  const PrivateLegacyPages = dynamic(
+    () => import('~/client/components/PrivateLegacyPages'),
+    { ssr: false },
+  );
 
   // commons
   useCsrfToken(props.csrfToken);
@@ -87,39 +106,63 @@ const PrivateLegacyPage: NextPage<Props> = (props: Props) => {
   );
 };
 
-async function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): Promise<void> {
+async function injectServerConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+): Promise<void> {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { configManager, searchService } = crowi;
 
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceReachable = searchService.isReachable;
-  props.isSearchScopeChildrenAsDefault = configManager.getConfig('customize:isSearchScopeChildrenAsDefault');
+  props.isSearchScopeChildrenAsDefault = configManager.getConfig(
+    'customize:isSearchScopeChildrenAsDefault',
+  );
   props.isEnabledMarp = configManager.getConfig('customize:isEnabledMarp');
 
   props.sidebarConfig = {
-    isSidebarCollapsedMode: configManager.getConfig('customize:isSidebarCollapsedMode'),
-    isSidebarClosedAtDockMode: configManager.getConfig('customize:isSidebarClosedAtDockMode'),
+    isSidebarCollapsedMode: configManager.getConfig(
+      'customize:isSidebarCollapsedMode',
+    ),
+    isSidebarClosedAtDockMode: configManager.getConfig(
+      'customize:isSidebarClosedAtDockMode',
+    ),
   };
 
   props.rendererConfig = {
-    isEnabledLinebreaks: configManager.getConfig('markdown:isEnabledLinebreaks'),
-    isEnabledLinebreaksInComments: configManager.getConfig('markdown:isEnabledLinebreaksInComments'),
+    isEnabledLinebreaks: configManager.getConfig(
+      'markdown:isEnabledLinebreaks',
+    ),
+    isEnabledLinebreaksInComments: configManager.getConfig(
+      'markdown:isEnabledLinebreaksInComments',
+    ),
     isEnabledMarp: configManager.getConfig('customize:isEnabledMarp'),
-    adminPreferredIndentSize: configManager.getConfig('markdown:adminPreferredIndentSize'),
+    adminPreferredIndentSize: configManager.getConfig(
+      'markdown:adminPreferredIndentSize',
+    ),
     isIndentSizeForced: configManager.getConfig('markdown:isIndentSizeForced'),
 
     drawioUri: configManager.getConfig('app:drawioUri'),
     plantumlUri: configManager.getConfig('app:plantumlUri'),
 
     // XSS Options
-    isEnabledXssPrevention: configManager.getConfig('markdown:rehypeSanitize:isEnabledPrevention'),
+    isEnabledXssPrevention: configManager.getConfig(
+      'markdown:rehypeSanitize:isEnabledPrevention',
+    ),
     sanitizeType: configManager.getConfig('markdown:rehypeSanitize:option'),
-    customTagWhitelist: crowi.configManager.getConfig('markdown:rehypeSanitize:tagNames'),
-    customAttrWhitelist: configManager.getConfig('markdown:rehypeSanitize:attributes') != null
-      ? JSON.parse(configManager.getConfig('markdown:rehypeSanitize:attributes'))
-      : undefined,
-    highlightJsStyleBorder: crowi.configManager.getConfig('customize:highlightJsStyleBorder'),
+    customTagWhitelist: crowi.configManager.getConfig(
+      'markdown:rehypeSanitize:tagNames',
+    ),
+    customAttrWhitelist:
+      configManager.getConfig('markdown:rehypeSanitize:attributes') != null
+        ? JSON.parse(
+            configManager.getConfig('markdown:rehypeSanitize:attributes'),
+          )
+        : undefined,
+    highlightJsStyleBorder: crowi.configManager.getConfig(
+      'customize:highlightJsStyleBorder',
+    ),
   };
 }
 
@@ -129,12 +172,22 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
  * @param props
  * @param namespacesRequired
  */
-async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
-  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+async function injectNextI18NextConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+  namespacesRequired?: string[] | undefined,
+): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(
+    serverSideTranslations,
+    context,
+    namespacesRequired,
+  );
   props._nextI18Next = nextI18NextConfig._nextI18Next;
 }
 
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const req = context.req as CrowiRequest;
   const { user } = req;
 

+ 92 - 46
apps/app/src/pages/_search.page.tsx

@@ -1,49 +1,61 @@
-import type { ReactNode, JSX } from 'react';
-
-import type { IUser } from '@growi/core';
+import type { JSX, ReactNode } from 'react';
 import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
-import { useTranslation } from 'next-i18next';
-import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import type { IUser } from '@growi/core';
+import { useTranslation } from 'next-i18next';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 
 import SearchResultLayout from '~/components/Layout/SearchResultLayout';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { ISidebarConfig } from '~/interfaces/sidebar-config';
+import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 import {
-  useCsrfToken, useCurrentUser, useIsContainerFluid, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
-  useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig, useShowPageLimitationL, useGrowiCloudUri, useCurrentPathname,
+  useCsrfToken,
+  useCurrentPathname,
+  useCurrentUser,
+  useGrowiCloudUri,
+  useIsContainerFluid,
+  useIsSearchPage,
+  useIsSearchScopeChildrenAsDefault,
+  useIsSearchServiceConfigured,
+  useIsSearchServiceReachable,
+  useRendererConfig,
+  useShowPageLimitationL,
 } from '~/stores-universal/context';
-import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 
 import type { NextPageWithLayout } from './_app.page';
 import type { CommonProps } from './utils/commons';
 import {
-  getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle, useInitSidebarConfig,
+  generateCustomTitle,
+  getNextI18NextConfig,
+  getServerSideCommonProps,
+  useInitSidebarConfig,
 } from './utils/commons';
 
-
-const SearchPage = dynamic(() => import('~/client/components/SearchPage').then(mod => mod.SearchPage), { ssr: false });
-
+const SearchPage = dynamic(
+  () => import('~/client/components/SearchPage').then((mod) => mod.SearchPage),
+  { ssr: false },
+);
 
 type Props = CommonProps & {
-  currentUser: IUser,
+  currentUser: IUser;
 
-  isSearchServiceConfigured: boolean,
-  isSearchServiceReachable: boolean,
-  isSearchScopeChildrenAsDefault: boolean,
+  isSearchServiceConfigured: boolean;
+  isSearchServiceReachable: boolean;
+  isSearchScopeChildrenAsDefault: boolean;
 
   // Render config
-  rendererConfig: RendererConfig,
+  rendererConfig: RendererConfig;
 
   // search limit
-  showPageLimitationL: number
+  showPageLimitationL: number;
 
-  isContainerFluid: boolean,
+  isContainerFluid: boolean;
 
-  sidebarConfig: ISidebarConfig,
+  sidebarConfig: ISidebarConfig;
 };
 
 const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
@@ -90,19 +102,15 @@ const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
 };
 
 type LayoutProps = Props & {
-  sidebarConfig: ISidebarConfig,
-  children?: ReactNode,
-}
+  sidebarConfig: ISidebarConfig;
+  children?: ReactNode;
+};
 
 const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
   // init sidebar config with UserUISettings and sidebarConfig
   useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
 
-  return (
-    <SearchResultLayout>
-      {children}
-    </SearchResultLayout>
-  );
+  return <SearchResultLayout>{children}</SearchResultLayout>;
 };
 
 SearchResultPage.getLayout = function getLayout(page) {
@@ -114,42 +122,70 @@ SearchResultPage.getLayout = function getLayout(page) {
   );
 };
 
-function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): void {
+function injectServerConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+): void {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { configManager, searchService } = crowi;
 
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceReachable = searchService.isReachable;
-  props.isSearchScopeChildrenAsDefault = configManager.getConfig('customize:isSearchScopeChildrenAsDefault');
-  props.isContainerFluid = configManager.getConfig('customize:isContainerFluid');
+  props.isSearchScopeChildrenAsDefault = configManager.getConfig(
+    'customize:isSearchScopeChildrenAsDefault',
+  );
+  props.isContainerFluid = configManager.getConfig(
+    'customize:isContainerFluid',
+  );
 
   props.sidebarConfig = {
-    isSidebarCollapsedMode: configManager.getConfig('customize:isSidebarCollapsedMode'),
-    isSidebarClosedAtDockMode: configManager.getConfig('customize:isSidebarClosedAtDockMode'),
+    isSidebarCollapsedMode: configManager.getConfig(
+      'customize:isSidebarCollapsedMode',
+    ),
+    isSidebarClosedAtDockMode: configManager.getConfig(
+      'customize:isSidebarClosedAtDockMode',
+    ),
   };
 
   props.rendererConfig = {
-    isEnabledLinebreaks: configManager.getConfig('markdown:isEnabledLinebreaks'),
-    isEnabledLinebreaksInComments: configManager.getConfig('markdown:isEnabledLinebreaksInComments'),
+    isEnabledLinebreaks: configManager.getConfig(
+      'markdown:isEnabledLinebreaks',
+    ),
+    isEnabledLinebreaksInComments: configManager.getConfig(
+      'markdown:isEnabledLinebreaksInComments',
+    ),
     isEnabledMarp: configManager.getConfig('customize:isEnabledMarp'),
-    adminPreferredIndentSize: configManager.getConfig('markdown:adminPreferredIndentSize'),
+    adminPreferredIndentSize: configManager.getConfig(
+      'markdown:adminPreferredIndentSize',
+    ),
     isIndentSizeForced: configManager.getConfig('markdown:isIndentSizeForced'),
 
     drawioUri: configManager.getConfig('app:drawioUri'),
     plantumlUri: configManager.getConfig('app:plantumlUri'),
 
     // XSS Options
-    isEnabledXssPrevention: configManager.getConfig('markdown:rehypeSanitize:isEnabledPrevention'),
+    isEnabledXssPrevention: configManager.getConfig(
+      'markdown:rehypeSanitize:isEnabledPrevention',
+    ),
     sanitizeType: configManager.getConfig('markdown:rehypeSanitize:option'),
-    customTagWhitelist: crowi.configManager.getConfig('markdown:rehypeSanitize:tagNames'),
-    customAttrWhitelist: configManager.getConfig('markdown:rehypeSanitize:attributes') != null
-      ? JSON.parse(configManager.getConfig('markdown:rehypeSanitize:attributes'))
-      : undefined,
-    highlightJsStyleBorder: crowi.configManager.getConfig('customize:highlightJsStyleBorder'),
+    customTagWhitelist: crowi.configManager.getConfig(
+      'markdown:rehypeSanitize:tagNames',
+    ),
+    customAttrWhitelist:
+      configManager.getConfig('markdown:rehypeSanitize:attributes') != null
+        ? JSON.parse(
+            configManager.getConfig('markdown:rehypeSanitize:attributes'),
+          )
+        : undefined,
+    highlightJsStyleBorder: crowi.configManager.getConfig(
+      'customize:highlightJsStyleBorder',
+    ),
   };
 
-  props.showPageLimitationL = configManager.getConfig('customize:showPageLimitationL');
+  props.showPageLimitationL = configManager.getConfig(
+    'customize:showPageLimitationL',
+  );
 }
 
 /**
@@ -158,12 +194,22 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
  * @param props
  * @param namespacesRequired
  */
-async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
-  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+async function injectNextI18NextConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+  namespacesRequired?: string[] | undefined,
+): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(
+    serverSideTranslations,
+    context,
+    namespacesRequired,
+  );
   props._nextI18Next = nextI18NextConfig._nextI18Next;
 }
 
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const req = context.req as CrowiRequest;
   const { user } = req;
 

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

@@ -1,18 +1,31 @@
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
 import dynamic from 'next/dynamic';
 
 import AdminLayout from '~/components/Layout/AdminLayout';
 import type { CommonProps } from '~/pages/utils/commons';
-import { useCurrentUser } from '~/stores-universal/context';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
+import { useCurrentUser } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
-const AdminNotFoundPage = dynamic(() => import('~/client/components/Admin/NotFoundPage').then(mod => mod.AdminNotFoundPage), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
-
+const AdminNotFoundPage = dynamic(
+  () =>
+    import('~/client/components/Admin/NotFoundPage').then(
+      (mod) => mod.AdminNotFoundPage,
+    ),
+  { ssr: false },
+);
+const ForbiddenPage = dynamic(
+  () =>
+    import('~/client/components/Admin/ForbiddenPage').then(
+      (mod) => mod.ForbiddenPage,
+    ),
+  { ssr: false },
+);
 
 const AdminAppPage: NextPage<CommonProps> = (props) => {
   useIsMaintenanceMode(props.isMaintenanceMode);
@@ -29,10 +42,11 @@ const AdminAppPage: NextPage<CommonProps> = (props) => {
   );
 };
 
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const props = await retrieveServerSideProps(context);
   return props;
 };
 
-
 export default AdminAppPage;

+ 39 - 14
apps/app/src/pages/admin/ai-integration.page.tsx

@@ -1,9 +1,11 @@
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import { useTranslation } from 'next-i18next';
 
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CommonProps } from '~/pages/utils/commons';
@@ -11,15 +13,33 @@ import { generateCustomTitle } from '~/pages/utils/commons';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
-const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
-const AiIntegration = dynamic(() => import('~/features/openai/client/components/AiIntegration/AiIntegration').then(mod => mod.AiIntegration), { ssr: false });
+const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), {
+  ssr: false,
+});
+const ForbiddenPage = dynamic(
+  () =>
+    import('~/client/components/Admin/ForbiddenPage').then(
+      (mod) => mod.ForbiddenPage,
+    ),
+  { ssr: false },
+);
+const AiIntegration = dynamic(
+  () =>
+    import(
+      '~/features/openai/client/components/AiIntegration/AiIntegration'
+    ).then((mod) => mod.AiIntegration),
+  { ssr: false },
+);
 const AiIntegrationDisableMode = dynamic(
-  () => import('~/features/openai/client/components/AiIntegration/AiIntegrationDisableMode').then(mod => mod.AiIntegrationDisableMode), { ssr: false },
+  () =>
+    import(
+      '~/features/openai/client/components/AiIntegration/AiIntegrationDisableMode'
+    ).then((mod) => mod.AiIntegrationDisableMode),
+  { ssr: false },
 );
 
 type Props = CommonProps & {
-  aiEnabled: boolean,
+  aiEnabled: boolean;
 };
 
 const AdminAiIntegrationPage: NextPage<Props> = (props: Props) => {
@@ -37,15 +57,15 @@ const AdminAiIntegrationPage: NextPage<Props> = (props: Props) => {
       <Head>
         <title>{headTitle}</title>
       </Head>
-      {props.aiEnabled
-        ? <AiIntegration />
-        : <AiIntegrationDisableMode />
-      }
+      {props.aiEnabled ? <AiIntegration /> : <AiIntegrationDisableMode />}
     </AdminLayout>
   );
 };
 
-const injectServerConfigurations = async(context: GetServerSidePropsContext, props: Props): Promise<void> => {
+const injectServerConfigurations = async (
+  context: GetServerSidePropsContext,
+  props: Props,
+): Promise<void> => {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { configManager } = crowi;
@@ -53,8 +73,13 @@ const injectServerConfigurations = async(context: GetServerSidePropsContext, pro
   props.aiEnabled = configManager.getConfig('app:aiEnabled');
 };
 
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
-  const props = await retrieveServerSideProps(context, injectServerConfigurations);
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
+  const props = await retrieveServerSideProps(
+    context,
+    injectServerConfigurations,
+  );
   return props;
 };
 

+ 26 - 13
apps/app/src/pages/admin/app.page.tsx

@@ -1,26 +1,36 @@
 import { useEffect, useMemo } from 'react';
-
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import { useTranslation } from 'next-i18next';
 import type { Container } from 'unstated';
 import { Provider } from 'unstated';
 
 import type { CommonProps } from '~/pages/utils/commons';
 import { generateCustomTitle } from '~/pages/utils/commons';
-import { useCurrentUser } from '~/stores-universal/context';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
+import { useCurrentUser } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
-
-const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
-const AppSettingsPageContents = dynamic(() => import('~/client/components/Admin/App/AppSettingsPageContents'), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
-
+const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), {
+  ssr: false,
+});
+const AppSettingsPageContents = dynamic(
+  () => import('~/client/components/Admin/App/AppSettingsPageContents'),
+  { ssr: false },
+);
+const ForbiddenPage = dynamic(
+  () =>
+    import('~/client/components/Admin/ForbiddenPage').then(
+      (mod) => mod.ForbiddenPage,
+    ),
+  { ssr: false },
+);
 
 const AdminAppPage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('commons');
@@ -30,8 +40,10 @@ const AdminAppPage: NextPage<CommonProps> = (props) => {
   const injectableContainers: Container<any>[] = useMemo(() => [], []);
 
   useEffect(() => {
-    (async() => {
-      const AdminAppContainer = (await import('~/client/services/AdminAppContainer')).default;
+    (async () => {
+      const AdminAppContainer = (
+        await import('~/client/services/AdminAppContainer')
+      ).default;
       const adminAppContainer = new AdminAppContainer();
       injectableContainers.push(adminAppContainer);
     })();
@@ -55,10 +67,11 @@ const AdminAppPage: NextPage<CommonProps> = (props) => {
   );
 };
 
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const props = await retrieveServerSideProps(context);
   return props;
 };
 
-
 export default AdminAppPage;

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

@@ -1,32 +1,49 @@
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import { useTranslation } from 'next-i18next';
 
 import type { SupportedActionType } from '~/interfaces/activity';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CommonProps } from '~/pages/utils/commons';
 import { generateCustomTitle } from '~/pages/utils/commons';
 import {
-  useCurrentUser, useAuditLogEnabled, useAuditLogAvailableActions, useActivityExpirationSeconds,
+  useActivityExpirationSeconds,
+  useAuditLogAvailableActions,
+  useAuditLogEnabled,
+  useCurrentUser,
 } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
-const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
-const AuditLogManagement = dynamic(() => import('~/client/components/Admin/AuditLogManagement').then(mod => mod.AuditLogManagement), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
-
+const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), {
+  ssr: false,
+});
+const AuditLogManagement = dynamic(
+  () =>
+    import('~/client/components/Admin/AuditLogManagement').then(
+      (mod) => mod.AuditLogManagement,
+    ),
+  { ssr: false },
+);
+const ForbiddenPage = dynamic(
+  () =>
+    import('~/client/components/Admin/ForbiddenPage').then(
+      (mod) => mod.ForbiddenPage,
+    ),
+  { ssr: false },
+);
 
 type Props = CommonProps & {
-  auditLogEnabled: boolean,
-  activityExpirationSeconds: number,
-  auditLogAvailableActions: SupportedActionType[],
+  auditLogEnabled: boolean;
+  activityExpirationSeconds: number;
+  auditLogAvailableActions: SupportedActionType[];
 };
 
-
 const AdminAuditLogPage: NextPage<Props> = (props) => {
   const { t } = useTranslation('admin');
   useAuditLogEnabled(props.auditLogEnabled);
@@ -49,24 +66,31 @@ const AdminAuditLogPage: NextPage<Props> = (props) => {
       <AuditLogManagement />
     </AdminLayout>
   );
-
 };
 
-const injectServerConfigurations = async(context: GetServerSidePropsContext, props: Props): Promise<void> => {
+const injectServerConfigurations = async (
+  context: GetServerSidePropsContext,
+  props: Props,
+): Promise<void> => {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { activityService } = crowi;
 
   props.auditLogEnabled = crowi.configManager.getConfig('app:auditLogEnabled');
-  props.activityExpirationSeconds = crowi.configManager.getConfig('app:activityExpirationSeconds');
+  props.activityExpirationSeconds = crowi.configManager.getConfig(
+    'app:activityExpirationSeconds',
+  );
   props.auditLogAvailableActions = activityService.getAvailableActions(false);
 };
 
-
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
-  const props = await retrieveServerSideProps(context, injectServerConfigurations);
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
+  const props = await retrieveServerSideProps(
+    context,
+    injectServerConfigurations,
+  );
   return props;
 };
 
-
 export default AdminAuditLogPage;

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

@@ -1,11 +1,12 @@
 import { useEffect, useMemo } from 'react';
-
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import { useTranslation } from 'next-i18next';
 import type { Container } from 'unstated';
 import { Provider } from 'unstated';
 
@@ -13,21 +14,34 @@ import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CommonProps } from '~/pages/utils/commons';
 import { generateCustomTitle } from '~/pages/utils/commons';
 import { configManager } from '~/server/service/config-manager';
-import { useCustomizeTitle, useCurrentUser, useIsCustomizedLogoUploaded } from '~/stores-universal/context';
+import {
+  useCurrentUser,
+  useCustomizeTitle,
+  useIsCustomizedLogoUploaded,
+} from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
-const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
-const CustomizeSettingContents = dynamic(() => import('~/client/components/Admin/Customize/Customize'), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
-
+const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), {
+  ssr: false,
+});
+const CustomizeSettingContents = dynamic(
+  () => import('~/client/components/Admin/Customize/Customize'),
+  { ssr: false },
+);
+const ForbiddenPage = dynamic(
+  () =>
+    import('~/client/components/Admin/ForbiddenPage').then(
+      (mod) => mod.ForbiddenPage,
+    ),
+  { ssr: false },
+);
 
 type Props = CommonProps & {
-  customizeTitle?: string,
-  isCustomizedLogoUploaded: boolean,
+  customizeTitle?: string;
+  isCustomizedLogoUploaded: boolean;
 };
 
-
 const AdminCustomizeSettingsPage: NextPage<Props> = (props) => {
   const { t } = useTranslation('admin');
   useCustomizeTitle(props.customizeTitle);
@@ -39,8 +53,10 @@ const AdminCustomizeSettingsPage: NextPage<Props> = (props) => {
   const injectableContainers: Container<any>[] = useMemo(() => [], []);
 
   useEffect(() => {
-    (async() => {
-      const AdminCustomizeContainer = (await import('~/client/services/AdminCustomizeContainer')).default;
+    (async () => {
+      const AdminCustomizeContainer = (
+        await import('~/client/services/AdminCustomizeContainer')
+      ).default;
       const adminCustomizeContainer = new AdminCustomizeContainer();
       injectableContainers.push(adminCustomizeContainer);
     })();
@@ -62,19 +78,26 @@ const AdminCustomizeSettingsPage: NextPage<Props> = (props) => {
   );
 };
 
-
-const injectServerConfigurations = async(context: GetServerSidePropsContext, props: Props): Promise<void> => {
+const injectServerConfigurations = async (
+  context: GetServerSidePropsContext,
+  props: Props,
+): Promise<void> => {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
 
   props.customizeTitle = crowi.configManager.getConfig('customize:title');
-  props.isCustomizedLogoUploaded = await crowi.attachmentService.isBrandLogoExist();
+  props.isCustomizedLogoUploaded =
+    await crowi.attachmentService.isBrandLogoExist();
 };
 
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
-  const props = await retrieveServerSideProps(context, injectServerConfigurations);
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
+  const props = await retrieveServerSideProps(
+    context,
+    injectServerConfigurations,
+  );
   return props;
 };
 
-
 export default AdminCustomizeSettingsPage;

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

@@ -1,11 +1,12 @@
 import { useEffect, useMemo } from 'react';
-
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import { useTranslation } from 'next-i18next';
 import type { Container } from 'unstated';
 import { Provider } from 'unstated';
 
@@ -15,14 +16,23 @@ import { useCurrentUser, useGrowiCloudUri } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
-const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
-const G2GDataTransferPage = dynamic(() => import('~/client/components/Admin/G2GDataTransfer'), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
-
+const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), {
+  ssr: false,
+});
+const G2GDataTransferPage = dynamic(
+  () => import('~/client/components/Admin/G2GDataTransfer'),
+  { ssr: false },
+);
+const ForbiddenPage = dynamic(
+  () =>
+    import('~/client/components/Admin/ForbiddenPage').then(
+      (mod) => mod.ForbiddenPage,
+    ),
+  { ssr: false },
+);
 
 type Props = CommonProps;
 
-
 const DataTransferPage: NextPage<Props> = (props) => {
   const { t } = useTranslation('commons');
   useCurrentUser(props.currentUser ?? null);
@@ -33,8 +43,10 @@ const DataTransferPage: NextPage<Props> = (props) => {
   const injectableContainers: Container<any>[] = useMemo(() => [], []);
 
   useEffect(() => {
-    (async() => {
-      const AdminAppContainer = (await import('~/client/services/AdminAppContainer')).default;
+    (async () => {
+      const AdminAppContainer = (
+        await import('~/client/services/AdminAppContainer')
+      ).default;
       const adminAppContainer = new AdminAppContainer();
       injectableContainers.push(adminAppContainer);
     })();
@@ -56,17 +68,25 @@ const DataTransferPage: NextPage<Props> = (props) => {
   );
 };
 
-const injectServerConfigurations = async(context: GetServerSidePropsContext, props: Props): Promise<void> => {
+const injectServerConfigurations = async (
+  context: GetServerSidePropsContext,
+  props: Props,
+): Promise<void> => {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
 
-  props.growiCloudUri = await crowi.configManager.getConfig('app:growiCloudUri');
+  props.growiCloudUri =
+    await crowi.configManager.getConfig('app:growiCloudUri');
 };
 
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
-  const props = await retrieveServerSideProps(context, injectServerConfigurations);
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
+  const props = await retrieveServerSideProps(
+    context,
+    injectServerConfigurations,
+  );
   return props;
 };
 
-
 export default DataTransferPage;

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

@@ -1,11 +1,12 @@
 import { useEffect, useMemo } from 'react';
-
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import { useTranslation } from 'next-i18next';
 import type { Container } from 'unstated';
 import { Provider } from 'unstated';
 
@@ -15,10 +16,20 @@ import { useCurrentUser } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
-const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
-const ExportArchiveDataPage = dynamic(() => import('~/client/components/Admin/ExportArchiveDataPage'), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
-
+const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), {
+  ssr: false,
+});
+const ExportArchiveDataPage = dynamic(
+  () => import('~/client/components/Admin/ExportArchiveDataPage'),
+  { ssr: false },
+);
+const ForbiddenPage = dynamic(
+  () =>
+    import('~/client/components/Admin/ForbiddenPage').then(
+      (mod) => mod.ForbiddenPage,
+    ),
+  { ssr: false },
+);
 
 const AdminExportDataArchivePage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('admin');
@@ -30,8 +41,10 @@ const AdminExportDataArchivePage: NextPage<CommonProps> = (props) => {
   const injectableContainers: Container<any>[] = useMemo(() => [], []);
 
   useEffect(() => {
-    (async() => {
-      const AdminAppContainer = (await import('~/client/services/AdminAppContainer')).default;
+    (async () => {
+      const AdminAppContainer = (
+        await import('~/client/services/AdminAppContainer')
+      ).default;
       const adminAppContainer = new AdminAppContainer();
       injectableContainers.push(adminAppContainer);
     })();
@@ -53,11 +66,11 @@ const AdminExportDataArchivePage: NextPage<CommonProps> = (props) => {
   );
 };
 
-
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const props = await retrieveServerSideProps(context);
   return props;
 };
 
-
 export default AdminExportDataArchivePage;

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

@@ -1,13 +1,14 @@
 import { useEffect, useMemo } from 'react';
-
-import { objectIdUtils } from '@growi/core/dist/utils';
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import { useRouter } from 'next/router';
+import { objectIdUtils } from '@growi/core/dist/utils';
+import { useTranslation } from 'next-i18next';
 import type { Container } from 'unstated';
 import { Provider } from 'unstated';
 
@@ -17,44 +18,63 @@ import { useCurrentUser } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../../utils/admin-page-util';
 
-
-const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
-const ManageGlobalNotification = dynamic(() => import('~/client/components/Admin/Notification/ManageGlobalNotification'), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
-
+const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), {
+  ssr: false,
+});
+const ManageGlobalNotification = dynamic(
+  () =>
+    import('~/client/components/Admin/Notification/ManageGlobalNotification'),
+  { ssr: false },
+);
+const ForbiddenPage = dynamic(
+  () =>
+    import('~/client/components/Admin/ForbiddenPage').then(
+      (mod) => mod.ForbiddenPage,
+    ),
+  { ssr: false },
+);
 
 const AdminGlobalNotificationNewPage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('admin');
   useCurrentUser(props.currentUser ?? null);
   const router = useRouter();
   const { globalNotificationId } = router.query;
-  const currentGlobalNotificationId = Array.isArray(globalNotificationId) ? globalNotificationId[0] : globalNotificationId;
-
+  const currentGlobalNotificationId = Array.isArray(globalNotificationId)
+    ? globalNotificationId[0]
+    : globalNotificationId;
 
   useEffect(() => {
-    const toastError = import('~/client/util/toastr').then(mod => mod.toastError);
+    const toastError = import('~/client/util/toastr').then(
+      (mod) => mod.toastError,
+    );
 
     if (globalNotificationId == null) {
       router.push('/admin/notification');
     }
-    if ((currentGlobalNotificationId != null && !objectIdUtils.isValidObjectId(currentGlobalNotificationId))) {
-      (async() => {
-        (await toastError)(t('notification_settings.not_found_global_notification_triggerid'));
+    if (
+      currentGlobalNotificationId != null &&
+      !objectIdUtils.isValidObjectId(currentGlobalNotificationId)
+    ) {
+      (async () => {
+        (await toastError)(
+          t('notification_settings.not_found_global_notification_triggerid'),
+        );
         router.push('/admin/global-notification/new');
       })();
       return;
     }
   }, [currentGlobalNotificationId, globalNotificationId, router, t]);
 
-
   const title = t('external_notification.external_notification');
   const customTitle = generateCustomTitle(props, title);
 
   const injectableContainers: Container<any>[] = useMemo(() => [], []);
 
   useEffect(() => {
-    (async() => {
-      const AdminNotificationContainer = (await import('~/client/services/AdminNotificationContainer')).default;
+    (async () => {
+      const AdminNotificationContainer = (
+        await import('~/client/services/AdminNotificationContainer')
+      ).default;
       const adminNotificationContainer = new AdminNotificationContainer();
       injectableContainers.push(adminNotificationContainer);
     })();
@@ -70,21 +90,21 @@ const AdminGlobalNotificationNewPage: NextPage<CommonProps> = (props) => {
         <Head>
           <title>{customTitle}</title>
         </Head>
-        {
-          currentGlobalNotificationId != null && router.isReady
-      && <ManageGlobalNotification globalNotificationId={currentGlobalNotificationId} />
-        }
+        {currentGlobalNotificationId != null && router.isReady && (
+          <ManageGlobalNotification
+            globalNotificationId={currentGlobalNotificationId}
+          />
+        )}
       </AdminLayout>
     </Provider>
   );
-
 };
 
-
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const props = await retrieveServerSideProps(context);
   return props;
 };
 
-
 export default AdminGlobalNotificationNewPage;

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

@@ -1,11 +1,12 @@
 import { useEffect, useMemo } from 'react';
-
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import { useTranslation } from 'next-i18next';
 import type { Container } from 'unstated';
 import { Provider } from 'unstated';
 
@@ -14,10 +15,21 @@ import { useCurrentUser } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../../utils/admin-page-util';
 
-const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
-const ManageGlobalNotification = dynamic(() => import('~/client/components/Admin/Notification/ManageGlobalNotification'), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
-
+const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), {
+  ssr: false,
+});
+const ManageGlobalNotification = dynamic(
+  () =>
+    import('~/client/components/Admin/Notification/ManageGlobalNotification'),
+  { ssr: false },
+);
+const ForbiddenPage = dynamic(
+  () =>
+    import('~/client/components/Admin/ForbiddenPage').then(
+      (mod) => mod.ForbiddenPage,
+    ),
+  { ssr: false },
+);
 
 const AdminGlobalNotificationNewPage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('admin');
@@ -28,8 +40,10 @@ const AdminGlobalNotificationNewPage: NextPage<CommonProps> = (props) => {
   const injectableContainers: Container<any>[] = useMemo(() => [], []);
 
   useEffect(() => {
-    (async() => {
-      const AdminNotificationContainer = (await import('~/client/services/AdminNotificationContainer')).default;
+    (async () => {
+      const AdminNotificationContainer = (
+        await import('~/client/services/AdminNotificationContainer')
+      ).default;
       const adminNotificationContainer = new AdminNotificationContainer();
       injectableContainers.push(adminNotificationContainer);
     })();
@@ -49,14 +63,13 @@ const AdminGlobalNotificationNewPage: NextPage<CommonProps> = (props) => {
       </AdminLayout>
     </Provider>
   );
-
 };
 
-
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const props = await retrieveServerSideProps(context);
   return props;
 };
 
-
 export default AdminGlobalNotificationNewPage;

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

@@ -1,11 +1,12 @@
 import { useEffect, useMemo } from 'react';
-
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import { useTranslation } from 'next-i18next';
 import type { Container } from 'unstated';
 import { Provider } from 'unstated';
 
@@ -15,10 +16,20 @@ import { useCurrentUser } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
-const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
-const DataImportPageContents = dynamic(() => import('~/client/components/Admin/ImportData/ImportDataPageContents'), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
-
+const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), {
+  ssr: false,
+});
+const DataImportPageContents = dynamic(
+  () => import('~/client/components/Admin/ImportData/ImportDataPageContents'),
+  { ssr: false },
+);
+const ForbiddenPage = dynamic(
+  () =>
+    import('~/client/components/Admin/ForbiddenPage').then(
+      (mod) => mod.ForbiddenPage,
+    ),
+  { ssr: false },
+);
 
 const AdminDataImportPage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('admin');
@@ -30,8 +41,10 @@ const AdminDataImportPage: NextPage<CommonProps> = (props) => {
   const injectableContainers: Container<any>[] = useMemo(() => [], []);
 
   useEffect(() => {
-    (async() => {
-      const AdminImportContainer = (await import('~/client/services/AdminImportContainer')).default;
+    (async () => {
+      const AdminImportContainer = (
+        await import('~/client/services/AdminImportContainer')
+      ).default;
       const adminImportContainer = new AdminImportContainer();
       injectableContainers.push(adminImportContainer);
     })();
@@ -41,7 +54,6 @@ const AdminDataImportPage: NextPage<CommonProps> = (props) => {
     return <ForbiddenPage />;
   }
 
-
   return (
     <Provider inject={[...injectableContainers]}>
       <AdminLayout componentTitle={componentTitle}>
@@ -52,14 +64,13 @@ const AdminDataImportPage: NextPage<CommonProps> = (props) => {
       </AdminLayout>
     </Provider>
   );
-
 };
 
-
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const props = await retrieveServerSideProps(context);
   return props;
 };
 
-
 export default AdminDataImportPage;

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

@@ -1,11 +1,12 @@
 import { useEffect, useMemo } from 'react';
-
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import { useTranslation } from 'next-i18next';
 import type { Container } from 'unstated';
 import { Provider } from 'unstated';
 
@@ -13,23 +14,33 @@ import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CommonProps } from '~/pages/utils/commons';
 import { generateCustomTitle } from '~/pages/utils/commons';
 import {
-  useCurrentUser, useGrowiCloudUri, useGrowiAppIdForGrowiCloud,
+  useCurrentUser,
+  useGrowiAppIdForGrowiCloud,
+  useGrowiCloudUri,
 } from '~/stores-universal/context';
 
-
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
-const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
-const AdminHome = dynamic(() => import('~/client/components/Admin/AdminHome/AdminHome'), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
-
+const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), {
+  ssr: false,
+});
+const AdminHome = dynamic(
+  () => import('~/client/components/Admin/AdminHome/AdminHome'),
+  { ssr: false },
+);
+const ForbiddenPage = dynamic(
+  () =>
+    import('~/client/components/Admin/ForbiddenPage').then(
+      (mod) => mod.ForbiddenPage,
+    ),
+  { ssr: false },
+);
 
 type Props = CommonProps & {
-  growiCloudUri?: string,
-  growiAppIdForGrowiCloud?: number,
+  growiCloudUri?: string;
+  growiAppIdForGrowiCloud?: number;
 };
 
-
 const AdminHomepage: NextPage<Props> = (props: Props) => {
   useCurrentUser(props.currentUser ?? null);
   useGrowiCloudUri(props.growiCloudUri);
@@ -42,8 +53,10 @@ const AdminHomepage: NextPage<Props> = (props: Props) => {
   const injectableContainers: Container<any>[] = useMemo(() => [], []);
 
   useEffect(() => {
-    (async() => {
-      const AdminHomeContainer = (await import('~/client/services/AdminHomeContainer')).default;
+    (async () => {
+      const AdminHomeContainer = (
+        await import('~/client/services/AdminHomeContainer')
+      ).default;
       const adminHomeContainer = new AdminHomeContainer();
       injectableContainers.push(adminHomeContainer);
     })();
@@ -53,7 +66,6 @@ const AdminHomepage: NextPage<Props> = (props: Props) => {
     return <ForbiddenPage />;
   }
 
-
   return (
     <Provider inject={[...injectableContainers]}>
       <AdminLayout componentTitle={title}>
@@ -66,20 +78,27 @@ const AdminHomepage: NextPage<Props> = (props: Props) => {
   );
 };
 
-
-const injectServerConfigurations = async(context: GetServerSidePropsContext, props: Props): Promise<void> => {
+const injectServerConfigurations = async (
+  context: GetServerSidePropsContext,
+  props: Props,
+): Promise<void> => {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
 
   props.growiCloudUri = crowi.configManager.getConfig('app:growiCloudUri');
-  props.growiAppIdForGrowiCloud = crowi.configManager.getConfig('app:growiAppIdForCloud');
+  props.growiAppIdForGrowiCloud = crowi.configManager.getConfig(
+    'app:growiAppIdForCloud',
+  );
 };
 
-
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
-  const props = await retrieveServerSideProps(context, injectServerConfigurations);
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
+  const props = await retrieveServerSideProps(
+    context,
+    injectServerConfigurations,
+  );
   return props;
 };
 
-
 export default AdminHomepage;

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

@@ -1,11 +1,12 @@
 import { useEffect, useMemo } from 'react';
-
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import { useTranslation } from 'next-i18next';
 import type { Container } from 'unstated';
 import { Provider } from 'unstated';
 
@@ -15,10 +16,21 @@ import { useCurrentUser } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
-const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
-const MarkDownSettingContents = dynamic(() => import('~/client/components/Admin/MarkdownSetting/MarkDownSettingContents'), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
-
+const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), {
+  ssr: false,
+});
+const MarkDownSettingContents = dynamic(
+  () =>
+    import('~/client/components/Admin/MarkdownSetting/MarkDownSettingContents'),
+  { ssr: false },
+);
+const ForbiddenPage = dynamic(
+  () =>
+    import('~/client/components/Admin/ForbiddenPage').then(
+      (mod) => mod.ForbiddenPage,
+    ),
+  { ssr: false },
+);
 
 const AdminMarkdownPage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('admin');
@@ -30,8 +42,10 @@ const AdminMarkdownPage: NextPage<CommonProps> = (props) => {
   const injectableContainers: Container<any>[] = useMemo(() => [], []);
 
   useEffect(() => {
-    (async() => {
-      const AdminMarkDownContainer = (await import('~/client/services/AdminMarkDownContainer')).default;
+    (async () => {
+      const AdminMarkDownContainer = (
+        await import('~/client/services/AdminMarkDownContainer')
+      ).default;
       const adminMarkDownContainer = new AdminMarkDownContainer();
       injectableContainers.push(adminMarkDownContainer);
     })();
@@ -51,14 +65,13 @@ const AdminMarkdownPage: NextPage<CommonProps> = (props) => {
       </AdminLayout>
     </Provider>
   );
-
 };
 
-
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const props = await retrieveServerSideProps(context);
   return props;
 };
 
-
 export default AdminMarkdownPage;

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

@@ -1,11 +1,12 @@
 import { useEffect, useMemo } from 'react';
-
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import { useTranslation } from 'next-i18next';
 import type { Container } from 'unstated';
 import { Provider } from 'unstated';
 
@@ -15,10 +16,20 @@ import { useCurrentUser } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
-const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
-const NotificationSetting = dynamic(() => import('~/client/components/Admin/Notification/NotificationSetting'), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
-
+const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), {
+  ssr: false,
+});
+const NotificationSetting = dynamic(
+  () => import('~/client/components/Admin/Notification/NotificationSetting'),
+  { ssr: false },
+);
+const ForbiddenPage = dynamic(
+  () =>
+    import('~/client/components/Admin/ForbiddenPage').then(
+      (mod) => mod.ForbiddenPage,
+    ),
+  { ssr: false },
+);
 
 const AdminExternalNotificationPage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('admin');
@@ -30,8 +41,10 @@ const AdminExternalNotificationPage: NextPage<CommonProps> = (props) => {
   const injectableContainers: Container<any>[] = useMemo(() => [], []);
 
   useEffect(() => {
-    (async() => {
-      const AdminNotificationContainer = (await import('~/client/services/AdminNotificationContainer')).default;
+    (async () => {
+      const AdminNotificationContainer = (
+        await import('~/client/services/AdminNotificationContainer')
+      ).default;
       const adminNotificationContainer = new AdminNotificationContainer();
       injectableContainers.push(adminNotificationContainer);
     })();
@@ -41,7 +54,6 @@ const AdminExternalNotificationPage: NextPage<CommonProps> = (props) => {
     return <ForbiddenPage />;
   }
 
-
   return (
     <Provider inject={[...injectableContainers]}>
       <AdminLayout componentTitle={componentTitle}>
@@ -52,14 +64,13 @@ const AdminExternalNotificationPage: NextPage<CommonProps> = (props) => {
       </AdminLayout>
     </Provider>
   );
-
 };
 
-
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const props = await retrieveServerSideProps(context);
   return props;
 };
 
-
 export default AdminExternalNotificationPage;

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

@@ -1,30 +1,39 @@
 import { useEffect, useMemo } from 'react';
-
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import { useTranslation } from 'next-i18next';
 import type { Container } from 'unstated';
 import { Provider } from 'unstated';
 
-
 import type { CommonProps } from '~/pages/utils/commons';
 import { generateCustomTitle } from '~/pages/utils/commons';
-import { useCurrentUser } from '~/stores-universal/context';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
+import { useCurrentUser } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
-
-const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
+const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), {
+  ssr: false,
+});
 const PluginsExtensionPageContents = dynamic(
-  () => import('~/features/growi-plugin/client/components/Admin').then(mod => mod.PluginsExtensionPageContents),
+  () =>
+    import('~/features/growi-plugin/client/components/Admin').then(
+      (mod) => mod.PluginsExtensionPageContents,
+    ),
+  { ssr: false },
+);
+const ForbiddenPage = dynamic(
+  () =>
+    import('~/client/components/Admin/ForbiddenPage').then(
+      (mod) => mod.ForbiddenPage,
+    ),
   { ssr: false },
 );
-const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
-
 
 const AdminAppPage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('admin');
@@ -36,8 +45,10 @@ const AdminAppPage: NextPage<CommonProps> = (props) => {
   const injectableContainers: Container<any>[] = useMemo(() => [], []);
 
   useEffect(() => {
-    (async() => {
-      const AdminAppContainer = (await import('~/client/services/AdminAppContainer')).default;
+    (async () => {
+      const AdminAppContainer = (
+        await import('~/client/services/AdminAppContainer')
+      ).default;
       const adminAppContainer = new AdminAppContainer();
       injectableContainers.push(adminAppContainer);
     })();
@@ -59,10 +70,11 @@ const AdminAppPage: NextPage<CommonProps> = (props) => {
   );
 };
 
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const props = await retrieveServerSideProps(context);
   return props;
 };
 
-
 export default AdminAppPage;

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

@@ -1,29 +1,44 @@
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import { useTranslation } from 'next-i18next';
 
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CommonProps } from '~/pages/utils/commons';
 import { generateCustomTitle } from '~/pages/utils/commons';
-import { useIsSearchServiceReachable, useCurrentUser } from '~/stores-universal/context';
+import {
+  useCurrentUser,
+  useIsSearchServiceReachable,
+} from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
-const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
+const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), {
+  ssr: false,
+});
 const FullTextSearchManagement = dynamic(
-  () => import('~/client/components/Admin/FullTextSearchManagement').then(mod => mod.FullTextSearchManagement), { ssr: false },
+  () =>
+    import('~/client/components/Admin/FullTextSearchManagement').then(
+      (mod) => mod.FullTextSearchManagement,
+    ),
+  { ssr: false },
+);
+const ForbiddenPage = dynamic(
+  () =>
+    import('~/client/components/Admin/ForbiddenPage').then(
+      (mod) => mod.ForbiddenPage,
+    ),
+  { ssr: false },
 );
-const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
-
 
 type Props = CommonProps & {
-  isSearchServiceReachable: boolean,
+  isSearchServiceReachable: boolean;
 };
 
-
 const AdminFullTextSearchManagementPage: NextPage<Props> = (props) => {
   const { t } = useTranslation('admin');
   useCurrentUser(props.currentUser ?? null);
@@ -46,7 +61,10 @@ const AdminFullTextSearchManagementPage: NextPage<Props> = (props) => {
   );
 };
 
-const injectServerConfigurations = async(context: GetServerSidePropsContext, props: Props): Promise<void> => {
+const injectServerConfigurations = async (
+  context: GetServerSidePropsContext,
+  props: Props,
+): Promise<void> => {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { searchService } = crowi;
@@ -54,11 +72,14 @@ const injectServerConfigurations = async(context: GetServerSidePropsContext, pro
   props.isSearchServiceReachable = searchService.isReachable;
 };
 
-
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
-  const props = await retrieveServerSideProps(context, injectServerConfigurations);
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
+  const props = await retrieveServerSideProps(
+    context,
+    injectServerConfigurations,
+  );
   return props;
 };
 
-
 export default AdminFullTextSearchManagementPage;

+ 58 - 24
apps/app/src/pages/admin/security.page.tsx

@@ -1,32 +1,46 @@
 import { useEffect, useMemo } from 'react';
-
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import { useTranslation } from 'next-i18next';
 import type { Container } from 'unstated';
 import { Provider } from 'unstated';
 
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CommonProps } from '~/pages/utils/commons';
 import { generateCustomTitle } from '~/pages/utils/commons';
-import { useCurrentUser, useIsMailerSetup, useSiteUrl } from '~/stores-universal/context';
+import {
+  useCurrentUser,
+  useIsMailerSetup,
+  useSiteUrl,
+} from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
-const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
-const SecurityManagement = dynamic(() => import('~/client/components/Admin/Security/SecurityManagement'), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
-
+const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), {
+  ssr: false,
+});
+const SecurityManagement = dynamic(
+  () => import('~/client/components/Admin/Security/SecurityManagement'),
+  { ssr: false },
+);
+const ForbiddenPage = dynamic(
+  () =>
+    import('~/client/components/Admin/ForbiddenPage').then(
+      (mod) => mod.ForbiddenPage,
+    ),
+  { ssr: false },
+);
 
 type Props = CommonProps & {
-  isMailerSetup: boolean,
-  siteUrl: string,
+  isMailerSetup: boolean;
+  siteUrl: string;
 };
 
-
 const AdminSecuritySettingsPage: NextPage<Props> = (props) => {
   const { t } = useTranslation('admin');
   useCurrentUser(props.currentUser ?? null);
@@ -39,26 +53,40 @@ const AdminSecuritySettingsPage: NextPage<Props> = (props) => {
   const adminSecurityContainers: Container<any>[] = useMemo(() => [], []);
 
   useEffect(() => {
-    (async() => {
-      const AdminGeneralSecurityContainer = (await import('~/client/services/AdminGeneralSecurityContainer')).default;
+    (async () => {
+      const AdminGeneralSecurityContainer = (
+        await import('~/client/services/AdminGeneralSecurityContainer')
+      ).default;
       const adminGeneralSecurityContainer = new AdminGeneralSecurityContainer();
 
-      const AdminLocalSecurityContainer = (await import('~/client/services/AdminLocalSecurityContainer')).default;
+      const AdminLocalSecurityContainer = (
+        await import('~/client/services/AdminLocalSecurityContainer')
+      ).default;
       const adminLocalSecurityContainer = new AdminLocalSecurityContainer();
 
-      const AdminLdapSecurityContainer = (await import('~/client/services/AdminLdapSecurityContainer')).default;
+      const AdminLdapSecurityContainer = (
+        await import('~/client/services/AdminLdapSecurityContainer')
+      ).default;
       const adminLdapSecurityContainer = new AdminLdapSecurityContainer();
 
-      const AdminSamlSecurityContainer = (await import('~/client/services/AdminSamlSecurityContainer')).default;
+      const AdminSamlSecurityContainer = (
+        await import('~/client/services/AdminSamlSecurityContainer')
+      ).default;
       const adminSamlSecurityContainer = new AdminSamlSecurityContainer();
 
-      const AdminOidcSecurityContainer = (await import('~/client/services/AdminOidcSecurityContainer')).default;
+      const AdminOidcSecurityContainer = (
+        await import('~/client/services/AdminOidcSecurityContainer')
+      ).default;
       const adminOidcSecurityContainer = new AdminOidcSecurityContainer();
 
-      const AdminGoogleSecurityContainer = (await import('~/client/services/AdminGoogleSecurityContainer')).default;
+      const AdminGoogleSecurityContainer = (
+        await import('~/client/services/AdminGoogleSecurityContainer')
+      ).default;
       const adminGoogleSecurityContainer = new AdminGoogleSecurityContainer();
 
-      const AdminGitHubSecurityContainer = (await import('~/client/services/AdminGitHubSecurityContainer')).default;
+      const AdminGitHubSecurityContainer = (
+        await import('~/client/services/AdminGitHubSecurityContainer')
+      ).default;
       const adminGitHubSecurityContainer = new AdminGitHubSecurityContainer();
 
       adminSecurityContainers.push(
@@ -89,7 +117,10 @@ const AdminSecuritySettingsPage: NextPage<Props> = (props) => {
   );
 };
 
-const injectServerConfigurations = async(context: GetServerSidePropsContext, props: Props): Promise<void> => {
+const injectServerConfigurations = async (
+  context: GetServerSidePropsContext,
+  props: Props,
+): Promise<void> => {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { growiInfoService, mailService } = crowi;
@@ -98,11 +129,14 @@ const injectServerConfigurations = async(context: GetServerSidePropsContext, pro
   props.isMailerSetup = mailService.isMailerSetup;
 };
 
-
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
-  const props = await retrieveServerSideProps(context, injectServerConfigurations);
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
+  const props = await retrieveServerSideProps(
+    context,
+    injectServerConfigurations,
+  );
   return props;
 };
 
-
 export default AdminSecuritySettingsPage;

+ 30 - 14
apps/app/src/pages/admin/slack-integration-legacy.page.tsx

@@ -1,11 +1,12 @@
 import { useEffect, useMemo } from 'react';
-
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import { useTranslation } from 'next-i18next';
 import type { Container } from 'unstated';
 import { Provider } from 'unstated';
 
@@ -15,9 +16,23 @@ import { useCurrentUser } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
-const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
-const LegacySlackIntegration = dynamic(() => import('~/client/components/Admin/LegacySlackIntegration/LegacySlackIntegration'), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
+const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), {
+  ssr: false,
+});
+const LegacySlackIntegration = dynamic(
+  () =>
+    import(
+      '~/client/components/Admin/LegacySlackIntegration/LegacySlackIntegration'
+    ),
+  { ssr: false },
+);
+const ForbiddenPage = dynamic(
+  () =>
+    import('~/client/components/Admin/ForbiddenPage').then(
+      (mod) => mod.ForbiddenPage,
+    ),
+  { ssr: false },
+);
 
 const AdminLegacySlackIntegrationPage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('admin');
@@ -29,9 +44,12 @@ const AdminLegacySlackIntegrationPage: NextPage<CommonProps> = (props) => {
   const injectableContainers: Container<any>[] = useMemo(() => [], []);
 
   useEffect(() => {
-    (async() => {
-      const AdminSlackIntegrationLegacyContainer = (await import('~/client/services/AdminSlackIntegrationLegacyContainer')).default;
-      const adminSlackIntegrationLegacyContainer = new AdminSlackIntegrationLegacyContainer();
+    (async () => {
+      const AdminSlackIntegrationLegacyContainer = (
+        await import('~/client/services/AdminSlackIntegrationLegacyContainer')
+      ).default;
+      const adminSlackIntegrationLegacyContainer =
+        new AdminSlackIntegrationLegacyContainer();
       injectableContainers.push(adminSlackIntegrationLegacyContainer);
     })();
   }, [injectableContainers]);
@@ -40,7 +58,6 @@ const AdminLegacySlackIntegrationPage: NextPage<CommonProps> = (props) => {
     return <ForbiddenPage />;
   }
 
-
   return (
     <Provider inject={[...injectableContainers]}>
       <AdminLayout componentTitle={title}>
@@ -51,14 +68,13 @@ const AdminLegacySlackIntegrationPage: NextPage<CommonProps> = (props) => {
       </AdminLayout>
     </Provider>
   );
-
 };
 
-
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const props = await retrieveServerSideProps(context);
   return props;
 };
 
-
 export default AdminLegacySlackIntegrationPage;

+ 33 - 15
apps/app/src/pages/admin/slack-integration.page.tsx

@@ -1,9 +1,11 @@
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import { useTranslation } from 'next-i18next';
 
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CommonProps } from '~/pages/utils/commons';
@@ -12,17 +14,28 @@ import { useCurrentUser, useSiteUrl } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
-
-const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
-const SlackIntegration = dynamic(() => import('~/client/components/Admin/SlackIntegration/SlackIntegration').then(mod => mod.SlackIntegration), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
-
+const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), {
+  ssr: false,
+});
+const SlackIntegration = dynamic(
+  () =>
+    import('~/client/components/Admin/SlackIntegration/SlackIntegration').then(
+      (mod) => mod.SlackIntegration,
+    ),
+  { ssr: false },
+);
+const ForbiddenPage = dynamic(
+  () =>
+    import('~/client/components/Admin/ForbiddenPage').then(
+      (mod) => mod.ForbiddenPage,
+    ),
+  { ssr: false },
+);
 
 type Props = CommonProps & {
-  siteUrl: string
+  siteUrl: string;
 };
 
-
 const AdminSlackIntegrationPage: NextPage<Props> = (props) => {
   const { t } = useTranslation('admin');
   useCurrentUser(props.currentUser ?? null);
@@ -45,8 +58,10 @@ const AdminSlackIntegrationPage: NextPage<Props> = (props) => {
   );
 };
 
-
-const injectServerConfigurations = async(context: GetServerSidePropsContext, props: Props): Promise<void> => {
+const injectServerConfigurations = async (
+  context: GetServerSidePropsContext,
+  props: Props,
+): Promise<void> => {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { growiInfoService } = crowi;
@@ -54,11 +69,14 @@ const injectServerConfigurations = async(context: GetServerSidePropsContext, pro
   props.siteUrl = growiInfoService.getSiteUrl();
 };
 
-
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
-  const props = await retrieveServerSideProps(context, injectServerConfigurations);
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
+  const props = await retrieveServerSideProps(
+    context,
+    injectServerConfigurations,
+  );
   return props;
 };
 
-
 export default AdminSlackIntegrationPage;

+ 41 - 16
apps/app/src/pages/admin/user-group-detail/[userGroupId].page.tsx

@@ -1,26 +1,39 @@
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import { useRouter } from 'next/router';
+import { useTranslation } from 'next-i18next';
 
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CommonProps } from '~/pages/utils/commons';
 import { generateCustomTitle } from '~/pages/utils/commons';
-import { useIsAclEnabled, useCurrentUser } from '~/stores-universal/context';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
+import { useCurrentUser, useIsAclEnabled } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../../utils/admin-page-util';
 
-const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
-const UserGroupDetailPage = dynamic(() => import('~/client/components/Admin/UserGroupDetail/UserGroupDetailPage'), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
+const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), {
+  ssr: false,
+});
+const UserGroupDetailPage = dynamic(
+  () => import('~/client/components/Admin/UserGroupDetail/UserGroupDetailPage'),
+  { ssr: false },
+);
+const ForbiddenPage = dynamic(
+  () =>
+    import('~/client/components/Admin/ForbiddenPage').then(
+      (mod) => mod.ForbiddenPage,
+    ),
+  { ssr: false },
+);
 
 type Props = CommonProps & {
-  isAclEnabled: boolean
-}
+  isAclEnabled: boolean;
+};
 
 const AdminUserGroupDetailPage: NextPage<Props> = (props: Props) => {
   const { t } = useTranslation('admin');
@@ -32,7 +45,9 @@ const AdminUserGroupDetailPage: NextPage<Props> = (props: Props) => {
   const title = t('user_group_management.user_group_management');
   const customTitle = generateCustomTitle(props, title);
 
-  const currentUserGroupId = Array.isArray(userGroupId) ? userGroupId[0] : userGroupId;
+  const currentUserGroupId = Array.isArray(userGroupId)
+    ? userGroupId[0]
+    : userGroupId;
 
   const isExternalGroupBool = isExternalGroup === 'true';
 
@@ -47,21 +62,31 @@ const AdminUserGroupDetailPage: NextPage<Props> = (props: Props) => {
       <Head>
         <title>{customTitle}</title>
       </Head>
-      {
-        currentUserGroupId != null && router.isReady
-      && <UserGroupDetailPage userGroupId={currentUserGroupId} isExternalGroup={isExternalGroupBool} />
-      }
+      {currentUserGroupId != null && router.isReady && (
+        <UserGroupDetailPage
+          userGroupId={currentUserGroupId}
+          isExternalGroup={isExternalGroupBool}
+        />
+      )}
     </AdminLayout>
   );
 };
 
-const injectServerConfigurations = async(context: GetServerSidePropsContext, props: Props): Promise<void> => {
+const injectServerConfigurations = async (
+  context: GetServerSidePropsContext,
+  props: Props,
+): Promise<void> => {
   const req: CrowiRequest = context.req as CrowiRequest;
   props.isAclEnabled = req.crowi.aclService.isAclEnabled();
 };
 
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
-  const props = await retrieveServerSideProps(context, injectServerConfigurations);
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
+  const props = await retrieveServerSideProps(
+    context,
+    injectServerConfigurations,
+  );
 
   return props;
 };

+ 34 - 14
apps/app/src/pages/admin/user-groups.page.tsx

@@ -1,27 +1,41 @@
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import { useTranslation } from 'next-i18next';
 
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CommonProps } from '~/pages/utils/commons';
 import { generateCustomTitle } from '~/pages/utils/commons';
-import { useIsAclEnabled, useCurrentUser } from '~/stores-universal/context';
+import { useCurrentUser, useIsAclEnabled } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
-const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
-const UserGroupPage = dynamic(() => import('~/client/components/Admin/UserGroup/UserGroupPage').then(mod => mod.UserGroupPage), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
-
+const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), {
+  ssr: false,
+});
+const UserGroupPage = dynamic(
+  () =>
+    import('~/client/components/Admin/UserGroup/UserGroupPage').then(
+      (mod) => mod.UserGroupPage,
+    ),
+  { ssr: false },
+);
+const ForbiddenPage = dynamic(
+  () =>
+    import('~/client/components/Admin/ForbiddenPage').then(
+      (mod) => mod.ForbiddenPage,
+    ),
+  { ssr: false },
+);
 
 type Props = CommonProps & {
-  isAclEnabled: boolean
+  isAclEnabled: boolean;
 };
 
-
 const AdminUserGroupPage: NextPage<Props> = (props) => {
   const { t } = useTranslation('admin');
   useCurrentUser(props.currentUser ?? null);
@@ -44,8 +58,10 @@ const AdminUserGroupPage: NextPage<Props> = (props) => {
   );
 };
 
-
-const injectServerConfigurations = async(context: GetServerSidePropsContext, props: Props): Promise<void> => {
+const injectServerConfigurations = async (
+  context: GetServerSidePropsContext,
+  props: Props,
+): Promise<void> => {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { aclService } = crowi;
@@ -53,10 +69,14 @@ const injectServerConfigurations = async(context: GetServerSidePropsContext, pro
   props.isAclEnabled = aclService.isAclEnabled();
 };
 
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
-  const props = await retrieveServerSideProps(context, injectServerConfigurations);
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
+  const props = await retrieveServerSideProps(
+    context,
+    injectServerConfigurations,
+  );
   return props;
 };
 
-
 export default AdminUserGroupPage;

+ 27 - 14
apps/app/src/pages/admin/users/external-accounts.page.tsx

@@ -1,11 +1,12 @@
 import { useEffect, useMemo } from 'react';
-
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import { useTranslation } from 'next-i18next';
 import type { Container } from 'unstated';
 import { Provider } from 'unstated';
 
@@ -14,10 +15,20 @@ import { useCurrentUser } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../../utils/admin-page-util';
 
-const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
-const ManageExternalAccount = dynamic(() => import('~/client/components/Admin/ManageExternalAccount'), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
-
+const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), {
+  ssr: false,
+});
+const ManageExternalAccount = dynamic(
+  () => import('~/client/components/Admin/ManageExternalAccount'),
+  { ssr: false },
+);
+const ForbiddenPage = dynamic(
+  () =>
+    import('~/client/components/Admin/ForbiddenPage').then(
+      (mod) => mod.ForbiddenPage,
+    ),
+  { ssr: false },
+);
 
 const AdminUserManagementPage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('admin');
@@ -28,9 +39,12 @@ const AdminUserManagementPage: NextPage<CommonProps> = (props) => {
   const injectableContainers: Container<any>[] = useMemo(() => [], []);
 
   useEffect(() => {
-    (async() => {
-      const AdminExternalAccountsContainer = (await import('~/client/services/AdminExternalAccountsContainer')).default;
-      const adminExternalAccountsContainer = new AdminExternalAccountsContainer();
+    (async () => {
+      const AdminExternalAccountsContainer = (
+        await import('~/client/services/AdminExternalAccountsContainer')
+      ).default;
+      const adminExternalAccountsContainer =
+        new AdminExternalAccountsContainer();
       injectableContainers.push(adminExternalAccountsContainer);
     })();
   }, [injectableContainers]);
@@ -49,14 +63,13 @@ const AdminUserManagementPage: NextPage<CommonProps> = (props) => {
       </AdminLayout>
     </Provider>
   );
-
 };
 
-
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const props = await retrieveServerSideProps(context);
   return props;
 };
 
-
 export default AdminUserManagementPage;

+ 34 - 16
apps/app/src/pages/admin/users/index.page.tsx

@@ -1,11 +1,12 @@
 import { useEffect, useMemo } from 'react';
-
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import { useTranslation } from 'next-i18next';
 import type { Container } from 'unstated';
 import { Provider } from 'unstated';
 
@@ -16,16 +17,25 @@ import { useCurrentUser, useIsMailerSetup } from '~/stores-universal/context';
 
 import { retrieveServerSideProps } from '../../../utils/admin-page-util';
 
-const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
-const UserManagement = dynamic(() => import('~/client/components/Admin/UserManagement'), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/client/components/Admin/ForbiddenPage').then(mod => mod.ForbiddenPage), { ssr: false });
-
+const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), {
+  ssr: false,
+});
+const UserManagement = dynamic(
+  () => import('~/client/components/Admin/UserManagement'),
+  { ssr: false },
+);
+const ForbiddenPage = dynamic(
+  () =>
+    import('~/client/components/Admin/ForbiddenPage').then(
+      (mod) => mod.ForbiddenPage,
+    ),
+  { ssr: false },
+);
 
 type Props = CommonProps & {
-  isMailerSetup: boolean,
+  isMailerSetup: boolean;
 };
 
-
 const AdminUserManagementPage: NextPage<Props> = (props) => {
   const { t } = useTranslation('admin');
   useCurrentUser(props.currentUser ?? null);
@@ -37,8 +47,10 @@ const AdminUserManagementPage: NextPage<Props> = (props) => {
   const injectableContainers: Container<any>[] = useMemo(() => [], []);
 
   useEffect(() => {
-    (async() => {
-      const AdminUsersContainer = (await import('~/client/services/AdminUsersContainer')).default;
+    (async () => {
+      const AdminUsersContainer = (
+        await import('~/client/services/AdminUsersContainer')
+      ).default;
       const adminUsersContainer = new AdminUsersContainer();
       injectableContainers.push(adminUsersContainer);
     })();
@@ -58,10 +70,12 @@ const AdminUserManagementPage: NextPage<Props> = (props) => {
       </AdminLayout>
     </Provider>
   );
-
 };
 
-const injectServerConfigurations = async(context: GetServerSidePropsContext, props: Props): Promise<void> => {
+const injectServerConfigurations = async (
+  context: GetServerSidePropsContext,
+  props: Props,
+): Promise<void> => {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { mailService } = crowi;
@@ -69,10 +83,14 @@ const injectServerConfigurations = async(context: GetServerSidePropsContext, pro
   props.isMailerSetup = mailService.isMailerSetup;
 };
 
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
-  const props = await retrieveServerSideProps(context, injectServerConfigurations);
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
+  const props = await retrieveServerSideProps(
+    context,
+    injectServerConfigurations,
+  );
   return props;
 };
 
-
 export default AdminUserManagementPage;

+ 53 - 20
apps/app/src/pages/forgot-password-errors.page.tsx

@@ -1,17 +1,23 @@
 import React from 'react';
-
-import type { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
+import type {
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
+} from 'next';
+import Link from 'next/link';
 import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
-import Link from 'next/link';
 
 import { forgotPasswordErrorCode } from '~/interfaces/errors/forgot-password';
 
 import type { CommonProps } from './utils/commons';
-import { getNextI18NextConfig, getServerSideCommonProps } from './utils/commons';
+import {
+  getNextI18NextConfig,
+  getServerSideCommonProps,
+} from './utils/commons';
 
 type Props = CommonProps & {
-  errorCode?: forgotPasswordErrorCode
+  errorCode?: forgotPasswordErrorCode;
 };
 
 const ForgotPasswordErrorsPage: NextPage<Props> = (props: Props) => {
@@ -25,28 +31,45 @@ const ForgotPasswordErrorsPage: NextPage<Props> = (props: Props) => {
           <div className="row justify-content-md-center">
             <div className="col-md-6 mt-5">
               <div className="text-center">
-                <h1><span className="material-symbols-outlined large">lock_open</span></h1>
-                <h2 className="text-center">{ t('forgot_password.reset_password') }</h2>
-
-                { errorCode == null && (
+                <h1>
+                  <span className="material-symbols-outlined large">
+                    lock_open
+                  </span>
+                </h1>
+                <h2 className="text-center">
+                  {t('forgot_password.reset_password')}
+                </h2>
+
+                {errorCode == null && (
                   <h3 className="text-muted">errorCode Unknown</h3>
                 )}
 
-                { errorCode === forgotPasswordErrorCode.PASSWORD_RESET_IS_UNAVAILABLE && (
-                  <h3 className="text-muted">{ t('forgot_password.feature_is_unavailable') }</h3>
+                {errorCode ===
+                  forgotPasswordErrorCode.PASSWORD_RESET_IS_UNAVAILABLE && (
+                  <h3 className="text-muted">
+                    {t('forgot_password.feature_is_unavailable')}
+                  </h3>
                 )}
 
-                { (errorCode === forgotPasswordErrorCode.PASSWORD_RESET_ORDER_IS_NOT_APPROPRIATE || errorCode === forgotPasswordErrorCode.TOKEN_NOT_FOUND) && (
+                {(errorCode ===
+                  forgotPasswordErrorCode.PASSWORD_RESET_ORDER_IS_NOT_APPROPRIATE ||
+                  errorCode === forgotPasswordErrorCode.TOKEN_NOT_FOUND) && (
                   <div>
                     <div className="alert alert-warning mb-3">
-                      <h2>{ t('forgot_password.incorrect_token_or_expired_url') }</h2>
+                      <h2>
+                        {t('forgot_password.incorrect_token_or_expired_url')}
+                      </h2>
                     </div>
-                    <Link href="/forgot-password" className="link-switch" prefetch={false}>
-                      <span className="material-symbols-outlined">key</span> { t('forgot_password.forgot_password') }
+                    <Link
+                      href="/forgot-password"
+                      className="link-switch"
+                      prefetch={false}
+                    >
+                      <span className="material-symbols-outlined">key</span>{' '}
+                      {t('forgot_password.forgot_password')}
                     </Link>
                   </div>
-                ) }
-
+                )}
               </div>
             </div>
           </div>
@@ -56,12 +79,22 @@ const ForgotPasswordErrorsPage: NextPage<Props> = (props: Props) => {
   );
 };
 
-async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
-  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+async function injectNextI18NextConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+  namespacesRequired?: string[] | undefined,
+): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(
+    serverSideTranslations,
+    context,
+    namespacesRequired,
+  );
   props._nextI18Next = nextI18NextConfig._nextI18Next;
 }
 
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const result = await getServerSideCommonProps(context);
 
   // check for presence

+ 36 - 11
apps/app/src/pages/forgot-password.page.tsx

@@ -1,20 +1,29 @@
 import React from 'react';
-
-import type { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
-import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
+import type {
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
+} from 'next';
 import dynamic from 'next/dynamic';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 
 import { RawLayout } from '~/components/Layout/RawLayout';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import { useIsMailerSetup } from '~/stores-universal/context';
 
 import type { CommonProps } from './utils/commons';
-import { getNextI18NextConfig, getServerSideCommonProps } from './utils/commons';
+import {
+  getNextI18NextConfig,
+  getServerSideCommonProps,
+} from './utils/commons';
 
-const PasswordResetRequestForm = dynamic(() => import('~/client/components/PasswordResetRequestForm'), { ssr: false });
+const PasswordResetRequestForm = dynamic(
+  () => import('~/client/components/PasswordResetRequestForm'),
+  { ssr: false },
+);
 
 type Props = CommonProps & {
-  isMailerSetup: boolean,
+  isMailerSetup: boolean;
 };
 
 const ForgotPasswordPage: NextPage<Props> = (props: Props) => {
@@ -40,12 +49,23 @@ const ForgotPasswordPage: NextPage<Props> = (props: Props) => {
 };
 
 // eslint-disable-next-line max-len
-async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
-  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+async function injectNextI18NextConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+  namespacesRequired?: string[] | undefined,
+): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(
+    serverSideTranslations,
+    context,
+    namespacesRequired,
+  );
   props._nextI18Next = nextI18NextConfig._nextI18Next;
 }
 
-const injectServerConfigurations = async(context: GetServerSidePropsContext, props: Props): Promise<void> => {
+const injectServerConfigurations = async (
+  context: GetServerSidePropsContext,
+  props: Props,
+): Promise<void> => {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { mailService } = crowi;
@@ -53,7 +73,9 @@ const injectServerConfigurations = async(context: GetServerSidePropsContext, pro
   props.isMailerSetup = mailService.isMailerSetup;
 };
 
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const result = await getServerSideCommonProps(context);
 
   // check for presence
@@ -65,7 +87,10 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
   const props: Props = result.props as Props;
 
   injectServerConfigurations(context, props);
-  await injectNextI18NextConfigurations(context, props, ['translation', 'commons']);
+  await injectNextI18NextConfigurations(context, props, [
+    'translation',
+    'commons',
+  ]);
 
   return {
     props,

+ 74 - 24
apps/app/src/pages/installer.page.tsx

@@ -1,56 +1,95 @@
 import React, { useMemo } from 'react';
-
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
-import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import { useTranslation } from 'next-i18next';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 
 import { NoLoginLayout } from '~/components/Layout/NoLoginLayout';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import {
-  useCsrfToken, useAppTitle, useSiteUrl, useConfidential, useGrowiCloudUri,
+  useAppTitle,
+  useConfidential,
+  useCsrfToken,
+  useGrowiCloudUri,
+  useSiteUrl,
 } from '~/stores-universal/context';
 
 import type { CommonProps } from './utils/commons';
-import { getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle } from './utils/commons';
-
-
-const InstallerForm = dynamic(() => import('~/client/components/InstallerForm'), { ssr: false });
-const DataTransferForm = dynamic(() => import('~/client/components/DataTransferForm'), { ssr: false });
-const CustomNavAndContents = dynamic(() => import('~/client/components/CustomNavigation/CustomNavAndContents'), { ssr: false });
-
-async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
-  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired, true);
+import {
+  generateCustomTitle,
+  getNextI18NextConfig,
+  getServerSideCommonProps,
+} from './utils/commons';
+
+const InstallerForm = dynamic(
+  () => import('~/client/components/InstallerForm'),
+  { ssr: false },
+);
+const DataTransferForm = dynamic(
+  () => import('~/client/components/DataTransferForm'),
+  { ssr: false },
+);
+const CustomNavAndContents = dynamic(
+  () => import('~/client/components/CustomNavigation/CustomNavAndContents'),
+  { ssr: false },
+);
+
+async function injectNextI18NextConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+  namespacesRequired?: string[] | undefined,
+): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(
+    serverSideTranslations,
+    context,
+    namespacesRequired,
+    true,
+  );
   props._nextI18Next = nextI18NextConfig._nextI18Next;
 }
 
 type Props = CommonProps & {
-  minPasswordLength: number,
-  pageWithMetaStr: string,
+  minPasswordLength: number;
+  pageWithMetaStr: string;
 };
 
+const UserInfoIcon = () => (
+  <span className="material-symbols-outlined me-2">person</span>
+);
+
+const ExternalAccountsIcon = () => (
+  <span className="growi-custom-icons me-2">external_link</span>
+);
+
 const InstallerPage: NextPage<Props> = (props: Props) => {
   const { t } = useTranslation();
   const { t: tCommons } = useTranslation('commons');
 
+  const BoundInstallerForm = useMemo(
+    () => () => <InstallerForm minPasswordLength={props.minPasswordLength} />,
+    [props.minPasswordLength],
+  );
+
   const navTabMapping = useMemo(() => {
     return {
       user_infomation: {
-        Icon: () => <span className="material-symbols-outlined me-2">person</span>,
-        Content: () => <InstallerForm minPasswordLength={props.minPasswordLength} />,
+        Icon: UserInfoIcon,
+        Content: BoundInstallerForm,
         i18n: t('installer.tab'),
       },
       external_accounts: {
         // TODO: chack and fix font-size. see: https://redmine.weseek.co.jp/issues/143015
-        Icon: () => <span className="growi-custom-icons me-2">external_link</span>,
+        Icon: ExternalAccountsIcon,
         Content: DataTransferForm,
         i18n: tCommons('g2g_data_transfer.tab'),
       },
     };
-  }, [props.minPasswordLength, t, tCommons]);
+  }, [BoundInstallerForm, t, tCommons]);
 
   // commons
   useAppTitle(props.appTitle);
@@ -67,14 +106,23 @@ const InstallerPage: NextPage<Props> = (props: Props) => {
       <Head>
         <title>{title}</title>
       </Head>
-      <div id="installer-form-container" className="nologin-dialog mx-auto rounded-4 rounded-top-0">
-        <CustomNavAndContents navTabMapping={navTabMapping} tabContentClasses={['p-0']} />
+      <div
+        id="installer-form-container"
+        className="nologin-dialog mx-auto rounded-4 rounded-top-0"
+      >
+        <CustomNavAndContents
+          navTabMapping={navTabMapping}
+          tabContentClasses={['p-0']}
+        />
       </div>
     </NoLoginLayout>
   );
 };
 
-async function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): Promise<void> {
+async function injectServerConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+): Promise<void> {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { configManager } = crowi;
@@ -82,7 +130,9 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
   props.minPasswordLength = configManager.getConfig('app:minPasswordLength');
 }
 
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const result = await getServerSideCommonProps(context);
 
   // check for presence

+ 48 - 19
apps/app/src/pages/invited.page.tsx

@@ -1,27 +1,42 @@
 import React from 'react';
-
+import type {
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
+} from 'next';
+import dynamic from 'next/dynamic';
+import Head from 'next/head';
 import type { IUser } from '@growi/core';
 import { USER_STATUS } from '@growi/core';
-import type { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
-import dynamic from 'next/dynamic';
-import Head from 'next/head';
 
 import { NoLoginLayout } from '~/components/Layout/NoLoginLayout';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
-import { useCsrfToken, useCurrentPathname, useCurrentUser } from '~/stores-universal/context';
+import {
+  useCsrfToken,
+  useCurrentPathname,
+  useCurrentUser,
+} from '~/stores-universal/context';
 
 import type { CommonProps } from './utils/commons';
-import { getServerSideCommonProps, generateCustomTitle, getNextI18NextConfig } from './utils/commons';
-
-const InvitedForm = dynamic(() => import('~/client/components/InvitedForm').then(mod => mod.InvitedForm), { ssr: false });
+import {
+  generateCustomTitle,
+  getNextI18NextConfig,
+  getServerSideCommonProps,
+} from './utils/commons';
+
+const InvitedForm = dynamic(
+  () =>
+    import('~/client/components/InvitedForm').then((mod) => mod.InvitedForm),
+  { ssr: false },
+);
 
 type Props = CommonProps & {
-  currentUser: IUser,
-  invitedFormUsername: string,
-  invitedFormName: string,
-}
+  currentUser: IUser;
+  invitedFormUsername: string;
+  invitedFormName: string;
+};
 
 const InvitedPage: NextPage<Props> = (props: Props) => {
   const { t } = useTranslation();
@@ -38,13 +53,18 @@ const InvitedPage: NextPage<Props> = (props: Props) => {
       <Head>
         <title>{title}</title>
       </Head>
-      <InvitedForm invitedFormUsername={props.invitedFormUsername} invitedFormName={props.invitedFormName} />
+      <InvitedForm
+        invitedFormUsername={props.invitedFormUsername}
+        invitedFormName={props.invitedFormName}
+      />
     </NoLoginLayout>
   );
-
 };
 
-async function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): Promise<void> {
+async function injectServerConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+): Promise<void> {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { body: invitedForm } = req;
 
@@ -62,12 +82,22 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
  * @param props
  * @param namespacesRequired
  */
-async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
-  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+async function injectNextI18NextConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+  namespacesRequired?: string[] | undefined,
+): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(
+    serverSideTranslations,
+    context,
+    namespacesRequired,
+  );
   props._nextI18Next = nextI18NextConfig._nextI18Next;
 }
 
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const req = context.req as CrowiRequest;
   const { user } = req;
   const result = await getServerSideCommonProps(context);
@@ -91,7 +121,6 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
         },
       };
     }
-
   }
 
   await injectServerConfigurations(context, props);

+ 72 - 59
apps/app/src/pages/login/error/[message].page.tsx

@@ -1,71 +1,77 @@
 import React from 'react';
-
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
+import { useRouter } from 'next/router';
 import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
-import { useRouter } from 'next/router';
 
 import { NoLoginLayout } from '~/components/Layout/NoLoginLayout';
 import type { CommonProps } from '~/pages/utils/commons';
-import { getServerSideCommonProps, getNextI18NextConfig } from '~/pages/utils/commons';
-
+import {
+  getNextI18NextConfig,
+  getServerSideCommonProps,
+} from '~/pages/utils/commons';
 
 type Props = CommonProps;
 const classNames: string[] = ['login-page'];
 
-const LoginPage: NextPage<CommonProps> = () => {
-
+const ApprovalPendingUserError = () => {
   const { t } = useTranslation();
-  const router = useRouter();
-  const { message } = router.query;
-
+  return (
+    <>
+      <div className="alert alert-warning">
+        <h2>{t('login.sign_in_error')}</h2>
+      </div>
+      <p>Wait for approved by administrators.</p>
+    </>
+  );
+};
 
-  let loginErrorElm;
+const SuspendedUserError = () => {
+  const { t } = useTranslation();
+  return (
+    <>
+      <div className="alert alert-warning">
+        <h2>{t('login.sign_in_error')}</h2>
+      </div>
+      <p>This account is suspended.</p>
+    </>
+  );
+};
 
-  const ApprovalPendingUserError = () => {
-    return (
-      <>
-        <div className="alert alert-warning">
-          <h2>{ t('login.sign_in_error') }</h2>
-        </div>
-        <p>Wait for approved by administrators.</p>
-      </>
-    );
-  };
+const PasswordResetOrderError = () => {
+  const { t } = useTranslation();
+  return (
+    <>
+      <div className="alert alert-warning mb-3">
+        <h2>{t('forgot_password.incorrect_token_or_expired_url')}</h2>
+      </div>
+      <a href="/forgot-password" className="link-switch">
+        <span className="material-symbols-outlined">key</span>{' '}
+        {t('forgot_password.forgot_password')}
+      </a>
+    </>
+  );
+};
 
-  const SuspendedUserError = () => {
-    return (
-      <>
-        <div className="alert alert-warning">
-          <h2>{ t('login.sign_in_error') }</h2>
-        </div>
-        <p>This account is suspended.</p>
-      </>
-    );
-  };
+const DefaultLoginError = () => {
+  const { t } = useTranslation();
+  return (
+    <div className="alert alert-warning">
+      <h2>{t('login.sign_in_error')}</h2>
+    </div>
+  );
+};
 
-  const PasswordResetOrderError = () => {
-    return (
-      <>
-        <div className="alert alert-warning mb-3">
-          <h2>{ t('forgot_password.incorrect_token_or_expired_url') }</h2>
-        </div>
-        <a href="/forgot-password" className="link-switch">
-          <span className="material-symbols-outlined">key</span> { t('forgot_password.forgot_password') }
-        </a>
-      </>
-    );
-  };
+const LoginPage: NextPage<CommonProps> = () => {
+  const { t } = useTranslation();
+  const router = useRouter();
+  const { message } = router.query;
 
-  const DefaultLoginError = () => {
-    return (
-      <div className="alert alert-warning">
-        <h2>{ t('login.sign_in_error') }</h2>
-      </div>
-    );
-  };
+  let loginErrorElm: JSX.Element;
 
   switch (message) {
     case 'registered':
@@ -81,17 +87,15 @@ const LoginPage: NextPage<CommonProps> = () => {
       loginErrorElm = <DefaultLoginError />;
   }
 
-
   return (
     <NoLoginLayout className={classNames.join(' ')}>
       <div className="mb-4 login-form-errors text-center">
         <div className="nologin-dialog pb-4 mx-auto">
-          <div className="col-12">
-            {loginErrorElm}
-          </div>
+          <div className="col-12">{loginErrorElm}</div>
           {/* If the transition source is "/login", use <a /> tag since the transition will not occur if next/link is used. */}
           <a href="/login">
-            <span className="material-symbols-outlined me-1">login</span>{t('Sign in is here')}
+            <span className="material-symbols-outlined me-1">login</span>
+            {t('Sign in is here')}
           </a>
         </div>
       </div>
@@ -105,13 +109,22 @@ const LoginPage: NextPage<CommonProps> = () => {
  * @param props
  * @param namespacesRequired
  */
-async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
-  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+async function injectNextI18NextConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+  namespacesRequired?: string[] | undefined,
+): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(
+    serverSideTranslations,
+    context,
+    namespacesRequired,
+  );
   props._nextI18Next = nextI18NextConfig._nextI18Next;
 }
 
-
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const result = await getServerSideCommonProps(context);
 
   // check for presence

+ 88 - 52
apps/app/src/pages/login/index.page.tsx

@@ -1,13 +1,14 @@
 import React from 'react';
-
-import { pagePathUtils } from '@growi/core/dist/utils';
 import type {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
 } from 'next';
-import { useTranslation } from 'next-i18next';
-import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import { pagePathUtils } from '@growi/core/dist/utils';
+import { useTranslation } from 'next-i18next';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 
 import { NoLoginLayout } from '~/components/Layout/NoLoginLayout';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
@@ -16,33 +17,35 @@ import { isExternalAccountLoginError } from '~/interfaces/errors/external-accoun
 import { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
 import type { RegistrationMode } from '~/interfaces/registration-mode';
 import type { CommonProps } from '~/pages/utils/commons';
-import { getServerSideCommonProps, generateCustomTitle, getNextI18NextConfig } from '~/pages/utils/commons';
 import {
-  useCsrfToken,
-  useCurrentPathname,
-} from '~/stores-universal/context';
+  generateCustomTitle,
+  getNextI18NextConfig,
+  getServerSideCommonProps,
+} from '~/pages/utils/commons';
+import { useCsrfToken, useCurrentPathname } from '~/stores-universal/context';
 
 import styles from './index.module.scss';
 
-
 const { isPermalink, isUserPage, isUsersTopPage } = pagePathUtils;
 
-const LoginForm = dynamic(() => import('~/client/components/LoginForm').then(mod => mod.LoginForm), { ssr: false });
-
+const LoginForm = dynamic(
+  () => import('~/client/components/LoginForm').then((mod) => mod.LoginForm),
+  { ssr: false },
+);
 
 type Props = CommonProps & {
-  registrationMode: RegistrationMode,
-  pageWithMetaStr: string,
-  isMailerSetup: boolean,
-  enabledExternalAuthType: IExternalAuthProviderType[],
-  registrationWhitelist: string[],
-  isLocalStrategySetup: boolean,
-  isLdapStrategySetup: boolean,
-  isLdapSetupFailed: boolean,
-  isPasswordResetEnabled: boolean,
-  isEmailAuthenticationEnabled: boolean,
-  externalAccountLoginError?: IExternalAccountLoginError,
-  minPasswordLength: number,
+  registrationMode: RegistrationMode;
+  pageWithMetaStr: string;
+  isMailerSetup: boolean;
+  enabledExternalAuthType: IExternalAuthProviderType[];
+  registrationWhitelist: string[];
+  isLocalStrategySetup: boolean;
+  isLdapStrategySetup: boolean;
+  isLdapSetupFailed: boolean;
+  isPasswordResetEnabled: boolean;
+  isEmailAuthenticationEnabled: boolean;
+  externalAccountLoginError?: IExternalAccountLoginError;
+  minPasswordLength: number;
 };
 
 const LoginPage: NextPage<Props> = (props: Props) => {
@@ -85,56 +88,86 @@ const LoginPage: NextPage<Props> = (props: Props) => {
  * @param props
  * @param namespacesRequired
  */
-async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
-  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+async function injectNextI18NextConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+  namespacesRequired?: string[] | undefined,
+): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(
+    serverSideTranslations,
+    context,
+    namespacesRequired,
+  );
   props._nextI18Next = nextI18NextConfig._nextI18Next;
 }
 
-function injectEnabledStrategies(context: GetServerSidePropsContext, props: Props): void {
+function injectEnabledStrategies(
+  context: GetServerSidePropsContext,
+  props: Props,
+): void {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
-  const {
-    configManager,
-  } = crowi;
+  const { configManager } = crowi;
 
   props.enabledExternalAuthType = [
-    configManager.getConfig('security:passport-google:isEnabled') === true ? IExternalAuthProviderType.google : undefined,
-    configManager.getConfig('security:passport-github:isEnabled') === true ? IExternalAuthProviderType.github : undefined,
-    configManager.getConfig('security:passport-saml:isEnabled') === true ? IExternalAuthProviderType.saml : undefined,
-    configManager.getConfig('security:passport-oidc:isEnabled') === true ? IExternalAuthProviderType.oidc : undefined,
-
-  ]
-    .filter((authType): authType is Exclude<typeof authType, undefined> => authType != null);
+    configManager.getConfig('security:passport-google:isEnabled') === true
+      ? IExternalAuthProviderType.google
+      : undefined,
+    configManager.getConfig('security:passport-github:isEnabled') === true
+      ? IExternalAuthProviderType.github
+      : undefined,
+    configManager.getConfig('security:passport-saml:isEnabled') === true
+      ? IExternalAuthProviderType.saml
+      : undefined,
+    configManager.getConfig('security:passport-oidc:isEnabled') === true
+      ? IExternalAuthProviderType.oidc
+      : undefined,
+  ].filter(
+    (authType): authType is Exclude<typeof authType, undefined> =>
+      authType != null,
+  );
 }
 
-async function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): Promise<void> {
+async function injectServerConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+): Promise<void> {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
-  const {
-    mailService,
-    configManager,
-    passportService,
-  } = crowi;
+  const { mailService, configManager, passportService } = crowi;
 
-  props.isPasswordResetEnabled = configManager.getConfig('security:passport-local:isPasswordResetEnabled');
+  props.isPasswordResetEnabled = configManager.getConfig(
+    'security:passport-local:isPasswordResetEnabled',
+  );
   props.isMailerSetup = mailService.isMailerSetup;
   props.isLocalStrategySetup = passportService.isLocalStrategySetup;
   props.isLdapStrategySetup = passportService.isLdapStrategySetup;
-  props.isLdapSetupFailed = configManager.getConfig('security:passport-ldap:isEnabled') && !props.isLdapStrategySetup;
-  props.registrationWhitelist = configManager.getConfig('security:registrationWhitelist');
-  props.isEmailAuthenticationEnabled = configManager.getConfig('security:passport-local:isEmailAuthenticationEnabled');
+  props.isLdapSetupFailed =
+    configManager.getConfig('security:passport-ldap:isEnabled') &&
+    !props.isLdapStrategySetup;
+  props.registrationWhitelist = configManager.getConfig(
+    'security:registrationWhitelist',
+  );
+  props.isEmailAuthenticationEnabled = configManager.getConfig(
+    'security:passport-local:isEmailAuthenticationEnabled',
+  );
   props.registrationMode = configManager.getConfig('security:registrationMode');
   props.minPasswordLength = configManager.getConfig('app:minPasswordLength');
 }
 
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const result = await getServerSideCommonProps(context);
 
-
   // redirect to the page the user was on before moving to the Login Page
   if (context.req.headers.referer != null) {
     const urlBeforeLogin = new URL(context.req.headers.referer);
-    if (isPermalink(urlBeforeLogin.pathname) || isUserPage(urlBeforeLogin.pathname) || isUsersTopPage(urlBeforeLogin.pathname)) {
+    if (
+      isPermalink(urlBeforeLogin.pathname) ||
+      isUserPage(urlBeforeLogin.pathname) ||
+      isUsersTopPage(urlBeforeLogin.pathname)
+    ) {
       (context.req as CrowiRequest).session.redirectTo = urlBeforeLogin.href;
     }
   }
@@ -147,12 +180,15 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
 
   const props: Props = result.props as Props;
 
-  const externalAccountLoginError = (context.req as CrowiRequest).session.externalAccountLoginError;
+  const externalAccountLoginError = (context.req as CrowiRequest).session
+    .externalAccountLoginError;
   if (externalAccountLoginError != null) {
     delete (context.req as CrowiRequest).session.externalAccountLoginError;
     const parsedError = JSON.parse(externalAccountLoginError);
     if (isExternalAccountLoginError(parsedError)) {
-      props.externalAccountLoginError = { ...parsedError as IExternalAccountLoginError };
+      props.externalAccountLoginError = {
+        ...(parsedError as IExternalAccountLoginError),
+      };
     }
   }
 

+ 29 - 10
apps/app/src/pages/maintenance.page.tsx

@@ -1,23 +1,32 @@
+import type {
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
+} from 'next';
+import dynamic from 'next/dynamic';
 import type { IUser } from '@growi/core';
-import type { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
-import dynamic from 'next/dynamic';
 
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import { useCurrentUser } from '~/stores-universal/context';
 
 import type { CommonProps } from './utils/commons';
-import { getServerSideCommonProps, getNextI18NextConfig } from './utils/commons';
-
+import {
+  getNextI18NextConfig,
+  getServerSideCommonProps,
+} from './utils/commons';
 
-const Maintenance = dynamic(() => import('~/client/components/Maintenance').then(mod => mod.Maintenance), { ssr: false });
+const Maintenance = dynamic(
+  () =>
+    import('~/client/components/Maintenance').then((mod) => mod.Maintenance),
+  { ssr: false },
+);
 
 type Props = CommonProps & {
-  currentUser: IUser,
+  currentUser: IUser;
 };
 
 const MaintenancePage: NextPage<CommonProps> = (props: Props) => {
-
   useCurrentUser(props.currentUser ?? null);
 
   return (
@@ -33,12 +42,22 @@ const MaintenancePage: NextPage<CommonProps> = (props: Props) => {
   );
 };
 
-async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
-  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+async function injectNextI18NextConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+  namespacesRequired?: string[] | undefined,
+): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(
+    serverSideTranslations,
+    context,
+    namespacesRequired,
+  );
   props._nextI18Next = nextI18NextConfig._nextI18Next;
 }
 
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const req = context.req as CrowiRequest;
 
   const result = await getServerSideCommonProps(context);

+ 116 - 61
apps/app/src/pages/me/[[...path]].page.tsx

@@ -1,62 +1,78 @@
-import React, { type ReactNode, useMemo, type JSX } from 'react';
-
-import type {
-  GetServerSideProps, GetServerSidePropsContext,
-} from 'next';
-import { useTranslation } from 'next-i18next';
-import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
+import React, { type JSX, type ReactNode, useMemo } from 'react';
+import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import { useRouter } from 'next/router';
+import { useTranslation } from 'next-i18next';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 
 import { BasicLayout } from '~/components/Layout/BasicLayout';
 import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { ISidebarConfig } from '~/interfaces/sidebar-config';
+import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 import {
-  useCurrentUser, useIsSearchPage, useGrowiCloudUri,
-  useIsSearchServiceConfigured, useIsSearchServiceReachable,
-  useCsrfToken, useIsSearchScopeChildrenAsDefault,
-  useRegistrationWhitelist, useShowPageLimitationXL, useRendererConfig, useIsEnabledMarp, useCurrentPathname,
+  useCsrfToken,
+  useCurrentPathname,
+  useCurrentUser,
+  useGrowiCloudUri,
+  useIsEnabledMarp,
+  useIsSearchPage,
+  useIsSearchScopeChildrenAsDefault,
+  useIsSearchServiceConfigured,
+  useIsSearchServiceReachable,
+  useRegistrationWhitelist,
+  useRendererConfig,
+  useShowPageLimitationXL,
 } from '~/stores-universal/context';
-import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 
 import type { NextPageWithLayout } from '../_app.page';
 import type { CommonProps } from '../utils/commons';
 import {
-  getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle, useInitSidebarConfig,
+  generateCustomTitle,
+  getNextI18NextConfig,
+  getServerSideCommonProps,
+  useInitSidebarConfig,
 } from '../utils/commons';
 
-
 const logger = loggerFactory('growi:pages:me');
 
 type Props = CommonProps & {
-  isSearchServiceConfigured: boolean,
-  isSearchServiceReachable: boolean,
-  isSearchScopeChildrenAsDefault: boolean,
-  isEnabledMarp: boolean,
-  rendererConfig: RendererConfig,
-  showPageLimitationXL: number,
+  isSearchServiceConfigured: boolean;
+  isSearchServiceReachable: boolean;
+  isSearchScopeChildrenAsDefault: boolean;
+  isEnabledMarp: boolean;
+  rendererConfig: RendererConfig;
+  showPageLimitationXL: number;
 
   // config
-  registrationWhitelist: string[],
+  registrationWhitelist: string[];
 
-  sidebarConfig: ISidebarConfig,
+  sidebarConfig: ISidebarConfig;
 };
 
-const PersonalSettings = dynamic(() => import('~/client/components/Me/PersonalSettings'), { ssr: false });
+const PersonalSettings = dynamic(
+  () => import('~/client/components/Me/PersonalSettings'),
+  { ssr: false },
+);
 // const MyDraftList = dynamic(() => import('~/components/MyDraftList/MyDraftList'), { ssr: false });
 const InAppNotificationPage = dynamic(
-  () => import('~/client/components/InAppNotification/InAppNotificationPage').then(mod => mod.InAppNotificationPage), { ssr: false },
+  () =>
+    import('~/client/components/InAppNotification/InAppNotificationPage').then(
+      (mod) => mod.InAppNotificationPage,
+    ),
+  { ssr: false },
 );
 
 const MePage: NextPageWithLayout<Props> = (props: Props) => {
   const router = useRouter();
   const { t } = useTranslation(['translation', 'commons']);
   const { path } = router.query;
-  const pagePathKeys: string[] = Array.isArray(path) ? path : ['personal-settings'];
+  const pagePathKeys: string[] = Array.isArray(path)
+    ? path
+    : ['personal-settings'];
 
   const mePagesMap = useMemo(() => {
     return {
@@ -75,7 +91,10 @@ const MePage: NextPageWithLayout<Props> = (props: Props) => {
     };
   }, [t]);
 
-  const getTargetPageToRender = (pagesMap, keys): {title: string, component: JSX.Element} => {
+  const getTargetPageToRender = (
+    pagesMap,
+    keys,
+  ): { title: string; component: JSX.Element } => {
     return keys.reduce((pagesMap, key) => {
       const page = pagesMap[key];
       if (page == null) {
@@ -131,11 +150,9 @@ const MePage: NextPageWithLayout<Props> = (props: Props) => {
 
         <div className="main ps-sidebar">
           <div className="container-lg wide-gutter-x-lg">
-
-            <h1 className="sticky-top py-2 fs-3">{ targetPage.title }</h1>
+            <h1 className="sticky-top py-2 fs-3">{targetPage.title}</h1>
 
             {targetPage.component}
-
           </div>
         </div>
       </div>
@@ -143,65 +160,85 @@ const MePage: NextPageWithLayout<Props> = (props: Props) => {
   );
 };
 
-
 type LayoutProps = Props & {
-  children?: ReactNode
-}
+  children?: ReactNode;
+};
 
 const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
   // init sidebar config with UserUISettings and sidebarConfig
   useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
 
-  return (
-    <BasicLayout>
-      {children}
-    </BasicLayout>
-  );
+  return <BasicLayout>{children}</BasicLayout>;
 };
 
 MePage.getLayout = function getLayout(page) {
   return <Layout {...page.props}>{page}</Layout>;
 };
 
-async function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): Promise<void> {
+async function injectServerConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+): Promise<void> {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
-  const {
-    searchService,
-    configManager,
-  } = crowi;
+  const { searchService, configManager } = crowi;
 
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceReachable = searchService.isReachable;
-  props.isSearchScopeChildrenAsDefault = configManager.getConfig('customize:isSearchScopeChildrenAsDefault');
+  props.isSearchScopeChildrenAsDefault = configManager.getConfig(
+    'customize:isSearchScopeChildrenAsDefault',
+  );
 
-  props.registrationWhitelist = configManager.getConfig('security:registrationWhitelist');
+  props.registrationWhitelist = configManager.getConfig(
+    'security:registrationWhitelist',
+  );
 
-  props.showPageLimitationXL = crowi.configManager.getConfig('customize:showPageLimitationXL');
+  props.showPageLimitationXL = crowi.configManager.getConfig(
+    'customize:showPageLimitationXL',
+  );
 
   props.sidebarConfig = {
-    isSidebarCollapsedMode: configManager.getConfig('customize:isSidebarCollapsedMode'),
-    isSidebarClosedAtDockMode: configManager.getConfig('customize:isSidebarClosedAtDockMode'),
+    isSidebarCollapsedMode: configManager.getConfig(
+      'customize:isSidebarCollapsedMode',
+    ),
+    isSidebarClosedAtDockMode: configManager.getConfig(
+      'customize:isSidebarClosedAtDockMode',
+    ),
   };
 
   props.rendererConfig = {
-    isEnabledLinebreaks: configManager.getConfig('markdown:isEnabledLinebreaks'),
-    isEnabledLinebreaksInComments: configManager.getConfig('markdown:isEnabledLinebreaksInComments'),
+    isEnabledLinebreaks: configManager.getConfig(
+      'markdown:isEnabledLinebreaks',
+    ),
+    isEnabledLinebreaksInComments: configManager.getConfig(
+      'markdown:isEnabledLinebreaksInComments',
+    ),
     isEnabledMarp: configManager.getConfig('customize:isEnabledMarp'),
-    adminPreferredIndentSize: configManager.getConfig('markdown:adminPreferredIndentSize'),
+    adminPreferredIndentSize: configManager.getConfig(
+      'markdown:adminPreferredIndentSize',
+    ),
     isIndentSizeForced: configManager.getConfig('markdown:isIndentSizeForced'),
 
     drawioUri: configManager.getConfig('app:drawioUri'),
     plantumlUri: configManager.getConfig('app:plantumlUri'),
 
     // XSS Options
-    isEnabledXssPrevention: configManager.getConfig('markdown:rehypeSanitize:isEnabledPrevention'),
+    isEnabledXssPrevention: configManager.getConfig(
+      'markdown:rehypeSanitize:isEnabledPrevention',
+    ),
     sanitizeType: configManager.getConfig('markdown:rehypeSanitize:option'),
-    customTagWhitelist: crowi.configManager.getConfig('markdown:rehypeSanitize:tagNames'),
-    customAttrWhitelist: configManager.getConfig('markdown:rehypeSanitize:attributes') != null
-      ? JSON.parse(configManager.getConfig('markdown:rehypeSanitize:attributes'))
-      : undefined,
-    highlightJsStyleBorder: crowi.configManager.getConfig('customize:highlightJsStyleBorder'),
+    customTagWhitelist: crowi.configManager.getConfig(
+      'markdown:rehypeSanitize:tagNames',
+    ),
+    customAttrWhitelist:
+      configManager.getConfig('markdown:rehypeSanitize:attributes') != null
+        ? JSON.parse(
+            configManager.getConfig('markdown:rehypeSanitize:attributes'),
+          )
+        : undefined,
+    highlightJsStyleBorder: crowi.configManager.getConfig(
+      'customize:highlightJsStyleBorder',
+    ),
   };
 }
 
@@ -211,13 +248,24 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
 //  * @param props
 //  * @param namespacesRequired
 //  */
-async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
+async function injectNextI18NextConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+  namespacesRequired?: string[] | undefined,
+): Promise<void> {
   // preload all languages because of language lists in user setting
-  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired, true);
+  const nextI18NextConfig = await getNextI18NextConfig(
+    serverSideTranslations,
+    context,
+    namespacesRequired,
+    true,
+  );
   props._nextI18Next = nextI18NextConfig._nextI18Next;
 }
 
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const req = context.req as CrowiRequest;
   const { user, crowi } = req;
 
@@ -233,12 +281,19 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
 
   if (user != null) {
     const User = crowi.model('User');
-    const userData = await User.findById(user.id).populate({ path: 'imageAttachment', select: 'filePathProxied' });
+    const userData = await User.findById(user.id).populate({
+      path: 'imageAttachment',
+      select: 'filePathProxied',
+    });
     props.currentUser = userData.toObject();
   }
 
   await injectServerConfigurations(context, props);
-  await injectNextI18NextConfigurations(context, props, ['translation', 'admin', 'commons']);
+  await injectNextI18NextConfigurations(context, props, [
+    'translation',
+    'admin',
+    'commons',
+  ]);
 
   return {
     props,

+ 44 - 15
apps/app/src/pages/reset-password.page.tsx

@@ -1,21 +1,29 @@
 import React from 'react';
-
-import type { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
+import type {
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
+} from 'next';
+import dynamic from 'next/dynamic';
 import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
-import dynamic from 'next/dynamic';
 
 import { RawLayout } from '~/components/Layout/RawLayout';
 
 import type { CommonProps } from './utils/commons';
-import { getNextI18NextConfig, getServerSideCommonProps } from './utils/commons';
-
+import {
+  getNextI18NextConfig,
+  getServerSideCommonProps,
+} from './utils/commons';
 
 type Props = CommonProps & {
-  email: string
+  email: string;
 };
 
-const PasswordResetExecutionForm = dynamic(() => import('~/client/components/PasswordResetExecutionForm'), { ssr: false });
+const PasswordResetExecutionForm = dynamic(
+  () => import('~/client/components/PasswordResetExecutionForm'),
+  { ssr: false },
+);
 
 const ForgotPasswordPage: NextPage<Props> = (props: Props) => {
   const { t } = useTranslation();
@@ -28,10 +36,18 @@ const ForgotPasswordPage: NextPage<Props> = (props: Props) => {
             <div className="row justify-content-md-center">
               <div className="col-md-6 mt-5">
                 <div className="text-center">
-                  <h1><span className="material-symbols-outlined large">lock_open</span></h1>
-                  <h2 className="text-center">{ t('forgot_password.reset_password') }</h2>
-                  <h5>{ props.email }</h5>
-                  <p className="mt-4">{ t('forgot_password.password_reset_excecution_desc') }</p>
+                  <h1>
+                    <span className="material-symbols-outlined large">
+                      lock_open
+                    </span>
+                  </h1>
+                  <h2 className="text-center">
+                    {t('forgot_password.reset_password')}
+                  </h2>
+                  <h5>{props.email}</h5>
+                  <p className="mt-4">
+                    {t('forgot_password.password_reset_excecution_desc')}
+                  </p>
                   <PasswordResetExecutionForm />
                 </div>
               </div>
@@ -44,12 +60,22 @@ const ForgotPasswordPage: NextPage<Props> = (props: Props) => {
 };
 
 // eslint-disable-next-line max-len
-async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
-  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+async function injectNextI18NextConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+  namespacesRequired?: string[] | undefined,
+): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(
+    serverSideTranslations,
+    context,
+    namespacesRequired,
+  );
   props._nextI18Next = nextI18NextConfig._nextI18Next;
 }
 
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const result = await getServerSideCommonProps(context);
 
   // check for presence
@@ -65,7 +91,10 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
     props.email = email;
   }
 
-  await injectNextI18NextConfigurations(context, props, ['translation', 'commons']);
+  await injectNextI18NextConfigurations(context, props, [
+    'translation',
+    'commons',
+  ]);
 
   return {
     props,

+ 146 - 76
apps/app/src/pages/share/[[...path]].page.tsx

@@ -1,12 +1,9 @@
-import React, { useEffect, type JSX } from 'react';
-
-import { type IPagePopulatedToShowRevision, getIdForRef } from '@growi/core';
-import type {
-  GetServerSideProps, GetServerSidePropsContext,
-} from 'next';
-import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
+import React, { type JSX, useEffect } from 'react';
+import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import { getIdForRef, type IPagePopulatedToShowRevision } from '@growi/core';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import superjson from 'superjson';
 
 import { ShareLinkLayout } from '~/components/Layout/ShareLinkLayout';
@@ -21,41 +18,59 @@ import type { IShareLinkHasId } from '~/interfaces/share-link';
 import type { PageDocument, PageModel } from '~/server/models/page';
 import ShareLink from '~/server/models/share-link';
 import {
-  useCurrentUser, useRendererConfig, useIsSearchPage, useCurrentPathname,
-  useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault, useIsContainerFluid, useIsEnabledMarp,
-  useIsLocalAccountRegistrationEnabled, useShowPageSideAuthors,
+  useCurrentPageId,
+  useIsNotFound,
+  useSWRMUTxCurrentPage,
+} from '~/stores/page';
+import {
+  useCurrentPathname,
+  useCurrentUser,
+  useIsContainerFluid,
+  useIsEnabledMarp,
+  useIsLocalAccountRegistrationEnabled,
+  useIsSearchPage,
+  useIsSearchScopeChildrenAsDefault,
+  useIsSearchServiceConfigured,
+  useIsSearchServiceReachable,
+  useRendererConfig,
+  useShareLinkId,
+  useShowPageSideAuthors,
 } from '~/stores-universal/context';
-import { useCurrentPageId, useIsNotFound, useSWRMUTxCurrentPage } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 
 import type { NextPageWithLayout } from '../_app.page';
 import type { CommonProps } from '../utils/commons';
 import {
-  getServerSideCommonProps, generateCustomTitleForPage, getNextI18NextConfig, skipSSR, addActivity,
+  addActivity,
+  generateCustomTitleForPage,
+  getNextI18NextConfig,
+  getServerSideCommonProps,
+  skipSSR,
 } from '../utils/commons';
 
-
-const GrowiContextualSubNavigationSubstance = dynamic(() => import('~/client/components/Navbar/GrowiContextualSubNavigation'), { ssr: false });
-
+const GrowiContextualSubNavigationSubstance = dynamic(
+  () => import('~/client/components/Navbar/GrowiContextualSubNavigation'),
+  { ssr: false },
+);
 
 const logger = loggerFactory('growi:next-page:share');
 
 type Props = CommonProps & {
-  shareLinkRelatedPage?: IShareLinkRelatedPage,
-  shareLink?: IShareLinkHasId,
-  isNotFound: boolean,
-  isExpired: boolean,
-  disableLinkSharing: boolean,
-  isSearchServiceConfigured: boolean,
-  isSearchServiceReachable: boolean,
-  isSearchScopeChildrenAsDefault: boolean,
-  showPageSideAuthors: boolean,
-  isEnabledMarp: boolean,
-  isLocalAccountRegistrationEnabled: boolean,
-  drawioUri: string | null,
-  rendererConfig: RendererConfig,
-  skipSSR: boolean,
-  ssrMaxRevisionBodyLength: number,
+  shareLinkRelatedPage?: IShareLinkRelatedPage;
+  shareLink?: IShareLinkHasId;
+  isNotFound: boolean;
+  isExpired: boolean;
+  disableLinkSharing: boolean;
+  isSearchServiceConfigured: boolean;
+  isSearchServiceReachable: boolean;
+  isSearchScopeChildrenAsDefault: boolean;
+  showPageSideAuthors: boolean;
+  isEnabledMarp: boolean;
+  isLocalAccountRegistrationEnabled: boolean;
+  drawioUri: string | null;
+  rendererConfig: RendererConfig;
+  skipSSR: boolean;
+  ssrMaxRevisionBodyLength: number;
 };
 
 type IShareLinkRelatedPage = IPagePopulatedToShowRevision & PageDocument;
@@ -63,11 +78,14 @@ type IShareLinkRelatedPage = IPagePopulatedToShowRevision & PageDocument;
 superjson.registerCustom<IShareLinkRelatedPage, string>(
   {
     isApplicable: (v): v is IShareLinkRelatedPage => {
-      return v != null
-        && v.toObject != null;
+      return v != null && v.toObject != null;
+    },
+    serialize: (v) => {
+      return superjson.stringify(v.toObject());
+    },
+    deserialize: (v) => {
+      return superjson.parse(v);
     },
-    serialize: (v) => { return superjson.stringify(v.toObject()) },
-    deserialize: (v) => { return superjson.parse(v) },
   },
   'IShareLinkRelatedPageTransformer',
 );
@@ -75,15 +93,20 @@ superjson.registerCustom<IShareLinkRelatedPage, string>(
 // GrowiContextualSubNavigation for shared page
 // get page info from props not to send request 'GET /page' from client
 type GrowiContextualSubNavigationForSharedPageProps = {
-  page?: IPagePopulatedToShowRevision,
-  isLinkSharingDisabled: boolean,
-}
+  page?: IPagePopulatedToShowRevision;
+  isLinkSharingDisabled: boolean;
+};
 
-const GrowiContextualSubNavigationForSharedPage = (props: GrowiContextualSubNavigationForSharedPageProps): JSX.Element => {
+const GrowiContextualSubNavigationForSharedPage = (
+  props: GrowiContextualSubNavigationForSharedPageProps,
+): JSX.Element => {
   const { page, isLinkSharingDisabled } = props;
 
   return (
-    <GrowiContextualSubNavigationSubstance currentPage={page} isLinkSharingDisabled={isLinkSharingDisabled} />
+    <GrowiContextualSubNavigationSubstance
+      currentPage={page}
+      isLinkSharingDisabled={isLinkSharingDisabled}
+    />
   );
 };
 
@@ -103,7 +126,8 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
   useShowPageSideAuthors(props.showPageSideAuthors);
   useIsContainerFluid(props.isContainerFluid);
 
-  const { trigger: mutateCurrentPage, data: currentPage } = useSWRMUTxCurrentPage();
+  const { trigger: mutateCurrentPage, data: currentPage } =
+    useSWRMUTxCurrentPage();
 
   useEffect(() => {
     if (!props.skipSSR) {
@@ -113,8 +137,12 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
     if (props.shareLink?.relatedPage._id != null && !props.isNotFound) {
       mutateCurrentPage();
     }
-  }, [mutateCurrentPage, props.isNotFound, props.shareLink?.relatedPage._id, props.skipSSR]);
-
+  }, [
+    mutateCurrentPage,
+    props.isNotFound,
+    props.shareLink?.relatedPage._id,
+    props.skipSSR,
+  ]);
 
   const pagePath = props.shareLinkRelatedPage?.path ?? '';
 
@@ -127,8 +155,10 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
       </Head>
 
       <div className="dynamic-layout-root justify-content-between">
-
-        <GrowiContextualSubNavigationForSharedPage page={currentPage ?? props.shareLinkRelatedPage} isLinkSharingDisabled={props.disableLinkSharing} />
+        <GrowiContextualSubNavigationForSharedPage
+          page={currentPage ?? props.shareLinkRelatedPage}
+          isLinkSharingDisabled={props.disableLinkSharing}
+        />
 
         <ShareLinkPageView
           pagePath={pagePath}
@@ -138,7 +168,6 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
           isExpired={props.isExpired}
           disableLinkSharing={props.disableLinkSharing}
         />
-
       </div>
     </>
   );
@@ -153,50 +182,86 @@ SharedPage.getLayout = function getLayout(page) {
   );
 };
 
-function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): void {
+function injectServerConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+): void {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { configManager, searchService } = crowi;
 
-  props.disableLinkSharing = configManager.getConfig('security:disableLinkSharing');
+  props.disableLinkSharing = configManager.getConfig(
+    'security:disableLinkSharing',
+  );
 
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceReachable = searchService.isReachable;
-  props.isSearchScopeChildrenAsDefault = configManager.getConfig('customize:isSearchScopeChildrenAsDefault');
+  props.isSearchScopeChildrenAsDefault = configManager.getConfig(
+    'customize:isSearchScopeChildrenAsDefault',
+  );
 
   props.drawioUri = configManager.getConfig('app:drawioUri');
 
-  props.showPageSideAuthors = configManager.getConfig('customize:showPageSideAuthors');
+  props.showPageSideAuthors = configManager.getConfig(
+    'customize:showPageSideAuthors',
+  );
 
-  props.isLocalAccountRegistrationEnabled = crowi.passportService.isLocalStrategySetup
-    && configManager.getConfig('security:registrationMode') !== RegistrationMode.CLOSED;
+  props.isLocalAccountRegistrationEnabled =
+    crowi.passportService.isLocalStrategySetup &&
+    configManager.getConfig('security:registrationMode') !==
+      RegistrationMode.CLOSED;
 
   props.rendererConfig = {
     isSharedPage: true,
-    isEnabledLinebreaks: configManager.getConfig('markdown:isEnabledLinebreaks'),
-    isEnabledLinebreaksInComments: configManager.getConfig('markdown:isEnabledLinebreaksInComments'),
+    isEnabledLinebreaks: configManager.getConfig(
+      'markdown:isEnabledLinebreaks',
+    ),
+    isEnabledLinebreaksInComments: configManager.getConfig(
+      'markdown:isEnabledLinebreaksInComments',
+    ),
     isEnabledMarp: configManager.getConfig('customize:isEnabledMarp'),
-    adminPreferredIndentSize: configManager.getConfig('markdown:adminPreferredIndentSize'),
+    adminPreferredIndentSize: configManager.getConfig(
+      'markdown:adminPreferredIndentSize',
+    ),
     isIndentSizeForced: configManager.getConfig('markdown:isIndentSizeForced'),
 
     drawioUri: configManager.getConfig('app:drawioUri'),
     plantumlUri: configManager.getConfig('app:plantumlUri'),
 
     // XSS Options
-    isEnabledXssPrevention: configManager.getConfig('markdown:rehypeSanitize:isEnabledPrevention'),
+    isEnabledXssPrevention: configManager.getConfig(
+      'markdown:rehypeSanitize:isEnabledPrevention',
+    ),
     sanitizeType: configManager.getConfig('markdown:rehypeSanitize:option'),
-    customTagWhitelist: crowi.configManager.getConfig('markdown:rehypeSanitize:tagNames'),
-    customAttrWhitelist: configManager.getConfig('markdown:rehypeSanitize:attributes') != null
-      ? JSON.parse(configManager.getConfig('markdown:rehypeSanitize:attributes'))
-      : undefined,
-    highlightJsStyleBorder: crowi.configManager.getConfig('customize:highlightJsStyleBorder'),
+    customTagWhitelist: crowi.configManager.getConfig(
+      'markdown:rehypeSanitize:tagNames',
+    ),
+    customAttrWhitelist:
+      configManager.getConfig('markdown:rehypeSanitize:attributes') != null
+        ? JSON.parse(
+            configManager.getConfig('markdown:rehypeSanitize:attributes'),
+          )
+        : undefined,
+    highlightJsStyleBorder: crowi.configManager.getConfig(
+      'customize:highlightJsStyleBorder',
+    ),
   };
 
-  props.ssrMaxRevisionBodyLength = configManager.getConfig('app:ssrMaxRevisionBodyLength');
+  props.ssrMaxRevisionBodyLength = configManager.getConfig(
+    'app:ssrMaxRevisionBodyLength',
+  );
 }
 
-async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
-  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+async function injectNextI18NextConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+  namespacesRequired?: string[] | undefined,
+): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(
+    serverSideTranslations,
+    context,
+    namespacesRequired,
+  );
   props._nextI18Next = nextI18NextConfig._nextI18Next;
 }
 
@@ -204,17 +269,17 @@ function getAction(props: Props): SupportedActionType {
   let action: SupportedActionType;
   if (props.isExpired) {
     action = SupportedAction.ACTION_SHARE_LINK_EXPIRED_PAGE_VIEW;
-  }
-  else if (props.shareLink == null) {
+  } else if (props.shareLink == null) {
     action = SupportedAction.ACTION_SHARE_LINK_NOT_FOUND;
-  }
-  else {
+  } else {
     action = SupportedAction.ACTION_SHARE_LINK_PAGE_VIEW;
   }
 
   return action;
 }
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const req = context.req as CrowiRequest;
   const { crowi, params } = req;
   const result = await getServerSideCommonProps(context);
@@ -225,29 +290,34 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
   const props: Props = result.props as Props;
 
   try {
-    const shareLink = await ShareLink.findOne({ _id: params.linkId }).populate('relatedPage');
+    const shareLink = await ShareLink.findOne({ _id: params.linkId }).populate(
+      'relatedPage',
+    );
     if (shareLink == null) {
       props.isNotFound = true;
-    }
-    else {
+    } else {
       props.isNotFound = false;
       props.isExpired = shareLink.isExpired();
       props.shareLink = shareLink.toObject();
 
       // retrieve Page
       const Page = crowi.model('Page') as PageModel;
-      const relatedPage = await Page.findOne({ _id: getIdForRef(shareLink.relatedPage) });
+      const relatedPage = await Page.findOne({
+        _id: getIdForRef(shareLink.relatedPage),
+      });
       // determine whether skip SSR
-      const ssrMaxRevisionBodyLength = crowi.configManager.getConfig('app:ssrMaxRevisionBodyLength');
+      const ssrMaxRevisionBodyLength = crowi.configManager.getConfig(
+        'app:ssrMaxRevisionBodyLength',
+      );
 
       if (relatedPage != null) {
         props.skipSSR = await skipSSR(relatedPage, ssrMaxRevisionBodyLength);
         // populate
-        props.shareLinkRelatedPage = await relatedPage.populateDataToShowRevision(props.skipSSR); // shouldExcludeBody = skipSSR
+        props.shareLinkRelatedPage =
+          await relatedPage.populateDataToShowRevision(props.skipSSR); // shouldExcludeBody = skipSSR
       }
     }
-  }
-  catch (err) {
+  } catch (err) {
     logger.error(err);
   }
 

+ 76 - 61
apps/app/src/pages/tags.page.tsx

@@ -1,13 +1,12 @@
-import type { ReactNode, JSX } from 'react';
-import React, { useState, useCallback } from 'react';
-
+import type { JSX, ReactNode } from 'react';
+import React, { useCallback, useState } from 'react';
+import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
+import dynamic from 'next/dynamic';
+import Head from 'next/head';
 import type { IUser } from '@growi/core';
 import { LoadingSpinner } from '@growi/ui/dist/components';
-import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
-import dynamic from 'next/dynamic';
-import Head from 'next/head';
 
 import { BasicLayout } from '~/components/Layout/BasicLayout';
 import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
@@ -15,36 +14,46 @@ import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { IDataTagCount } from '~/interfaces/tag';
-import {
-  useCurrentUser, useIsSearchPage,
-  useIsSearchServiceConfigured, useIsSearchServiceReachable,
-  useIsSearchScopeChildrenAsDefault, useGrowiCloudUri, useCurrentPathname,
-} from '~/stores-universal/context';
 import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 import { useSWRxTagsList } from '~/stores/tag';
-
+import {
+  useCurrentPathname,
+  useCurrentUser,
+  useGrowiCloudUri,
+  useIsSearchPage,
+  useIsSearchScopeChildrenAsDefault,
+  useIsSearchServiceConfigured,
+  useIsSearchServiceReachable,
+} from '~/stores-universal/context';
 
 import type { NextPageWithLayout } from './_app.page';
 import type { CommonProps } from './utils/commons';
 import {
-  getServerSideCommonProps, getNextI18NextConfig, generateCustomTitle, useInitSidebarConfig,
+  generateCustomTitle,
+  getNextI18NextConfig,
+  getServerSideCommonProps,
+  useInitSidebarConfig,
 } from './utils/commons';
 
 const PAGING_LIMIT = 10;
 
 type Props = CommonProps & {
-  currentUser: IUser,
-  isSearchServiceConfigured: boolean,
-  isSearchServiceReachable: boolean,
-  isSearchScopeChildrenAsDefault: boolean,
+  currentUser: IUser;
+  isSearchServiceConfigured: boolean;
+  isSearchServiceReachable: boolean;
+  isSearchScopeChildrenAsDefault: boolean;
 
-  rendererConfig: RendererConfig,
+  rendererConfig: RendererConfig;
 
-  sidebarConfig: ISidebarConfig,
+  sidebarConfig: ISidebarConfig;
 };
 
-const TagList = dynamic(() => import('~/client/components/TagList'), { ssr: false });
-const TagCloudBox = dynamic(() => import('~/client/components/TagCloudBox'), { ssr: false });
+const TagList = dynamic(() => import('~/client/components/TagList'), {
+  ssr: false,
+});
+const TagCloudBox = dynamic(() => import('~/client/components/TagCloudBox'), {
+  ssr: false,
+});
 
 const TagPage: NextPageWithLayout<CommonProps> = (props: Props) => {
   const [activePage, setActivePage] = useState<number>(1);
@@ -91,32 +100,26 @@ const TagPage: NextPageWithLayout<CommonProps> = (props: Props) => {
 
         <div className="main ps-sidebar" data-testid="tags-page">
           <div className="container-lg wide-gutter-x-lg">
-
-            <h2 className="sticky-top py-1">
-              {`${t('Tags')}(${totalCount})`}
-            </h2>
+            <h2 className="sticky-top py-1">{`${t('Tags')}(${totalCount})`}</h2>
 
             <div className="px-3 mb-5 text-center">
               <TagCloudBox tags={tagData} minSize={20} />
             </div>
-            { isLoading
-              ? (
-                <div className="text-muted text-center">
-                  <LoadingSpinner className="mt-3 fs-3" />
-                </div>
-              )
-              : (
-                <div data-testid="grw-tags-list">
-                  <TagList
-                    tagData={tagData}
-                    totalTags={totalCount}
-                    activePage={activePage}
-                    onChangePage={setOffsetByPageNumber}
-                    pagingLimit={PAGING_LIMIT}
-                  />
-                </div>
-              )
-            }
+            {isLoading ? (
+              <div className="text-muted text-center">
+                <LoadingSpinner className="mt-3 fs-3" />
+              </div>
+            ) : (
+              <div data-testid="grw-tags-list">
+                <TagList
+                  tagData={tagData}
+                  totalTags={totalCount}
+                  activePage={activePage}
+                  onChangePage={setOffsetByPageNumber}
+                  pagingLimit={PAGING_LIMIT}
+                />
+              </div>
+            )}
           </div>
         </div>
       </div>
@@ -125,8 +128,8 @@ const TagPage: NextPageWithLayout<CommonProps> = (props: Props) => {
 };
 
 type LayoutProps = Props & {
-  children?: ReactNode
-}
+  children?: ReactNode;
+};
 
 const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
   // init sidebar config with UserUISettings and sidebarConfig
@@ -136,29 +139,31 @@ const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
 };
 
 TagPage.getLayout = function getLayout(page) {
-  return (
-    <Layout {...page.props}>
-      {page}
-    </Layout>
-  );
+  return <Layout {...page.props}>{page}</Layout>;
 };
 
-function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): void {
+function injectServerConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+): void {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
-  const {
-    searchService, configManager,
-  } = crowi;
+  const { searchService, configManager } = crowi;
 
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceReachable = searchService.isReachable;
-  props.isSearchScopeChildrenAsDefault = configManager.getConfig('customize:isSearchScopeChildrenAsDefault');
+  props.isSearchScopeChildrenAsDefault = configManager.getConfig(
+    'customize:isSearchScopeChildrenAsDefault',
+  );
 
   props.sidebarConfig = {
-    isSidebarCollapsedMode: configManager.getConfig('customize:isSidebarCollapsedMode'),
-    isSidebarClosedAtDockMode: configManager.getConfig('customize:isSidebarClosedAtDockMode'),
+    isSidebarCollapsedMode: configManager.getConfig(
+      'customize:isSidebarCollapsedMode',
+    ),
+    isSidebarClosedAtDockMode: configManager.getConfig(
+      'customize:isSidebarClosedAtDockMode',
+    ),
   };
-
 }
 
 /**
@@ -167,12 +172,22 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
  * @param props
  * @param namespacesRequired
  */
-async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
-  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+async function injectNextI18NextConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+  namespacesRequired?: string[] | undefined,
+): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(
+    serverSideTranslations,
+    context,
+    namespacesRequired,
+  );
   props._nextI18Next = nextI18NextConfig._nextI18Next;
 }
 
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const req = context.req as CrowiRequest;
   const { user } = req;
   const result = await getServerSideCommonProps(context);

+ 67 - 38
apps/app/src/pages/trash.page.tsx

@@ -1,10 +1,9 @@
-import type { ReactNode, JSX } from 'react';
-
-import type { IUser } from '@growi/core';
+import type { JSX, ReactNode } from 'react';
 import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
-import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import type { IUser } from '@growi/core';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 
 import { PagePathNavTitle } from '~/components/Common/PagePathNavTitle';
 import { BasicLayout } from '~/components/Layout/BasicLayout';
@@ -12,35 +11,49 @@ import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { ISidebarConfig } from '~/interfaces/sidebar-config';
+import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 import {
-  useCurrentUser, useCurrentPathname, useGrowiCloudUri,
-  useIsSearchServiceConfigured, useIsSearchServiceReachable,
-  useIsSearchScopeChildrenAsDefault, useIsSearchPage, useShowPageLimitationXL,
+  useCurrentPathname,
+  useCurrentUser,
+  useGrowiCloudUri,
+  useIsSearchPage,
+  useIsSearchScopeChildrenAsDefault,
+  useIsSearchServiceConfigured,
+  useIsSearchServiceReachable,
+  useShowPageLimitationXL,
 } from '~/stores-universal/context';
-import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
-
 
 import type { NextPageWithLayout } from './_app.page';
 import type { CommonProps } from './utils/commons';
 import {
-  getServerSideCommonProps, getNextI18NextConfig, generateCustomTitleForPage, useInitSidebarConfig,
+  generateCustomTitleForPage,
+  getNextI18NextConfig,
+  getServerSideCommonProps,
+  useInitSidebarConfig,
 } from './utils/commons';
 
-
-const TrashPageList = dynamic(() => import('~/client/components/TrashPageList').then(mod => mod.TrashPageList), { ssr: false });
-const EmptyTrashModal = dynamic(() => import('~/client/components/EmptyTrashModal'), { ssr: false });
-
+const TrashPageList = dynamic(
+  () =>
+    import('~/client/components/TrashPageList').then(
+      (mod) => mod.TrashPageList,
+    ),
+  { ssr: false },
+);
+const EmptyTrashModal = dynamic(
+  () => import('~/client/components/EmptyTrashModal'),
+  { ssr: false },
+);
 
 type Props = CommonProps & {
-  currentUser: IUser,
-  isSearchServiceConfigured: boolean,
-  isSearchServiceReachable: boolean,
-  isSearchScopeChildrenAsDefault: boolean,
-  showPageLimitationXL: number,
+  currentUser: IUser;
+  isSearchServiceConfigured: boolean;
+  isSearchServiceReachable: boolean;
+  isSearchScopeChildrenAsDefault: boolean;
+  showPageLimitationXL: number;
 
-  rendererConfig: RendererConfig,
+  rendererConfig: RendererConfig;
 
-  sidebarConfig: ISidebarConfig,
+  sidebarConfig: ISidebarConfig;
 };
 
 const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
@@ -87,8 +100,8 @@ const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
 };
 
 type LayoutProps = Props & {
-  children?: ReactNode,
-}
+  children?: ReactNode;
+};
 
 const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
   // init sidebar config with UserUISettings and sidebarConfig
@@ -100,31 +113,37 @@ const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
 TrashPage.getLayout = function getLayout(page) {
   return (
     <>
-      <Layout {...page.props}>
-        {page}
-      </Layout>
+      <Layout {...page.props}>{page}</Layout>
       <EmptyTrashModal />
     </>
   );
 };
 
-function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): void {
+function injectServerConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+): void {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
-  const {
-    searchService, configManager,
-  } = crowi;
+  const { searchService, configManager } = crowi;
 
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceReachable = searchService.isReachable;
-  props.isSearchScopeChildrenAsDefault = configManager.getConfig('customize:isSearchScopeChildrenAsDefault');
-  props.showPageLimitationXL = crowi.configManager.getConfig('customize:showPageLimitationXL');
+  props.isSearchScopeChildrenAsDefault = configManager.getConfig(
+    'customize:isSearchScopeChildrenAsDefault',
+  );
+  props.showPageLimitationXL = crowi.configManager.getConfig(
+    'customize:showPageLimitationXL',
+  );
 
   props.sidebarConfig = {
-    isSidebarCollapsedMode: configManager.getConfig('customize:isSidebarCollapsedMode'),
-    isSidebarClosedAtDockMode: configManager.getConfig('customize:isSidebarClosedAtDockMode'),
+    isSidebarCollapsedMode: configManager.getConfig(
+      'customize:isSidebarCollapsedMode',
+    ),
+    isSidebarClosedAtDockMode: configManager.getConfig(
+      'customize:isSidebarClosedAtDockMode',
+    ),
   };
-
 }
 
 /**
@@ -133,12 +152,22 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
  * @param props
  * @param namespacesRequired
  */
-async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
-  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+async function injectNextI18NextConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+  namespacesRequired?: string[] | undefined,
+): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(
+    serverSideTranslations,
+    context,
+    namespacesRequired,
+  );
   props._nextI18Next = nextI18NextConfig._nextI18Next;
 }
 
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const req = context.req as CrowiRequest;
   const { user } = req;
   const result = await getServerSideCommonProps(context);

+ 42 - 20
apps/app/src/pages/user-activation.page.tsx

@@ -1,8 +1,12 @@
-import type { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
-import { useTranslation } from 'next-i18next';
-import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
+import type {
+  GetServerSideProps,
+  GetServerSidePropsContext,
+  NextPage,
+} from 'next';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
+import { useTranslation } from 'next-i18next';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 
 import { NoLoginLayout } from '~/components/Layout/NoLoginLayout';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
@@ -10,24 +14,28 @@ import type { UserActivationErrorCode } from '~/interfaces/errors/user-activatio
 import type { RegistrationMode } from '~/interfaces/registration-mode';
 import type { ReqWithUserRegistrationOrder } from '~/server/middlewares/inject-user-registration-order-by-token-middleware';
 
-
 import type { CommonProps } from './utils/commons';
 import {
-  getServerSideCommonProps, getNextI18NextConfig, generateCustomTitle,
+  generateCustomTitle,
+  getNextI18NextConfig,
+  getServerSideCommonProps,
 } from './utils/commons';
 
-
-const CompleteUserRegistrationForm = dynamic(() => import('~/client/components/CompleteUserRegistrationForm')
-  .then(mod => mod.CompleteUserRegistrationForm), { ssr: false });
-
+const CompleteUserRegistrationForm = dynamic(
+  () =>
+    import('~/client/components/CompleteUserRegistrationForm').then(
+      (mod) => mod.CompleteUserRegistrationForm,
+    ),
+  { ssr: false },
+);
 
 type Props = CommonProps & {
-  token: string
-  email: string
-  errorCode?: UserActivationErrorCode
-  registrationMode: RegistrationMode
-  isEmailAuthenticationEnabled: boolean
-}
+  token: string;
+  email: string;
+  errorCode?: UserActivationErrorCode;
+  registrationMode: RegistrationMode;
+  isEmailAuthenticationEnabled: boolean;
+};
 
 const UserActivationPage: NextPage<Props> = (props: Props) => {
   const { t } = useTranslation();
@@ -56,12 +64,22 @@ const UserActivationPage: NextPage<Props> = (props: Props) => {
  * @param props
  * @param namespacesRequired
  */
-async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
-  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+async function injectNextI18NextConfigurations(
+  context: GetServerSidePropsContext,
+  props: Props,
+  namespacesRequired?: string[] | undefined,
+): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(
+    serverSideTranslations,
+    context,
+    namespacesRequired,
+  );
   props._nextI18Next = nextI18NextConfig._nextI18Next;
 }
 
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+export const getServerSideProps: GetServerSideProps = async (
+  context: GetServerSidePropsContext,
+) => {
   const result = await getServerSideCommonProps(context);
   const req = context.req as ReqWithUserRegistrationOrder & CrowiRequest;
 
@@ -81,8 +99,12 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
   if (typeof context.query.errorCode === 'string') {
     props.errorCode = context.query.errorCode as UserActivationErrorCode;
   }
-  props.registrationMode = req.crowi.configManager.getConfig('security:registrationMode');
-  props.isEmailAuthenticationEnabled = req.crowi.configManager.getConfig('security:passport-local:isEmailAuthenticationEnabled');
+  props.registrationMode = req.crowi.configManager.getConfig(
+    'security:registrationMode',
+  );
+  props.isEmailAuthenticationEnabled = req.crowi.configManager.getConfig(
+    'security:passport-local:isEmailAuthenticationEnabled',
+  );
 
   await injectNextI18NextConfigurations(context, props, ['translation']);
 

+ 98 - 55
apps/app/src/pages/utils/commons.ts

@@ -1,13 +1,13 @@
+import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import type { ColorScheme, IUserHasId, Locale } from '@growi/core';
-import { Lang, AllLang } from '@growi/core';
+import { AllLang, Lang } from '@growi/core';
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { isServer } from '@growi/core/dist/utils';
-import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import type { SSRConfig, UserConfig } from 'next-i18next';
 
 import * as nextI18NextConfig from '^/config/next-i18next.config';
 
-import { type SupportedActionType } from '~/interfaces/activity';
+import type { SupportedActionType } from '~/interfaces/activity';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { IUserUISettings } from '~/interfaces/user-ui-settings';
@@ -15,45 +15,51 @@ import type { PageDocument } from '~/server/models/page';
 import type { UserUISettingsDocument } from '~/server/models/user-ui-settings';
 import { detectLocaleFromBrowserAcceptLanguage } from '~/server/util/locale-utils';
 import {
-  useCurrentProductNavWidth, useCurrentSidebarContents, usePreferCollapsedMode,
+  useCurrentProductNavWidth,
+  useCurrentSidebarContents,
+  usePreferCollapsedMode,
 } from '~/stores/ui';
 import { getGrowiVersion } from '~/utils/growi-version';
 
 export type CommonProps = {
-  namespacesRequired: string[], // i18next
-  currentPathname: string,
-  appTitle: string,
-  siteUrl: string | undefined,
-  confidential: string,
-  customTitleTemplate: string,
-  csrfToken: string,
-  isContainerFluid: boolean,
-  growiVersion: string,
-  isMaintenanceMode: boolean,
-  redirectDestination: string | null,
-  isDefaultLogo: boolean,
-  growiCloudUri: string | undefined,
-  isAccessDeniedForNonAdminUser?: boolean,
-  currentUser?: IUserHasId,
-  forcedColorScheme?: ColorScheme,
-  userUISettings?: IUserUISettings
+  namespacesRequired: string[]; // i18next
+  currentPathname: string;
+  appTitle: string;
+  siteUrl: string | undefined;
+  confidential: string;
+  customTitleTemplate: string;
+  csrfToken: string;
+  isContainerFluid: boolean;
+  growiVersion: string;
+  isMaintenanceMode: boolean;
+  redirectDestination: string | null;
+  isDefaultLogo: boolean;
+  growiCloudUri: string | undefined;
+  isAccessDeniedForNonAdminUser?: boolean;
+  currentUser?: IUserHasId;
+  forcedColorScheme?: ColorScheme;
+  userUISettings?: IUserUISettings;
 } & Partial<SSRConfig>;
 
 // eslint-disable-next-line max-len
-export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(context: GetServerSidePropsContext) => {
-  const getModelSafely = await import('~/server/util/mongoose-utils').then(mod => mod.getModelSafely);
+export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async (
+  context: GetServerSidePropsContext,
+) => {
+  const getModelSafely = await import('~/server/util/mongoose-utils').then(
+    (mod) => mod.getModelSafely,
+  );
 
   const req = context.req as CrowiRequest;
   const { crowi, user } = req;
-  const {
-    appService, configManager, customizeService, attachmentService,
-  } = crowi;
+  const { appService, configManager, customizeService, attachmentService } =
+    crowi;
 
   const url = new URL(context.resolvedUrl, 'http://example.com');
   const currentPathname = decodeURIComponent(url.pathname);
 
   const isMaintenanceMode = appService.isMaintenanceMode();
 
+  // biome-ignore lint/suspicious/noImplicitAnyLet: ignore
   let currentUser;
   if (user != null) {
     currentUser = user.toObject();
@@ -63,26 +69,31 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
   let redirectDestination: string | null = null;
   if (!crowi.aclService.isGuestAllowedToRead() && currentUser == null) {
     redirectDestination = '/login';
-  }
-  else if (!isMaintenanceMode && currentPathname === '/maintenance') {
+  } else if (!isMaintenanceMode && currentPathname === '/maintenance') {
     redirectDestination = '/';
-  }
-  else if (isMaintenanceMode && !currentPathname.match('/admin/*') && !(currentPathname === '/maintenance')) {
+  } else if (
+    isMaintenanceMode &&
+    !currentPathname.match('/admin/*') &&
+    !(currentPathname === '/maintenance')
+  ) {
     redirectDestination = '/maintenance';
-  }
-  else {
+  } else {
     redirectDestination = null;
   }
 
   const isCustomizedLogoUploaded = await attachmentService.isBrandLogoExist();
-  const isDefaultLogo = crowi.configManager.getConfig('customize:isDefaultLogo') || !isCustomizedLogoUploaded;
+  const isDefaultLogo =
+    crowi.configManager.getConfig('customize:isDefaultLogo') ||
+    !isCustomizedLogoUploaded;
   const forcedColorScheme = crowi.customizeService.forcedColorScheme;
 
   // retrieve UserUISett ings
-  const UserUISettings = getModelSafely<UserUISettingsDocument>('UserUISettings');
-  const userUISettings = user != null && UserUISettings != null
-    ? await UserUISettings.findOne({ user: user._id }).exec()
-    : req.session.uiSettings; // for guests
+  const UserUISettings =
+    getModelSafely<UserUISettingsDocument>('UserUISettings');
+  const userUISettings =
+    user != null && UserUISettings != null
+      ? await UserUISettings.findOne({ user: user._id }).exec()
+      : req.session.uiSettings; // for guests
 
   const props: CommonProps = {
     namespacesRequired: ['translation'],
@@ -92,7 +103,8 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
     confidential: appService.getAppConfidential() || '',
     customTitleTemplate: customizeService.customTitleTemplate,
     csrfToken: req.csrfToken(),
-    isContainerFluid: configManager.getConfig('customize:isContainerFluid') ?? false,
+    isContainerFluid:
+      configManager.getConfig('customize:isContainerFluid') ?? false,
     growiVersion: getGrowiVersion(),
     isMaintenanceMode,
     redirectDestination,
@@ -123,8 +135,12 @@ export const getLangAtServerSide = (req: CrowiRequest): Lang => {
   const { user, headers } = req;
   const { configManager } = req.crowi;
 
-  return user == null ? detectLocaleFromBrowserAcceptLanguage(headers)
-    : (user.lang ?? configManager.getConfig('app:globalLang') ?? Lang.en_US) ?? Lang.en_US;
+  return user == null
+    ? detectLocaleFromBrowserAcceptLanguage(headers)
+    : (user.lang ??
+        configManager.getConfig('app:globalLang') ??
+        Lang.en_US ??
+        Lang.en_US);
 };
 
 // use this function to get locale for html lang attribute
@@ -132,15 +148,19 @@ export const getLocaleAtServerSide = (req: CrowiRequest): Locale => {
   return langMap[getLangAtServerSide(req)];
 };
 
-export const getNextI18NextConfig = async(
-    // 'serverSideTranslations' method should be given from Next.js Page
-    //  because importing it in this file causes https://github.com/isaachinman/next-i18next/issues/1545
-    serverSideTranslations: (
-      initialLocale: string, namespacesRequired?: string[] | undefined, configOverride?: UserConfig | null, extraLocales?: string[] | false
-    ) => Promise<SSRConfig>,
-    context: GetServerSidePropsContext, namespacesRequired?: string[] | undefined, preloadAllLang = false,
+export const getNextI18NextConfig = async (
+  // 'serverSideTranslations' method should be given from Next.js Page
+  //  because importing it in this file causes https://github.com/isaachinman/next-i18next/issues/1545
+  serverSideTranslations: (
+    initialLocale: string,
+    namespacesRequired?: string[] | undefined,
+    configOverride?: UserConfig | null,
+    extraLocales?: string[] | false,
+  ) => Promise<SSRConfig>,
+  context: GetServerSidePropsContext,
+  namespacesRequired?: string[] | undefined,
+  preloadAllLang = false,
 ): Promise<SSRConfig> => {
-
   // determine language
   const req: CrowiRequest = context.req as CrowiRequest;
   const lang = getLangAtServerSide(req);
@@ -155,7 +175,12 @@ export const getNextI18NextConfig = async(
   }
 
   // The first argument must be a language code with an underscore, such as en_US
-  return serverSideTranslations(lang, namespaces, nextI18NextConfig, preloadAllLang ? AllLang : false);
+  return serverSideTranslations(
+    lang,
+    namespaces,
+    nextI18NextConfig,
+    preloadAllLang ? AllLang : false,
+  );
 };
 
 /**
@@ -163,7 +188,10 @@ export const getNextI18NextConfig = async(
  * @param props
  * @param title
  */
-export const generateCustomTitle = (props: CommonProps, title: string): string => {
+export const generateCustomTitle = (
+  props: CommonProps,
+  title: string,
+): string => {
   return props.customTitleTemplate
     .replace('{{sitename}}', props.appTitle)
     .replace('{{pagepath}}', title)
@@ -175,7 +203,10 @@ export const generateCustomTitle = (props: CommonProps, title: string): string =
  * @param props
  * @param pagePath
  */
-export const generateCustomTitleForPage = (props: CommonProps, pagePath: string): string => {
+export const generateCustomTitleForPage = (
+  props: CommonProps,
+  pagePath: string,
+): string => {
   const dPagePath = new DevidedPagePath(pagePath, true, true);
 
   return props.customTitleTemplate
@@ -184,14 +215,23 @@ export const generateCustomTitleForPage = (props: CommonProps, pagePath: string)
     .replace('{{pagename}}', dPagePath.latter);
 };
 
-export const useInitSidebarConfig = (sidebarConfig: ISidebarConfig, userUISettings?: IUserUISettings): void => {
+export const useInitSidebarConfig = (
+  sidebarConfig: ISidebarConfig,
+  userUISettings?: IUserUISettings,
+): void => {
   // UserUISettings
-  usePreferCollapsedMode(userUISettings?.preferCollapsedModeByUser ?? sidebarConfig.isSidebarCollapsedMode);
+  usePreferCollapsedMode(
+    userUISettings?.preferCollapsedModeByUser ??
+      sidebarConfig.isSidebarCollapsedMode,
+  );
   useCurrentSidebarContents(userUISettings?.currentSidebarContents);
   useCurrentProductNavWidth(userUISettings?.currentProductNavWidth);
 };
 
-export const skipSSR = async(page: PageDocument, ssrMaxRevisionBodyLength: number): Promise<boolean> => {
+export const skipSSR = async (
+  page: PageDocument,
+  ssrMaxRevisionBodyLength: number,
+): Promise<boolean> => {
   if (!isServer()) {
     throw new Error('This method is not available on the client-side');
   }
@@ -205,7 +245,10 @@ export const skipSSR = async(page: PageDocument, ssrMaxRevisionBodyLength: numbe
   return ssrMaxRevisionBodyLength < latestRevisionBodyLength;
 };
 
-export const addActivity = async(context: GetServerSidePropsContext, action: SupportedActionType): Promise<void> => {
+export const addActivity = async (
+  context: GetServerSidePropsContext,
+  action: SupportedActionType,
+): Promise<void> => {
   const req = context.req as CrowiRequest;
 
   const parameters = {

+ 3 - 3
apps/app/src/pages/utils/objectid-transformer.ts

@@ -4,7 +4,7 @@ import type ObjectId from 'bson-objectid';
 import superjson from 'superjson';
 
 export const registerTransformerForObjectId = (): void => {
-  superjson.registerCustom<ObjectId|string, string>(
+  superjson.registerCustom<ObjectId | string, string>(
     {
       isApplicable: (v): v is ObjectId => {
         if (v == null) {
@@ -16,8 +16,8 @@ export const registerTransformerForObjectId = (): void => {
         }
         return objectIdUtils.isValidObjectId(v);
       },
-      serialize: v => (typeof v === 'string' ? v : v.toHexString()),
-      deserialize: v => v,
+      serialize: (v) => (typeof v === 'string' ? v : v.toHexString()),
+      deserialize: (v) => v,
     },
     'ObjectidTransformer',
   );

+ 1 - 6
biome.json

@@ -29,13 +29,8 @@
       "!packages/pdf-converter-client/specs",
       "!apps/app/playwright",
       "!apps/app/src/client",
-      "!apps/app/src/components",
       "!apps/app/src/features/openai",
-      "!apps/app/src/pages",
-      "!apps/app/src/server",
-      "!apps/app/src/styles",
-      "!apps/app/test-with-vite",
-      "!apps/app/tmp"
+      "!apps/app/src/server"
     ]
   },
   "formatter": {